Skip to content
This repository was archived by the owner on Mar 17, 2025. It is now read-only.

Refactored Firebase library #42

Merged
merged 19 commits into from
Jan 29, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
177 changes: 104 additions & 73 deletions Firebase.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -15,114 +15,145 @@
//
#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 makeFirebaseURL(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) {
sendRequest("GET", path);
return readBody();
FirebaseGet Firebase::get(const String& path) {
return FirebaseGet(host_, auth_, path, &http_);
}

String Firebase::push(const String& path, const String& value) {
sendRequest("POST", path, value);
return readBody();
FirebasePush Firebase::push(const String& path, const String& value) {
return FirebasePush(host_, auth_, path, value, &http_);
}

void Firebase::remove(const String& path) {
sendRequest("DELETE", path);
FirebaseRemove Firebase::remove(const String& path) {
return FirebaseRemove(host_, auth_, path, &http_);
}

String Firebase::set(const String& path, const String& value) {
sendRequest("PUT", path, value);
return readBody();
FirebaseStream Firebase::stream(const String& path) {
// TODO: create new client dedicated to stream.
return FirebaseStream(host_, auth_, 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);
// FirebaseCall
FirebaseCall::FirebaseCall(const String& host, const String& auth,
const char* method, const String& path,
const String& data, HTTPClient* http) : http_(http) {
String url = makeFirebaseURL(path, auth);
http_->setReuse(true);
http_->begin(host, kFirebasePort, url, true, kFirebaseFingerprint);

bool followRedirect = false;
if (method == "STREAM") {
method = "GET";
http_->addHeader("Accept", "text/event-stream");
followRedirect = true;
}
if (statusCode != 200) {
_error.set(statusCode,
"stream " + location + ": "
+ HTTPClient::errorToString(statusCode));

if (followRedirect) {
const char* headers[] = {"Location"};
http_->collectHeaders(headers, 1);
}
return *this;
}

int status = http_->sendRequest(method, (uint8_t*)data.c_str(), data.length());

String Firebase::makeURL(const String& path) {
String url;
if (path[0] != '/') {
url = "/";
// TODO: Add a max redirect check
if (followRedirect) {
while (status == HTTP_CODE_TEMPORARY_REDIRECT) {
String location = http_->header("Location");
http_->setReuse(false);
http_->end();
http_->setReuse(true);
http_->begin(location, kFirebaseFingerprint);
status = http_->sendRequest("GET", (uint8_t*)NULL, 0);
}
}
url += path + ".json";
if (_auth.length() > 0) {
url += "?auth=" + _auth;

if (status != 200) {
error_ = FirebaseError(status, String(method) + " " + url + ": " + HTTPClient::errorToString(status));
}

// if not streaming.
if (!followRedirect) {
response_ = http_->getString();
}
return url;
}

void 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());
_error.reset();
if (statusCode < 0) {
_error.set(statusCode,
String(method) + " " + url + ": "
+ HTTPClient::errorToString(statusCode));
}
// FirebaseGet
FirebaseGet::FirebaseGet(const String& host, const String& auth,
const String& path,
HTTPClient* http)
: FirebaseCall(host, auth, "GET", path, "", http) {

if (!error()) {
// TODO: parse json
json_ = response();
}
}

String Firebase::readBody() {
if (_error.code() != 0) {
return "";
// FirebasePush
FirebasePush::FirebasePush(const String& host, const String& auth,
const String& path, const String& value,
HTTPClient* http)
: FirebaseCall(host, auth, "POST", path, value, http) {
if (!error()) {
// TODO: parse name
name_ = response();
}
// no _http.end() because of connection reuse.
return _http.getString();
}

bool Firebase::connected() {
return _http.connected();
// FirebasePush
FirebaseRemove::FirebaseRemove(const String& host, const String& auth,
const String& path,
HTTPClient* http)
: FirebaseCall(host, auth, "DELETE", path, "", http) {
}

// FirebaseStream
FirebaseStream::FirebaseStream(const String& host, const String& auth,
const String& path,
HTTPClient* http)
: FirebaseCall(host, auth, "STREAM", path, "", http) {
}

bool Firebase::available() {
return _http.getStreamPtr()->available();
bool FirebaseStream::available() {
return http_->getStreamPtr()->available();
}

Firebase::Event Firebase::read(String& event) {
auto client = _http.getStreamPtr();
Event type;;
FirebaseStream::Event FirebaseStream::read(String& event) {
auto client = http_->getStreamPtr();
Event type;
String typeStr = client->readStringUntil('\n').substring(7);
if (typeStr == "put") {
type = Firebase::Event::PUT;
type = Event::PUT;
} else if (typeStr == "patch") {
type = Firebase::Event::PATCH;
type = Event::PATCH;
} else {
type = Firebase::Event::UNKNOWN;
type = Event::UNKNOWN;
}
event = client->readStringUntil('\n').substring(6);
client->readStringUntil('\n'); // consume separator
Expand Down
143 changes: 108 additions & 35 deletions Firebase.h
Original file line number Diff line number Diff line change
Expand Up @@ -25,56 +25,129 @@
#include <WiFiClientSecure.h>
#include <ESP8266HTTPClient.h>

// FirebaseError represents a Firebase API error with a code and a
// message.
class FirebaseGet;
class FirebasePush;
class FirebaseRemove;
class FirebaseStream;

// Primary client to the Firebase backend.
class Firebase {
public:
Firebase(const String& host);
Firebase& auth(const String& auth);

// Fetch result at "path".
FirebaseGet get(const String& path);

// Add new value to list at "path", will return key for the new item.
FirebasePush push(const String& path, const String& value);

// Deletes value at "path" from firebase.
FirebaseRemove remove(const String& path);

// Starts a stream of events that affect object at "path".
// TODO: fix FirebaseStream lifecycle
// https://github.com/esp8266/Arduino/issues/500
FirebaseStream stream(const String& path);

private:
HTTPClient http_;
String host_;
String auth_;
};

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;
FirebaseError() {}
FirebaseError(int code, const String& message) : code_(code), message_(message) {
}
operator bool() const { return code_ != 0; }
int code() const { return code_; }
const String& message() const { return message_; }
private:
int code_ = 0;
String message_ = "";
};

class FirebaseCall {
public:
FirebaseCall() {}
FirebaseCall(const String& host, const String& auth,
const char* method, const String& path,
const String& data = "",
HTTPClient* http = NULL);
const FirebaseError& error() const {
return error_;
}
const String& response() {
return response_;
}
protected:
HTTPClient* http_;
FirebaseError error_;
String response_;
};

class FirebaseGet : public FirebaseCall {
public:
FirebaseGet() {}
FirebaseGet(const String& host, const String& auth,
const String& path, HTTPClient* http = NULL);

const String& json() const {
return json_;
}

private:
int _code = 0;
String _message = "";
String json_;
};

// Firebase is the connection to firebase.
class Firebase {
class FirebasePush : public FirebaseCall {
public:
Firebase(const String& host);
Firebase& auth(const String& auth);
const FirebaseError& error() const {
return _error;
FirebasePush() {}
FirebasePush(const String& host, const String& auth,
const String& path, const String& value, HTTPClient* http = NULL);

const String& name() const {
return name_;
}
String get(const String& path);
// write a new JSON `value` to the given `path`.
// Note: A String `value` must include double quotes to be valid json.
String set(const String& path, const String& value);
// append a new JSON `value` to the given `path`.
// Note: A String `value` must include double quotes to be valid json.
String push(const String& path, const String& value);
void remove(const String& path);
bool connected();
Firebase& stream(const String& path);

private:
String name_;
};

class FirebaseRemove : public FirebaseCall {
public:
FirebaseRemove() {}
FirebaseRemove(const String& host, const String& auth,
const String& path, HTTPClient* http = NULL);
};


class FirebaseStream : public FirebaseCall {
public:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should probably allow a lazy construction of the FirebaseStream object (and even the other type) through a begin() method, that seems to be a popular pattern in the Arduino world, and would solve some of the lifecycle headache.

FirebaseStream() {}
FirebaseStream(const String& host, const String& auth,
const String& path, HTTPClient* http = NULL);

// True if there is an event available.
bool available();

// event type.
enum Event {
UNKNOWN,
PUT,
PATCH
};
Event read(String& event);
private:
String makeURL(const String& path);
void sendRequest(const char* method, const String& path, const String& value = "");
String readBody();

HTTPClient _http;
String _host;
String _auth;
// Read next event in stream.
Event read(String& event);

const FirebaseError& error() const {
return _error;
}

private:
FirebaseError _error;
};

Expand Down
Loading