ufront / ufront-mvc

The core MVC framework that powers ufront
MIT License
17 stars 15 forks source link

How to use UFApi remoting? #9

Open tiagolr opened 9 years ago

tiagolr commented 9 years ago

Hey, i have a server UFApi class like so:

class SignupApi extends UFApi { 
    public function test():String {
        return "returned!";
    }
}

and i have a JS client that tries to call the server method like so:

static public function main() {
    var sign:SignupApi = new SignupApi();
    trace("returned? " + sign.test());
}

The resulting trace is returned? null, I'm probably missing something as the return should be asynchronous, how can i fix this client? Thanks!

jasononeil commented 9 years ago

Hi,

To be honest, the code is in there but I haven't used this style of remoting in production yet, so it is not thoroughly tested. Here are my notes so far:

As I said, the code is in there, it compiles, but I'm not actually using it anywhere, so it is not thoroughly tested, and as you can see, not really documented either :P

There is a different way that is closer to the old school Haxe remoting, (with an async callback function as the final paremeter), which I can explain if you'd prefer that approach. I do use that one in production, but I'm hoping to move away from it because I prefer working with futures, and this will give more seamless client/server code sharing.

Jason

On Thu, Feb 26, 2015 at 9:37 AM, TiagoLr notifications@github.com wrote:

Hey, i have a server UFApi class like so:

class SignupApi extends UFApi { public function test():String { return "returned!"; } }

and i have a JS client that tries to call the server method like so:

static public function main() { var sign:SignupApi = new SignupApi(); trace("returned? " + sign.test()); }

The resulting trace is returned? null, I'm probably missing something as the return should be asynchronous, how can i fix this client? Thanks!

— Reply to this email directly or view it on GitHub https://github.com/ufront/ufront-mvc/issues/9.

tiagolr commented 9 years ago

Hey, thanks for the explanation.

To be honest i rather use normal ufront routes with ajax requests or configure standard haxe remoting for now, at least until such feature is working and has some reference, already experimenting with a lot of stuff at the moment and need to keep it simple.

Anyway great work so far with ufront, the routing and orm have been great to use so far!

tiagolr commented 9 years ago

By the way how do you mix haxe classic remoting with ufront? I am not sure where should i put these:

var ctx = new haxe.remoting.Context().ctx.addObject("MyObjs", new MyObj());
haxe.remoting.HttpConnection.handleRequest(ctx);

i think those two are all thats needed for remoting to run on the server, then i have a snippet for the client side (which does not use ufront so far).

If i can get remoting + ufront working, maybe i can use it for a client backoffice I'm starting this week, instead of ajax requests to UF routes, which are not bad, but remoting may make it simpler for this case.

jasononeil commented 9 years ago

So I've extracted out the important bits for an app that uses this style or remoting. Hopefully it's a complete enough example that you can follow along, let me know how you go.

First of all, I have an "UFApiContext" that defines which "UFApi" classes are available for remoting. I put it in app/Api.hx:

package app;

import ufront.api.UFApiContext;
import app.api.*;

class Api extends UFApiContext
{
    public var setupApi:SetupApi; // These are all UFApi objects
    public var signupApi:SignupApi;
    public var loginApi:LoginApi;
}

Then in my Server.hx file, I let it know to use this Api context for remoting:

var ufApp = new UfrontApplication({
    indexController: app.Routes,
    remotingApi: app.Api,
    logFile: "log/app.log"
});

Then on my client, I have something like this:

import app.Api;
class Client
{
    /** Launch the remoting API.  So you call Client.remoting.loginApi.attemptLogin(...) */
    // Note 1: `app.ApiClient` is automatically generated by the build macro in `app.Api`
    // Note 2: This doesn't actually need to point to /remoting/, it just makes it easier to debug which HTTP connections are Haxe remoting...
    public static var remoting = new app.ApiClient("/remoting/", processRemotingError);

    public static function main() {
        // You make remoting calls like this...
        Client.remoting.loginAPI.getCurrentUser(function (currentUser) {
            trace( 'You are logged in as $currentUser' );
        });
    }

