Fuse and BaaS

Can it be done?

I’m considering what it would take to get my app to store data in the cloud and communicate with other devices.

Googling around (i’m a total beginner with apps), it looks the easy way to do this is to use a Backend-as-a-Service such as Parse.com.

Now wondering which parse.com SDK to download (there’s one for each platform), and what it would take to hook up my Fuse-app with it…

It is possible to use existing SDKs in C++, Objective-C and Java through the Uno Extension Layer (called UXL). In fact, all the existing native functionality in Fuse is implemented this way.

However, documentation for UXL is not yet published, so it will be challenging to for you at this point in the beta.

We will surely look into providing out-of-the-box bindings for many popular third party services going forward. Parse is high on the wishlist :slight_smile:

UXL is quite advanced to use directly, and it is mostly meant as a layer for binding generators than as something to write by hand , except in exceptional cases.

We have binding generators for both Objective-C and Java. These are used to generate the bindings for the standard iOS and Android APIs which now ship with Fuse. However, these are not yet robust enough to be released as general puprose tools.

Thanks I’ll work on a cool front-end in the mean time :slight_smile:

A good tip from Erik was to use Parse’s REST API: https://parse.com/docs/rest

I plan to proceed by making an .uno file that wraps these calls to emulate the official parse sdk. In other words, I can continue developing :slight_smile:

Look like Parse is a very good solution to integrate external content! Please if you can integrate share examples!

Thanks!

Here’s what I’m at:

Parse.uno

using Uno;
using Uno.Collections;
using Fuse;
using Fuse.Elements;
using Uno.Net.Http;
using Uno.Data.Json;

namespace Parse
{

    //////////////////////////////////////////////
    // EVENTS
    //////////////////////////////////////////////

    // Event that can bubble up the node-tree
    abstract class AbstractEvent : Uno.EventArgs
    {
        public abstract string Message { get; }

        private readonly bool _bubble;
        private readonly Node _node;

        // Raise the event on given context, and bubble up through nodes
        protected AbstractEvent(Node node, bool bubble) : base() {
            _node = node;
            _bubble = bubble;
        }


        protected bool IsHandled { private get; set; }
        public void Raise() {
            assert(!IsHandled);

            // Bubble up through nodes
            for (var n = _node; n != null; n = n.ParentNode) {
                n.Properties.ForeachInList(PropertyHandle, Invoke, this);
                if (IsHandled) return;
                if (!bubble) break;
            }

            // shouldn't get here as long as someone handles the event
            throw new Uno.Exception("Unhandled Parse-Event: " + Message);
        }


        // Private propertyhandle identifying this type of event (for Raise)
        private abstract PropertyHandle PropertyHandle { get; }

        // Private callback to invoke handler and update IsHandled (for Raise)
        private abstract void Invoke(object handler, object args);
    }


    // Event that carries an exception
    public class ExceptionEvent : AbstractEvent {
        public delegate bool Handler(ExceptionEvent args);

        // Exception
        public Parse.Exception Exception { get; private set; }
        public override string Message { get { return Exception.Message; } }

        // Constructor
        public ExceptionEvent(Node node, Parse.Exception e) : base(node, true) {
            Exception = e;
        }
        // Invoke
        private override void Invoke(object handler, object args)
        {
            var handler = (Handler)handler;
            var args = (ExceptionEvent)args_;
            if (handler(args)) IsHandled = true;
        }

        // The propertyhandle
        static readonly PropertyHandle _propertyHandle = Properties.CreateHandle();
        private override PropertyHandle PropertyHandle { 
            get { 
                return _propertyHandle; 
            }
        }
        [Uno.UX.AttachedEventAdder("Parse.ExceptionEvent")]
        public static void AddEventHandler(Node n, Handler handler)
        {
            debug_log "adding " + handler + " to " + n + "'s " + _propertyHandle;
            n.Properties.AddToList(_propertyHandle, handler);
        }
        [Uno.UX.AttachedEventRemover("Parse.ExceptionEvent")]
        public static void RemoveEventHandler(Node n, Handler handler)
        {
            debug_log "removing " + handler + " from " + n;
            n.Properties.RemoveFromList(_propertyHandle, handler);
        }

    }



    //////////////////////////////////////////////
    // EXCEPTIONS
    //////////////////////////////////////////////


    // This is our exception base-class
    public class Exception : Uno.Exception {
        public Exception(string message) : base(message) {}
    }

    // Response could not be converted to expected format
    public class ConversionException : Parse.Exception {
        public ConversionException(string message) : base(message) {}
    }

    // Response status indicates error
    public class ResponseStatusException : Parse.Exception {
        public int Status { get; private set; }
        public object Content { get; private set; }

        public ResponseStatusException(int status, JsonString content) : base(status + ": " + (string)content) 
        {
            Status = status;
            Content = content;
        }

