-
Notifications
You must be signed in to change notification settings - Fork 492
Refactored Firebase library #42
Changes from 8 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,143 @@ | |
// | ||
#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; | ||
|
||
Firebase::Firebase(const String& host) : _host(host) { | ||
_http.setReuse(true); | ||
String makeUrl(const String& path, const String& auth) { | ||
String url; | ||
if (path[0] != '/') { | ||
url = "/"; | ||
} | ||
url += path + ".json"; | ||
if (auth.length() > 0) { | ||
url += "?auth=" + auth; | ||
} | ||
return url; | ||
} | ||
|
||
} // namespace | ||
|
||
Firebase::Firebase(const String& host) : host_(host) { | ||
http_.setReuse(true); | ||
} | ||
|
||
Firebase& Firebase::auth(const String& auth) { | ||
_auth = auth; | ||
auth_ = auth; | ||
return *this; | ||
} | ||
|
||
String Firebase::get(const String& path) { | ||
return sendRequestGetBody("GET", path); | ||
FirebaseCall Firebase::get(const String& path) { | ||
return FirebaseCall(host_, auth_, "GET", path, &http_); | ||
} | ||
|
||
String Firebase::push(const String& path, const String& value) { | ||
return sendRequestGetBody("POST", path, value); | ||
FirebaseCall Firebase::push(const String& path, const String& value) { | ||
return FirebaseCall(host_, auth_, "POST", path, value, &http_); | ||
} | ||
|
||
bool Firebase::remove(const String& path) { | ||
int status = sendRequest("DELETE", path); | ||
return status == HTTP_CODE_OK; | ||
FirebaseCall Firebase::remove(const String& path) { | ||
return FirebaseCall(host_, auth_, "DELETE", path, &http_); | ||
} | ||
|
||
Firebase& Firebase::stream(const String& path) { | ||
_error.reset(); | ||
String url = makeURL(path); | ||
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); | ||
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); | ||
} | ||
if (statusCode != 200) { | ||
_error.set(statusCode, | ||
"stream " + location + ": " | ||
+ HTTPClient::errorToString(statusCode)); | ||
} | ||
return *this; | ||
FirebaseEventStream Firebase::stream(const String& path) { | ||
return FirebaseEventStream(host_, auth_, path); | ||
} | ||
|
||
String Firebase::makeURL(const String& path) { | ||
String url; | ||
if (path[0] != '/') { | ||
url = "/"; | ||
} | ||
url += path + ".json"; | ||
if (_auth.length() > 0) { | ||
url += "?auth=" + _auth; | ||
/* FirebaseCall */ | ||
|
||
FirebaseCall::FirebaseCall(const String& host, const String& auth, | ||
const char* method, const String& path, const String& value, | ||
HTTPClient* http) : http_(http) { | ||
const String url = makeUrl(path, auth); | ||
http_->begin(host.c_str(), kFirebasePort, url.c_str(), true, kFirebaseFingerprint); | ||
status_ = http_->sendRequest(method, (uint8_t*)value.c_str(), value.length()); | ||
if (isError()) { | ||
error_message_ = String(method) + " " + url + ": " + HTTPClient::errorToString(status_); | ||
} | ||
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; | ||
FirebaseCall::FirebaseCall(const String& host, const String& auth, | ||
const char* method, const String& path, | ||
HTTPClient* http) : FirebaseCall(host, auth, method, path, "", http) { | ||
} | ||
|
||
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(); | ||
bool FirebaseCall::isOk() const { | ||
return status_ == HTTP_CODE_OK; | ||
} | ||
|
||
bool FirebaseCall::isError() const { | ||
return status_ < 0; | ||
} | ||
|
||
String FirebaseCall::errorMessage() const { | ||
return error_message_; | ||
} | ||
|
||
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)); | ||
String FirebaseCall::rawResponse() { | ||
return http_->getString(); | ||
} | ||
|
||
/* FirebaseEventStream */ | ||
|
||
FirebaseEventStream::FirebaseEventStream(const String& host, const String& auth, | ||
const String& path) { | ||
const String url = makeUrl(path, auth); | ||
http_.setReuse(true); | ||
http_.begin(host.c_str(), kFirebasePort, url.c_str(), true, | ||
kFirebaseFingerprint); | ||
const char* headers[] = {"Location"}; | ||
http_.collectHeaders(headers, 1); | ||
http_.addHeader("Accept", "text/event-stream"); | ||
status_ = http_.sendRequest("GET", (uint8_t*)NULL, 0); | ||
|
||
String location; | ||
// TODO(proppy): Add a max redirect check | ||
while (status_ == HTTP_CODE_TEMPORARY_REDIRECT) { | ||
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 do this in the stream() call so we can get some early failure, and keep the dedicated client only for the request after the last redirect. 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. It seems essentially the same. You will get the failure at the same time, when you check this object. |
||
location = http_.header("Location"); | ||
http_.setReuse(false); | ||
http_.end(); | ||
http_.setReuse(true); | ||
http_.begin(location, kFirebaseFingerprint); | ||
status_ = http_.sendRequest("GET", (uint8_t*)NULL, 0); | ||
} | ||
|
||
if (status_ != 200) { | ||
error_message_ = "stream " + location + ": " | ||
+ HTTPClient::errorToString(status_); | ||
} | ||
} | ||
|
||
bool Firebase::connected() { | ||
return _http.connected(); | ||
bool FirebaseEventStream::connected() { | ||
return http_.connected(); | ||
} | ||
|
||
bool Firebase::available() { | ||
return _http.getStreamPtr()->available(); | ||
bool FirebaseEventStream::available() { | ||
return http_.getStreamPtr()->available(); | ||
} | ||
|
||
Firebase::Event Firebase::read(String& event) { | ||
auto client = _http.getStreamPtr(); | ||
Event type;; | ||
FirebaseEventStream::Event FirebaseEventStream::read(String& event) { | ||
auto client = http_.getStreamPtr(); | ||
Event type; | ||
String typeStr = client->readStringUntil('\n').substring(7); | ||
if (typeStr == "put") { | ||
type = Firebase::Event::PUT; | ||
type = FirebaseEventStream::Event::PUT; | ||
} else if (typeStr == "patch") { | ||
type = Firebase::Event::PATCH; | ||
type = FirebaseEventStream::Event::PATCH; | ||
} else { | ||
type = Firebase::Event::UNKNOWN; | ||
type = FirebaseEventStream::Event::UNKNOWN; | ||
} | ||
event = client->readStringUntil('\n').substring(6); | ||
client->readStringUntil('\n'); // consume separator | ||
return type; | ||
} | ||
|
||
bool FirebaseEventStream::isError() const { | ||
return status_ < 0; | ||
} | ||
|
||
String FirebaseEventStream::errorMessage() const { | ||
return error_message_; | ||
} | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -25,53 +25,97 @@ | |
#include <WiFiClientSecure.h> | ||
#include <ESP8266HTTPClient.h> | ||
|
||
// FirebaseError represents a Firebase API error with a code and a | ||
// message. | ||
class FirebaseError { | ||
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; | ||
} | ||
private: | ||
int _code = 0; | ||
String _message = ""; | ||
}; | ||
//TODO(edcoyne) split these into multiple files. | ||
|
||
class FirebaseCall; | ||
class FirebaseEventStream; | ||
|
||
// Firebase is the connection to firebase. | ||
// Primary client to the Firebase backend. | ||
class Firebase { | ||
public: | ||
Firebase(const String& host); | ||
Firebase& auth(const String& auth); | ||
const FirebaseError& error() const { | ||
return _error; | ||
|
||
// Fetch result at "path" to a local variable. If the value is too large you will exceed | ||
// local memory. | ||
FirebaseCall get(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. another thing is that if we return a copy of the FirebaseCall object, user do have to worry about its lifetime:
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. The problem with not handing the Call object back to the client is the lifetime of the call object is bound now to the lifetime of the Firebase object. One should be short lived and one long lived. For example, if you call Firebase.get() then never read the response we can never clear the response from the buffer because we don't know if you will ask for it 3 hours later. However if the client owns the FirebaseCall object and deletes it, we can then clear the response from the buffer because it will never be read. Let me ponder on it for a bit and see if there is a way we can get the ease of use that you want with the lifetime isolation I want. 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 agree with this generally, but given that the interaction between the arduino sketch and the lib is not likely to be concurrent (1 request at a time for a given Firebase object). Do you still see this as an issue if we hold the buffer until the next request (and overwrite it)?. The esp8266 http lib is currently modeled that way: 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 there are a couple of issues with holding it even assuming non-concurrency. One is memory usage, on a memory constrained device having potentially large chunks of memory sitting unused can be an issue. The other is if I remember TCP/HTTP correctly than on a large request part of this will actually be held on the server so this could have larger effects. (i.e. if memory serves, i should verify, but if the client buffer is full the server will buffer the rest until the client resumes reading. |
||
|
||
// Add new value to list at "path", will return child name of new item. | ||
FirebaseCall push(const String& path, const String& value); | ||
|
||
// Deletes value at "path" from server. | ||
FirebaseCall remove(const String& path); | ||
|
||
// Starts a stream of events that effect object at "path". | ||
FirebaseEventStream stream(const String& path); | ||
|
||
private: | ||
HTTPClient http_; | ||
String host_; | ||
String auth_; | ||
}; | ||
|
||
class FirebaseCall { | ||
public: | ||
FirebaseCall(const String& host, const String& auth, | ||
const char* method, const String& path, const String& value, | ||
HTTPClient* http); | ||
FirebaseCall(const String& host, const String& auth, | ||
const char* method, const String& path, | ||
HTTPClient* http); | ||
|
||
|
||
// True if there was an error completing 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 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. This pulls value to local memory, | ||
// be careful if value can be large. | ||
String rawResponse(); | ||
|
||
int httpStatus() const { | ||
return status_; | ||
} | ||
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(); | ||
|
||
private: | ||
FirebaseCall(HTTPClient* http); | ||
|
||
HTTPClient* http_; | ||
|
||
int status_; | ||
String error_message_; | ||
}; | ||
|
||
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: | ||
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. We should probably allow a lazy construction of the |
||
enum Event { | ||
UNKNOWN, | ||
PUT, | ||
PATCH | ||
}; | ||
|
||
FirebaseEventStream(const String& host, const String& auth, 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(); | ||
|
||
// True if there was an error. | ||
bool isError() const; | ||
String errorMessage() const; | ||
|
||
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; | ||
HTTPClient http_; | ||
int status_; | ||
String error_message_; | ||
}; | ||
|
||
#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.
FirebaseStreamResult?
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.
Well since it is an interactive object it seemed like less of a "result" but this may be to your point of not naming any of them "result" because they may be interactive I. the future with laziness.