    // The error handler let's you know what kind of error was encountered.
    // Here is a fairly verbose one I use in my app.
    static function processRemotingError(error:RemotingError<Dynamic>)
    {
        var msg;
        switch error {
            case HttpError( callString, responseCode, responseData ):
                msg = 'Remoting error during $callString';
                msg += '\n  Type: HTTP Error ($responseCode)';
                msg += '\n  Data: $responseData';
            case ServerSideException( callString, e, stack ):
                stack = '    '+stack.replace("\n","\n    ");
                msg = 'Remoting error during $callString';
                msg += '\n  Type: Server Side Exception';
                msg += '\n  Exception: $e';
                msg += '\n  Stack: $stack';
            case ClientCallbackException( callString, e ):
                msg = 'Remoting error during $callString';
                msg += '\n  Type: Exception thrown after remoting call, and during callback on client.';
                msg += '\n  Error: $e';
                msg += '\n  Note: If you compile with -debug, you can let this exception by handled by your browser\'s debugger';
            case UnserializeFailed( callString, troubleLine, error ):
                msg = 'Remoting error during $callString';
                msg += '\n  Type: Failed to unserialize response';
                msg += '\n  Trouble Line: $troubleLine';
                msg += '\n  Error: $error';
            case NoRemotingResult( callString, responseData ):
                msg = 'Remoting error during $callString';
                msg += '\n  Type: No "hxr" remoting response line in data';
                msg += '\n  Data: $responseData';
            case ApiFailure( callString, err ):
                msg = 'Remoting API call $callString returned a Failure';
                msg += '\n  Failure data: $err';
            case UnknownException( e ):
                msg = 'Unknown exception during remoting call $e';
                msg += '\n  The error was not wrapped in a RemotingError, unsure how it was found using HttpAsyncConnectionWithTraces. ';
                msg += '\n  Please consider filing a bug report.';
        }
        trace( msg );
    }
}
kevinresol commented 9 years ago

Any examples of using remoting in a ClientJSApplication?

@:route(POST, "/login")
public function processLogin(args:{username:String, password:String}):ActionResult
{
  loginApi.attemptLogin(...);
  return ViewResult({success: ...});
}

The above code is what I use if it is on the server. And my question is as below, what would be the code for the client?

// suppose the request is intercepted by a pushstate button and reach here at client-side
@:route(POST, "/login")
public function processLogin(args:{username:String, password:String}):ActionResult
{
  asyncLoginApi.attemptLogin(...) >> function(user) 
  {
     // what can I do here to trigger a page change?
  }
  // what to return?
}
kevinresol commented 9 years ago

What I exactly wanted to do:

  1. User click a button "Login" (I can do this, simply html)
  2. On client side it send the request to server via haxe remoting (I can do this, using UFAsyncApi)
  3. Don't trigger a page change yet (I don't know how to do this with ufront)
  4. Handle the Future returned by step 2 and trigger a page change / UI change e.g. showing a error message (I don't know how to do this with ufront)
kevinresol commented 9 years ago

Just figured a way, not sure if it is the best but I just write it down here:

Haxe:

// Client.hx
public static function main()
{
  var app = new ClientJsApplication(...);

  // register a function in the global namespace so that the button in html can call it
  untyped js.Browser.window.onButtonClick = function() 
  {
    // call the async api. When the future is done, use pushstate to trigger a page 
    // change, which will be handled (at client-side) by the corresponding UFControllers
    asyncApi.doSometing().handle(function(outcome) PushState.push("/some/url"));
  }
}

html page:

<!-- Put a button in page which, when clicked, will call the function defined above-->
<button onclick="onButtonClick()">Click me</button>
kevinresol commented 9 years ago

But that leads to another problem, how to get the api injected in the button callback?

MichaPau commented 9 years ago

Hi I have tried, without success in the moment to use an AsyncApi wrapping my api inside a ClienJsApplication: My attempt was the following in the controller:

@inject public var testApi:app.api.AsyncTestApi;

    @:route("/jsonFile") 
    public function doJson() {

        var result = "";
        var surprise = testApi.gimmeJson().handle(function(outcome) {
            //it never ever goes inside the handler
            switch outcome {
              case Success(jsonStr):
                  result = jsonStr;
                  var json:Json = Json.parse(result);
                  return new ViewResult(json);

              case Failure(err):
                  return new ViewResult( { message: "error" }, "error.html");

            }
        });

        return new EmptyResult(true);
    }

The app just opens a json file and returns the content of it. With returning an EmptyResult I hopped the client just sits there waiting for the Surprise handler returning the ViewResult, but the handler is never ever called. I guess it is because the route method returns and the api no longer exists, so the handler is never called.. ?!

kevinresol commented 9 years ago

@MichaPooh see my pull request here: https://github.com/ufront/ufront-mvc/pull/15

MichaPau commented 9 years ago

Looks kinda cool. Is it working for you for the AsyncViewResult?

I can never make AsyncViewResult accept the outcome from the AsyncApi Event with your examples (ignoring that my templateEngine will complain) i get a bunch of compiler errrors.

kevinresol commented 9 years ago

It worked for me. Make sure you are supplying a Future<TemplateData> to AsyncViewResult's constructor

MichaPau commented 9 years ago

That's what I don't understand. The operator overload >> from Futur returns already the result wrapped in a Futur. So for example

var surpriseResult = testApi.gimmeJson() >> function (str) return Json.parse(str);
return new AsyncViewResult(surpriseResult);

should work - but i doesn't for me compiler complains with :

tink.core.Future<tink.core.Outcome<Unknown<0>, ufront.remoting.RemotingError<tink.core.Noise>>> should be tink.Future<ufront.view.TemplateData>

Sorry to bother you again - i just spent hours and hours with this and there is no other place to ask I guess.. can be I just don't understand enough the tink core lib...

kevinresol commented 9 years ago

try this

var surpriseResult = testApi.gimmeJson().map(function (str):TemplateData return Json.parse(str.sure()));
return new AsyncViewResult(surpriseResult);
kevinresol commented 9 years ago

Will map a Surprise<A, F> to a Surprise<B, F> with a A->B

According to tink_core doc, if your gimmeJson() is returning a Suprise, the >> operator will also give you a Suprise, which is not what AsyncViewResult wanted. It wants a Future, not Suprise.

MichaPau commented 9 years ago

Thanks for your answers... The function returns just a json String but it's my (injected) instance of UFAsyncApi which wraps my result in a Surprise. Isn't it the same for you using an AsyncApi in a ClientJsApplication ? The UFAsyncApi doc states

  • An API return type of :Surprise<A,B> will become :Surprise<A,RemotingError<B>>.
  • An API return type of :Future<T> will become :Surprise<T,RemotingError<Dynamic>>.
  • An API return type of :Outcome<A,B> will become :Surprise<A,RemotingError<B>>.
  • An API return type of :Void will become :Surprise<Noise,RemotingError<Dynamic>>.
  • An API return type of :T will become :Surprise<T,RemotingError<Dynamic>>.

so whatever the implementation in the api returns it will always be a Surprise using the the AsyncApi,

Edit: I can make it work with

var result = testApi.gimmeJson().map(function (o) {
    switch o {
        case Success(jsonStr):return Json.parse(jsonStr);
        case Failure(err): {
            var data:TemplateData = { 'data': [ { 'label': 'error', 'value': 0 } ] };
            return data;
        }
    }
    });
return new AsyncViewResult(result);
kevinresol commented 9 years ago

nice to hear that!

jasononeil commented 9 years ago

Sorry for the silence on this guys - but I'm glad to see you made some progress.

As I've been discussing with @kevinresol over in #15 - we do already support Future<ActionResult> as the return type of a controller action. All of this is still quite new and only lightly tested, but I'm looking to solidify support for ufront-client asap.

I'll try get a full complete example repo together before the WWX conference at the end of May, but in the mean time, the controller you want is something like:

    @inject public var testApi:app.api.AsyncTestApi;

    @:route("/jsonFile") 
    public function doJson() {
        var futureViewResult = testApi.gimmeJson().map(function(outcome) {
            switch outcome {
              case Success(jsonStr):
                  var json:Dynamic = Json.parse(jsonStr);
                  return new ViewResult(json);
              case Failure(err):
                  return new ViewResult( { message: "error" }, "error.html");
            }
        });
        return futureViewResult;
    }

If you are happy to use the default error handler, and you use tink_core's ">>" operator shortcut, you can make this smaller:

    @inject public var testApi:app.api.AsyncTestApi;

    @:route("/jsonFile") 
    public function doJson() {
        return testApi.gimmeJson() >> function(jsonStr:String):ViewResult {
              return new ViewResult( Json.parse(jsonStr) );
        };
    }
postite commented 9 years ago

hello guys ... what's the state of this ? i see differences between docs and those codes. Btw it's not clear to me what goes to the client and to the server.

kevinresol commented 9 years ago

What is your problem now? The controller action function does allow you to return a future result now e.g. Future<ViewResult>

postite commented 9 years ago

hello kevin Thx for the quick answer. the fact is i don't know what kind of code to follow. and what should rely on the server and on the client. i tried many things that won't work and i'm lost.

i think i need a good example ;)