        public ResponseStatusException(int status, string content) : base(status + ": " + content) 
        {
            Status = status;
            Content = content;
        }

        public ResponseStatusException(int status) : base("" + status) 
        {
            Status = status;
        }
    }

    // Got an error-callback
    public class CallbackErrorException : Parse.Exception {
        public CallbackErrorException(string message) : base(message) {}
    }

    // Aborted
    public class CallbackAbortedException : Parse.Exception {
        public CallbackAbortedException() : base("aborted") {}
    }

    // Timed out
    public class CallbackTimeoutException : Parse.Exception {
        public CallbackTimeoutException() : base("timeout") {}
    }



    ////////////////////////////////////////////////////
    // DATA TYPES
    ////////////////////////////////////////////////////

    // Wraps Json and allows returning it as a string
    public class JsonString 
    {
        string _string;
        JsonReader _jsonReader;
        public JsonString(string op) {
            _string = op;
            _jsonReader = JsonReader.Parse(op);
        }
        public static explicit operator string(JsonString op) {
            return op._string;
        }
        public static explicit operator JsonReader(JsonString op) {
            return op._jsonReader;
        }
        public JsonReader this[string key] {
            get { return _jsonReader[key]; }
        }
        public JsonReader this[int index] {
            get { return _jsonReader[index]; }
        }
        public int Count {
            get { return _jsonReader.Count; }
        }
        public string[] Keys {
            get { return _jsonReader.Keys; }
        }
        public bool HasKey(string key) {
            return _jsonReader.HasKey(key);
        }
    }


    ///////////////////////////////////////////////////////////
    // INTERFACE
    ///////////////////////////////////////////////////////////


    // Abstract context
    public abstract class Context
    {
        public abstract Request Get(string path);
        public abstract Request Put(string path);
        public abstract Request Post(string path);
        public abstract Request Patch(string path);
        public abstract Request Delete(string path);
    }

    // abstract request
    public abstract class Request 
    {
        public abstract Request Body(JsonString op, string content_type="application/json");
        public abstract Request Body(string op, string content_type="text/plain");
        public abstract Request Body(byte[] op, string content_type);

        public abstract Request OnDone(Action<JsonString> op);
        public abstract Request OnDone(Action<string> op);
        public abstract Request OnDone(Action<byte[]> op);
        public abstract Request OnDone(Action op);

        public abstract Request Send();
    }

    // client
    public class Client
    {
        ClientImpl _implementation = new ClientImpl();

        public string ApplicationId { 
            get {
                return _implementation.ApplicationId;
            }
            set {
                _implementation.ApplicationId = value;
            }
        }

        public string ClientKey { 
            get {
                return _implementation.ClientKey;
            }
            set {
                _implementation.ClientKey = value;
            }
        }

        public string SessionToken { 
            get {
                return _implementation.SessionToken;
            }
            set {
                _implementation.SessionToken = value;
            }
        }

        public Context CreateContext(Fuse.Node node) {
            return _implementation.CreateContext(node);
        }
    }



    ///////////////////////////////////////////////////////
    // IMPLEMENTATION
    ///////////////////////////////////////////////////////

    class RequestImpl : Request {
        Node _node;
        ClientImpl _client;
        HttpMessageHandlerRequest _httpMessageHandlerRequest;

