[tink_web] What is the proper way to return a mixed JSON response?

I have a route that processes a form. This form is backed by a model class that performs the data validation. The validation outcome is a haxe.ds.Option<DynamicAccess<String>>: None if the request is OK, otherwise Some with the validation errors (i.e. field => error message).

If the validation succeeds, the route returns the model data and a 201 status code.

If the validation fails, the route returns the anonymous structure providing the validation errors and a 422 status code.

My current code works, but it does not feel the right way to do the job:

import haxe.Json;
import httpstatus.HttpStatusCode;
import tink.http.Response;

@:post
@:header("Content-Type", "application/json")
public function handleForm(body: {field1: String, field2: String}) {
  final model = new FormModel();
  model.field1 = body.field1.trim();
  model.field2 = body.field2.trim();

  return switch model.validate() {
    case None: new OutgoingResponse(Created, Json.stringify(model));
    case Some(errors): new OutgoingResponse(UnprocessableEntity, Json.stringify(errors));
  };
}

I’ve tried alternatives, but every trial resulted in error or unsatisfying outcome:

1 - Replaced @:header("Content-Type", "application/json") meta by @:produces("application/json") : the response Content-Type is text/html instead of application/json.

2 - Removed the @:header meta, and replaced tink.http.OutgoingResponse by tink.web.Response to avoid the Json.stringify() calls and let tink_web do the serialization :

return switch model.validate() {
  case None: new Response(Created, model);
  case Some(errors): new Response(UnprocessableEntity, errors);
};

The response is OK… except that the code doesn’t compile. It produces a return type error on the Some(errors) case: haxe.DynamicAccess<String> should be FormModel.

The lack of documentation does not help in understanding what should be done:
https://haxetink.github.io/tink_web/#/basics/response

Does anyone has a sample code showing the best way to implement the response handling?

Like so?

import tink.http.Header;
import tink.web.routing.*;
import tink.http.Request;
using tink.io.Source;
using StringTools;
using tink.CoreApi;

class Main {
  function new() {}

  @:post
  @:statusCode(201)
  public function handleForm(body: {field1: String, field2: String}) {
    final model = new FormModel();
    model.field1 = body.field1.trim();
    model.field2 = body.field2.trim();

    return model.validate();
  }
  static function main() {
    var r = new Router<Main>(new Main());

    function post(data:Dynamic) // just a helper to simulate POST against our route
      r.route(
        Context.ofRequest(
          new IncomingRequest('0.0.0.0',
            new IncomingRequestHeader(POST, '/handleForm', HTTP1_1, [new HeaderField(CONTENT_TYPE, 'application/json')]),
            Plain(haxe.Json.stringify(data))
          )
        )
      ).handle(o -> switch o {
        case Success(res):
          res.body.all().handle(function (v) {
            trace(res.header.toString() + v.toString());
          });

        case Failure(e):
          trace(e.toString() + switch e.data {
            case null: '';
            case v: ' $v';
          });
      });

    post({ field1: 'foo', field2: 'bar' });// this should fail
    post({ field1: 'boink', field2: 'bar' });// this should not
  }
}

class FormModel {
  public var field1:String;
  public var field2:String;

  public function new() {}

  public function validate()
    return
      if (field1 != 'foo') Success(this);
      else Failure(Error.withData(UnprocessableEntity, 'Invalid form data', { field1: 'must not be foo' }));
}

The output for the two simulated calls:

Main.hx:38: Error#422: Invalid form data @ FormModel.validate:58 {
        field1 : must not be foo
}
Main.hx:34: HTTP/1.1 201 Created
content-type: application/json
content-length: 33

{"field1":"boink","field2":"bar"}

In itself, tink_web doesn’t allow for mixed responses (you can return enums though). It does allow returning promises (or outcomes, which are implicitly cast to promises), which always include an error branch. If needed, you can use a custom error printer in place of OutgoingResponse.reportError.

2 Likes

It’s much better now, and totally covers my requirements.
Thanks @back2dos for answering as quickly, and with such details.