what i understood is :

i need a Async api wrapping my api this async api is injected to my controller ( homeController ) with inject ( i don't get the need of the injection here ) on my Client.hx : this is where i would benefit of injection but can't inject anything ( nothing happens ) i don't know what to call from my Client. and i don't understand how i can separate pure server requests because of the isomorphic type of front.

if you got a gist with the 3 classes : the Api with the AsyncWrapper. the serverController the client with at least on call from client to the Api returning async data.

it would be very handy.

( by the way i struggled to make front work with those different versions of libs minject etc . i perhaps broke something) thanks

MichaPau commented 9 years ago

@postite I put up a simple Ufront api setup on Gist https://gist.github.com/MichaPau/237c85dac91590c3f437

Perhaps it's useful for you...

postite commented 9 years ago

that's very kind of you @MichaPau ... perhaps because i use v2 of minject, sadly ufront doesn't want to inject my TestApi:

Internal Server Error

Failed to inject app.api.TestApi into app.api.AsyncTestApi

ufront.handler.MVCHandler.handleRequest:-1

Exception Stack:

Called from minject.point.MethodInjectionPoint.applyInjection Called from minject.Injector.injectInto Called from minject.Injector._instantiate Called from minject.provider.ClassProvider.getValue Called from minject.InjectorMapping.getValue Called from minject.Injector.getValueForType Called from minject.point.PropertyInjectionPoint.applyInjection Called from minject.Injector.injectInto Called from minject.Injector._instantiate Called from ufront.handler.MVCHandler.processRequest Called from ufront.handler.MVCHandler.handleRequest Called from ufront.app.HttpApplication.setContentDirectory@306

kevinresol commented 9 years ago

see https://github.com/ufront/ufront-mvc/issues/19