-
Notifications
You must be signed in to change notification settings - Fork 491
Refactored Firebase library #42
Changes from 3 commits
0a613cd
d805dc6
217d58f
b2438cf
d5dd303
23fbbed
2bf3a4f
2df5bd6
077e060
6f65250
33f81ef
2ac7ab3
357a1d0
c65fef7
b286b19
34357a9
f38602f
e8ac1e9
caa70e9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -15,116 +15,158 @@ | |
// | ||
#include "Firebase.h" | ||
|
||
const char* firebaseFingerprint = "7A 54 06 9B DC 7A 25 B3 86 8D 66 53 48 2C 0B 96 42 C7 B3 0A"; | ||
const uint16_t firebasePort = 443; | ||
namespace { | ||
const char* kFirebaseFingerprint = "7A 54 06 9B DC 7A 25 B3 86 8D 66 53 48 2C 0B 96 42 C7 B3 0A"; | ||
const uint16_t kFirebasePort = 443; | ||
} // namespace | ||
|
||
Firebase::Firebase(const String& host) : _host(host) { | ||
_http.setReuse(true); | ||
Firebase::Firebase(const String& host) : connection_(host) { | ||
} | ||
|
||
Firebase& Firebase::auth(const String& auth) { | ||
_auth = auth; | ||
connection_.auth(auth); | ||
return *this; | ||
} | ||
|
||
String Firebase::get(const String& path) { | ||
return sendRequestGetBody("GET", path); | ||
FirebaseResult Firebase::get(const String& path) { | ||
return connection_.sendRequestGetBody("GET", path); | ||
} | ||
|
||
String Firebase::push(const String& path, const String& value) { | ||
return sendRequestGetBody("POST", path, value); | ||
FirebaseResult Firebase::push(const String& path, const String& value) { | ||
return connection_.sendRequestGetBody("POST", path, value); | ||
} | ||
|
||
bool Firebase::remove(const String& path) { | ||
int status = sendRequest("DELETE", path); | ||
return status == HTTP_CODE_OK; | ||
FirebaseResult Firebase::remove(const String& path) { | ||
return connection_.sendRequest("DELETE", path); | ||
} | ||
|
||
Firebase& Firebase::stream(const String& path) { | ||
_error.reset(); | ||
String url = makeURL(path); | ||
/* FirebaseEventStream */ | ||
|
||
FirebaseEventStream::FirebaseEventStream(const String& host) : connection_(host) { | ||
} | ||
|
||
FirebaseEventStream& FirebaseEventStream::auth(const String& auth) { | ||
connection_.auth(auth); | ||
return *this; | ||
} | ||
|
||
FirebaseResult FirebaseEventStream::connect(const String& path) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. what do you think of moving this to the connection class, it's low level enough and that would allow you to get rid of the |
||
String url = connection_.makeURL(path); | ||
auto& http = connection_.httpClient(); | ||
http.setReuse(true); | ||
http.begin(connection_.host().c_str(), kFirebasePort, url.c_str(), true, | ||
kFirebaseFingerprint); | ||
const char* headers[] = {"Location"}; | ||
_http.setReuse(true); | ||
_http.begin(_host.c_str(), firebasePort, url.c_str(), true, firebaseFingerprint); | ||
_http.collectHeaders(headers, 1); | ||
_http.addHeader("Accept", "text/event-stream"); | ||
int statusCode = _http.sendRequest("GET", (uint8_t*)NULL, 0); | ||
http.collectHeaders(headers, 1); | ||
http.addHeader("Accept", "text/event-stream"); | ||
int statusCode = http.sendRequest("GET", (uint8_t*)NULL, 0); | ||
|
||
String location; | ||
// TODO(proppy): Add a max redirect check | ||
while (statusCode == 307) { | ||
location = _http.header("Location"); | ||
_http.setReuse(false); | ||
_http.end(); | ||
_http.setReuse(true); | ||
_http.begin(location, firebaseFingerprint); | ||
statusCode = _http.sendRequest("GET", (uint8_t*)NULL, 0); | ||
while (statusCode == HTTP_CODE_TEMPORARY_REDIRECT) { | ||
location = http.header("Location"); | ||
http.setReuse(false); | ||
http.end(); | ||
http.setReuse(true); | ||
http.begin(location, kFirebaseFingerprint); | ||
statusCode = http.sendRequest("GET", (uint8_t*)NULL, 0); | ||
} | ||
if (statusCode != 200) { | ||
_error.set(statusCode, | ||
"stream " + location + ": " | ||
+ HTTPClient::errorToString(statusCode)); | ||
return FirebaseResult(statusCode); | ||
} | ||
|
||
bool FirebaseEventStream::connected() { | ||
return connection_.httpClient().connected(); | ||
} | ||
|
||
bool FirebaseEventStream::available() { | ||
return connection_.httpClient().getStreamPtr()->available(); | ||
} | ||
|
||
FirebaseEventStream::Event FirebaseEventStream::read(String& event) { | ||
auto client = connection_.httpClient().getStreamPtr(); | ||
Event type; | ||
String typeStr = client->readStringUntil('\n').substring(7); | ||
if (typeStr == "put") { | ||
type = FirebaseEventStream::Event::PUT; | ||
} else if (typeStr == "patch") { | ||
type = FirebaseEventStream::Event::PATCH; | ||
} else { | ||
type = FirebaseEventStream::Event::UNKNOWN; | ||
} | ||
event = client->readStringUntil('\n').substring(6); | ||
client->readStringUntil('\n'); // consume separator | ||
return type; | ||
} | ||
|
||
/* FirebaseConnection */ | ||
|
||
FirebaseConnection::FirebaseConnection(const String& host) : host_(host) { | ||
http_.setReuse(true); | ||
} | ||
|
||
FirebaseConnection& FirebaseConnection::auth(const String& auth) { | ||
auth_ = auth; | ||
return *this; | ||
} | ||
|
||
String Firebase::makeURL(const String& path) { | ||
FirebaseResult FirebaseConnection::sendRequest(const char* method, const String& path) { | ||
return sendRequest(method, path, ""); | ||
} | ||
|
||
FirebaseResult FirebaseConnection::sendRequest(const char* method, const String& path, const String& value) { | ||
const String url = makeURL(path); | ||
http_.begin(host_.c_str(), kFirebasePort, url.c_str(), true, kFirebaseFingerprint); | ||
int statusCode = http_.sendRequest(method, (uint8_t*)value.c_str(), value.length()); | ||
return FirebaseResult(statusCode); | ||
} | ||
|
||
FirebaseResult FirebaseConnection::sendRequestGetBody(const char* method, const String& path) { | ||
return sendRequestGetBody(method, path, ""); | ||
} | ||
|
||
FirebaseResult FirebaseConnection::sendRequestGetBody(const char* method, const String& path, const String& value) { | ||
FirebaseResult result = sendRequest(method, path, value); | ||
return FirebaseResult(result.httpStatus(), http_.getString()); | ||
} | ||
|
||
String FirebaseConnection::makeURL(const String& path) { | ||
String url; | ||
if (path[0] != '/') { | ||
url = "/"; | ||
} | ||
url += path + ".json"; | ||
if (_auth.length() > 0) { | ||
url += "?auth=" + _auth; | ||
if (auth_.length() > 0) { | ||
url += "?auth=" + auth_; | ||
} | ||
return url; | ||
} | ||
|
||
int Firebase::sendRequest(const char* method, const String& path, const String& value) { | ||
String url = makeURL(path); | ||
_http.begin(_host.c_str(), firebasePort, url.c_str(), true, firebaseFingerprint); | ||
int statusCode = _http.sendRequest(method, (uint8_t*)value.c_str(), value.length()); | ||
setError(method, url, statusCode); | ||
return statusCode; | ||
/* FirebaseResult */ | ||
|
||
FirebaseResult::FirebaseResult(int status) : status_(status) { | ||
} | ||
|
||
String Firebase::sendRequestGetBody(const char* method, const String& path, const String& value) { | ||
sendRequest(method, path, value); | ||
if (_error.code() != 0) { | ||
return ""; | ||
} | ||
// no _http.end() because of connection reuse. | ||
return _http.getString(); | ||
FirebaseResult::FirebaseResult(int status, const String& response) | ||
: status_(status), response_(response) { | ||
} | ||
|
||
void Firebase::setError(const char* method, const String& url, int statusCode) { | ||
_error.reset(); | ||
if (statusCode < 0) { | ||
_error.set(statusCode, | ||
String(method) + " " + url + ": " | ||
+ HTTPClient::errorToString(statusCode)); | ||
} | ||
FirebaseResult::FirebaseResult(const FirebaseResult& other) { | ||
status_ = other.status_; | ||
response_ = other.response_; | ||
} | ||
|
||
bool Firebase::connected() { | ||
return _http.connected(); | ||
bool FirebaseResult::isOk() const { | ||
return status_ == HTTP_CODE_OK; | ||
} | ||
|
||
bool Firebase::available() { | ||
return _http.getStreamPtr()->available(); | ||
bool FirebaseResult::isError() const { | ||
return status_ < 0; | ||
} | ||
|
||
Firebase::Event Firebase::read(String& event) { | ||
auto client = _http.getStreamPtr(); | ||
Event type;; | ||
String typeStr = client->readStringUntil('\n').substring(7); | ||
if (typeStr == "put") { | ||
type = Firebase::Event::PUT; | ||
} else if (typeStr == "patch") { | ||
type = Firebase::Event::PATCH; | ||
} else { | ||
type = Firebase::Event::UNKNOWN; | ||
} | ||
event = client->readStringUntil('\n').substring(6); | ||
client->readStringUntil('\n'); // consume separator | ||
return type; | ||
String FirebaseResult::errorMessage() const { | ||
return HTTPClient::errorToString(status_); | ||
} | ||
|
||
const String& FirebaseResult::response() const { | ||
return response_; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -25,53 +25,108 @@ | |
#include <WiFiClientSecure.h> | ||
#include <ESP8266HTTPClient.h> | ||
|
||
// FirebaseError represents a Firebase API error with a code and a | ||
// message. | ||
class FirebaseError { | ||
//TODO(edcoyne) split these into multiple files. | ||
|
||
// Result from call to Firebase backend. ALWAYS check isError() before | ||
// expecting any data. | ||
class FirebaseResult { | ||
public: | ||
operator bool() const { return _code < 0; } | ||
int code() const { return _code; } | ||
const String& message() const { return _message; } | ||
void reset() { set(0, ""); } | ||
void set(int code, const String& message) { | ||
_code = code; | ||
_message = message; | ||
FirebaseResult(int status); | ||
FirebaseResult(int status, const String& response); | ||
FirebaseResult(const FirebaseResult& result); | ||
|
||
// True if there was an error completeing call. | ||
bool isError() const; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I liked the idea to have a different error object you should check like a bool with
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I wonder if we should keep the separate There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sounds good. |
||
String errorMessage() const; | ||
|
||
// True if http status code is 200(OK). | ||
bool isOk() const; | ||
// Message sent back from Firebase backend. | ||
const String& response() const; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What do you think of exposing different result types to the user (maybe in a different PR), but I wanted to discuss that here since this is introducing a new result class. Maybe thru There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think that is a good idea. So this would integrate parsing the json response into our library instead of asking the client to do it. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So for direct key access of leaf value, we wouldn't even need json, because we can just atoi/atof. But for non-leaf value, we have the choice of either a/ returning the raw body and letting the user decode or b/ wrap the decoding. As discussed in #43, b/ is not recommended by |
||
|
||
int httpStatus() const { | ||
return status_; | ||
} | ||
|
||
private: | ||
int _code = 0; | ||
String _message = ""; | ||
int status_; | ||
String response_; | ||
}; | ||
|
||
// Firebase is the connection to firebase. | ||
// Low level connection to Firebase backend, you probably want the | ||
// Firebase class below. | ||
class FirebaseConnection { | ||
public: | ||
FirebaseConnection(const String& host); | ||
FirebaseConnection& auth(const String& auth); | ||
|
||
const String& host() { | ||
return host_; | ||
} | ||
|
||
HTTPClient& httpClient(){ | ||
return http_; | ||
} | ||
|
||
String makeURL(const String& path); | ||
|
||
FirebaseResult sendRequest(const char* method, const String& path, const String& value); | ||
FirebaseResult sendRequest(const char* method, const String& path); | ||
|
||
FirebaseResult sendRequestGetBody(const char* method, const String& path); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. rather than having separate So you would do something like |
||
FirebaseResult sendRequestGetBody(const char* method, const String& path, const String& value); | ||
|
||
private: | ||
HTTPClient http_; | ||
const String host_; | ||
String auth_; | ||
}; | ||
|
||
// Primary client to the Firebase backend. | ||
class Firebase { | ||
public: | ||
Firebase(const String& host); | ||
Firebase& auth(const String& auth); | ||
const FirebaseError& error() const { | ||
return _error; | ||
} | ||
String get(const String& path); | ||
String push(const String& path, const String& value); | ||
bool remove(const String& path); | ||
bool connected(); | ||
Firebase& stream(const String& path); | ||
bool available(); | ||
|
||
// Fetch result at "path" to a local variable. If the value is too large you will exceed | ||
// local memory. | ||
FirebaseResult get(const String& path); | ||
|
||
// Add new value to list at "path", will return child name of new item. | ||
FirebaseResult push(const String& path, const String& value); | ||
|
||
// Deletes value at "path" from server. | ||
FirebaseResult remove(const String& path); | ||
|
||
private: | ||
FirebaseConnection connection_; | ||
}; | ||
|
||
// Listens on a stream of events from Firebase backend. | ||
class FirebaseEventStream { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'd love if we could keep the stream method attached to the main If we decouple the method for making request and getting result it seems that most of the stream interface could apply to regular request, and the real different would be that you can ex: |
||
public: | ||
enum Event { | ||
UNKNOWN, | ||
PUT, | ||
PATCH | ||
}; | ||
|
||
FirebaseEventStream(const String& host); | ||
FirebaseEventStream& auth(const String& auth); | ||
|
||
// Connect to backend and start receiving events. | ||
FirebaseResult connect(const String& path); | ||
// Read next event in stream. | ||
Event read(String& event); | ||
|
||
// True if connected to backend. | ||
bool connected(); | ||
|
||
// True if there is an event available. | ||
bool available(); | ||
|
||
private: | ||
String makeURL(const String& path); | ||
int sendRequest(const char* method, const String& path, const String& value = ""); | ||
String sendRequestGetBody(const char* method, const String& path, const String& value = ""); | ||
void setError(const char* method, const String& url, int status_code); | ||
|
||
HTTPClient _http; | ||
String _host; | ||
String _auth; | ||
FirebaseError _error; | ||
FirebaseConnection connection_; | ||
}; | ||
|
||
#endif // firebase_h |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What do you think of making reading/decoding optional.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
While these examples flow very well and look great the problem is this style assumes the calls succeed, which isn't great for remote services. I think the resulting use will be uglier but safer if we encourage checking for errors.
That said I agree whole heartedly with making reading/decoding optional, the return value for a lot of these calls seems unnecessary (like echoing back the value on a put)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I tried to add explicit error checking after each call in every sample, but I agree with the sentiment.
An intermediate result object make things more discoverable (they have to lookup the doc for the result, and then it's easier for them to find the error methods here, rather than when it's hidden inside the main class), but they still need to remember to call
isError()
on it, and a good way to "learn" that pattern is to repeat it across sample.