        public RequestImpl(Node node, ClientImpl client, string method, string path) 
        {
            _node = node;
            _client = client;
            _httpMessageHandlerRequest = _client.HttpMessageHandler.CreateRequest(method, "https://api.parse.com/1/&quot; + path);
            _httpMessageHandlerRequest.SetTimeout(100000);
            _httpMessageHandlerRequest.SetHeader("X-Parse-Application-Id", _client.ApplicationId);
            _httpMessageHandlerRequest.SetHeader("X-Parse-Client-Key", _client.ClientKey);
            if (_client.SessionToken!=null) 
                _httpMessageHandlerRequest.SetHeader("X-Parse-Session-Token", _client.SessionToken);
            if (_client.InstallationId!=null) 
                _httpMessageHandlerRequest.SetHeader("X-Parse-Installation-Id", _client.InstallationId);
            _httpMessageHandlerRequest.Done += InternalCallback_Done;
            _httpMessageHandlerRequest.Error += InternalCallback_Error;
            _httpMessageHandlerRequest.Aborted += InternalCallback_Aborted;
            _httpMessageHandlerRequest.Timeout += InternalCallback_Timeout;
            _httpMessageHandlerRequest.Progress += InternalCallback_Progress;
            _httpMessageHandlerRequest.StateChanged += InternalCallback_StateChanged;
        }


        object _body;
        public override Request Body(JsonString op, string content_type="application/json") 
        {
            _httpMessageHandlerRequest.SetHeader("Content-type", content_type);
            _body = op;
            return this;
        }
        public override Request Body(string op, string content_type="text/plain") 
        {
            _httpMessageHandlerRequest.SetHeader("Content-type", content_type);
            _body = op;
            return this;
        }
        public override Request Body(byte[] op, string content_type) 
        {
            _httpMessageHandlerRequest.SetHeader("Content-type", content_type);
            _body = op;
            return this;
        }


        object _doneCb;
        public override Request OnDone(Action<JsonString> op)
        {
            _httpMessageHandlerRequest.SetResponseType(HttpResponseType.String);
            _doneCb = op;
            return this;
        }
        public override Request OnDone(Action<string> op)
        {
            _httpMessageHandlerRequest.SetResponseType(HttpResponseType.String);
            _doneCb = op;
            return this;
        }
        public override Request OnDone(Action<byte[]> op)
        {
            _httpMessageHandlerRequest.SetResponseType(HttpResponseType.ByteArray);
            _doneCb = op;
            return this;
        }
        public override Request OnDone(Action op)
        {
            _httpMessageHandlerRequest.SetResponseType(HttpResponseType.None);
            _doneCb = op;
            return this;
        }


        public override Request Send() 
        {
            // send with correct body type
            if (_body==null) {
                _httpMessageHandlerRequest.SendAsync();
            } else if (_body is JsonString) {
                _httpMessageHandlerRequest.SendAsync((string)(JsonString)_body);
            } else if (_body is string) {
                _httpMessageHandlerRequest.SendAsync((string)_body);
            } else if (_body is byte[]) {
                _httpMessageHandlerRequest.SendAsync((byte[])_body);
            } else {
                // should not get here
                debug_log "body is of an impossible type!";
                assert(false);
            }
            return this;
        }


        void InternalCallback_Done(HttpMessageHandlerRequest op) {
            assert(op==_httpMessageHandlerRequest);

            // attempt to parse result as json, remembering any exception thrown
            JsonString jsonString = null;
            JsonException jsonException = null;
            try {
                if (_httpMessageHandlerRequest.HttpResponseType == HttpResponseType.String) {
                    jsonString = new JsonString(_httpMessageHandlerRequest.GetResponseContentString());
                }
            } catch (JsonException e) {
                jsonException = e;
            }

            // process
            int status = _httpMessageHandlerRequest.GetResponseStatus();            try // capture Parse.Exceptions and raise them as events
            {
                if (status>=200 && status<300) // request was a success
                {

                    if (_doneCb == null) {} // no callback - nothing to do!
                    else if (_doneCb is Action) // callback expects no response data
                    {                        var cb = (Action)_doneCb;
                        if (_httpMessageHandlerRequest.HttpResponseType == HttpResponseType.None) 
                            cb();
                        else
                            throw new ConversionException("Response received when none was expected");
                    }
                    else if (_doneCb is Action<string>) // callback expects string data
                    {                        var cb = (Action<string>)_doneCb;
                        if (_httpMessageHandlerRequest.HttpResponseType == HttpResponseType.String)
                            cb(_httpMessageHandlerRequest.GetResponseContentString());
                        else
                            throw new ConversionException("Received " + _httpMessageHandlerRequest.HttpResponseType + " when string was expected");
                    }
                    else if (_doneCb is Action<JsonString>)  // callback expects Json data
                    {                        var cb = (Action<JsonString>)_doneCb;
                        if (_httpMessageHandlerRequest.HttpResponseType == HttpResponseType.String && jsonException==null)
                            cb(jsonString);
                        else
                            throw new ConversionException("Received " + _httpMessageHandlerRequest.HttpResponseType + " which could not be parsed as Json");
                    }
                    else if (_doneCb is Action<byte[]>)  // callback expects byte[] data
                    {                        var cb = (Action<byte[]>)_doneCb;
                        if (_httpMessageHandlerRequest.HttpResponseType == HttpResponseType.ByteArray)
                            cb(_httpMessageHandlerRequest.GetResponseContentByteArray());
                        else
                            throw new ConversionException("Received " + _httpMessageHandlerRequest.HttpResponseType + " when byte[] was expected");
                    }
                    else {
                        // should not get here
                        debug_log "_doneCb has an impossible type";
                        assert(false);
                    }
                } 
                else  // request was a failure
                {

                    // try to parse response data as info about the failure, and throw a ResponseStatusException
                    if (jsonString != null) // json in response
                        throw new ResponseStatusException(status, jsonString); 
                    else if (_httpMessageHandlerRequest.HttpResponseType == HttpResponseType.String) // string in response
                        throw new ResponseStatusException(status, _httpMessageHandlerRequest.GetResponseContentString()); 
                    else  // no response
                        throw new ResponseStatusException(status); 

                }
            } 
            catch (Parse.Exception e)  // a parse-exception has been trown, raise an event!
            {
                new ExceptionEvent(_node, e).Raise();
            }

        }

        void InternalCallback_Error(HttpMessageHandlerRequest rq, string error) {
            new ExceptionEvent(_node, new CallbackErrorException(error)).Raise();
        }

        void InternalCallback_Aborted(HttpMessageHandlerRequest rq) {
            new ExceptionEvent(_node, new CallbackAbortedException()).Raise();
        }

        void InternalCallback_Timeout(HttpMessageHandlerRequest rq) {
            new ExceptionEvent(_node, new CallbackTimeoutException()).Raise();
        }
        // do nothing for now
        void InternalCallback_Progress(HttpMessageHandlerRequest rq, int a, int b, bool c) {
            debug_log("progress callback: " + a + ", " + b + ", " + c);
        }
        // do nothing
        void InternalCallback_StateChanged(HttpMessageHandlerRequest rq) {
            debug_log("stateChanged callback, state: " + rq.State);
        }
    }


    class ContextImpl : Context {
        ClientImpl _client;
        Node _node;

        public ContextImpl(ClientImpl client, Node node) {
            _client = client;
            _node = node;
        }

        public override Request Get(string path) {
            return new RequestImpl(_node, _client, "GET", path);
        }

        public override Request Put(string path) {
            return new RequestImpl(_node, _client, "PUT", path);
        }

        public override Request Post(string path) {
            return new RequestImpl(_node, _client, "POST", path);
        }

        public override Request Patch(string path) {
            return new RequestImpl(_node, _client, "PATCH", path);
        }

        public override Request Delete(string path) {
            return new RequestImpl(_node, _client, "DELETE", path);
        }

    }

    // note: forwarding, rather than extension
    class ClientImpl {
        public string ApplicationId { get; set; }
        public string ClientKey { get; set; }
        public string SessionToken { get; set; }
        public string InstallationId { get; set; }

        HttpMessageHandler _httpMessageHandler;
        public HttpMessageHandler HttpMessageHandler { 
            get { 
                if (_httpMessageHandler==null)
                    _httpMessageHandler = new HttpMessageHandler();
                return _httpMessageHandler; 
            }
        }

        public Context CreateContext(Fuse.Node node) {
            return new ContextImpl(this, node);
        }

    }

}

How to use:

  • Instantiate Parse.Client as a global somewhere you can reach it, e.g.:
    <Parse.Client ux:Global="ParseClient" ClientKey="blablabla" ApplicationId="blebleble" />

  • In the code-behinds where you want to do requests to Parse: (the parameter is your panel, which you want errors to start bubbling from)
    Parse.Context _parseContext = MyApp.ParseClient.CreateContext(this);

  • Create a request with _parseContext.Post/.Get/.Put etc, set the body with Body(…), set a done-callback with OnDone(…), send the request with Send():
    _parseContext.Post("objects/messages").Body(body).OnDone(LoginDone).Send();

  • Exceptions are created if anything goes wrong, including if your callback expects a JsonString but the response could not be parsed as such. These exceptions get wrapped in events and bubbled up the Node hierarchy. Any Parse.Exception thrown in your done-callback will also be wrapped in an event and bubble up the hierarchy

  • Implement a function of Parse.ExceptionEvent.Handle to handle such events. You should return true if it was able to handle the event. Assign handlers on any node, like this:
    <Blabla Parse.ExceptionEvent="MyHandler">

  • A lot of things that the Parse SDK does automatically must be done manually, but it’s usually rather trivial to do so. E.g. you have to do the log-in by making the appropriate call and set the SessionToken property accordingly. I haven’t provided an example of a “normal” log-in, because I am using a custom log-in implemented in a cloud function.

Example (my custom log-in… a normal log-in will be very similar):

public Parse.Context _parseContext = MyApp.ParseClient.CreateContext(this);

public void LogIn_Click(object sender, ClickedArgs args) {
    // hide log-in button, show "logging in"
    _logInButton.Visibility = Visibility.Hidden;
    _loggingInCaption.Visibility = Visibility.Visible;

    // attempt to log in with provided access token
    var body = new Parse.JsonString("{ \"fbAccessToken\": \"" + _fbAccessToken + "\" }");
    _parseContext.Post("functions/login").Body(body).OnDone(LoginDone).Send();
}

void LoginDone(Parse.JsonString op) {
    // set session-token
    string sessionToken = op["result"]["sessionToken"].AsString();
    MyApp.ParseClient.SessionToken = sessionToken;

    // hide "logging in", show "done"
    _loggingInCaption.Visibility = Visibility.Hidden;
    _doneCaption.Visibility = Visibility.Visible;
}

I know we have a few examples using Parse in the pipeline. A few should be ready very soon. :slight_smile:

Gloom, good one. Are you making a linkage against one of the official SDKs?

Here’s my code in a gist:

https://gist.github.com/spookysys/4de6765aab0368c358f6