Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Can Post operation parse the whole attributes? #5

Closed
MortalFlesh opened this issue Dec 4, 2020 · 5 comments
Closed

Can Post operation parse the whole attributes? #5

MortalFlesh opened this issue Dec 4, 2020 · 5 comments

Comments

@MortalFlesh
Copy link

Hello,

I'd like to ask, if there is a way to parse whole attributes "json" as once or must I create every single attribute?

I'm doing a simple "command handler" api, which only receives generic commands and will do stuff based on that specific command (so yes I know, its more like GRPC, but we are using json-api format for that as well).

There won't be any Get, Put or Delete requests allowed for this resource, only Post would be available, to send new commands.

So I have a command

type Command<'CommandData> = {
    Id: Guid
    CorrelationId: Guid
    CausationId: Guid
    Timestamp: string

    Ttl: int
    AuthenticationBearer: string
    CommandName: string

    Data: 'CommandData
}

Note: It is a bit simplified, but otherwise pretty much like that.

The request the api gets could looks like this:

{
    "data": {
        "type": "commands",
        "attributes": {
            "id": "a689e2f0-15dc-448d-b2f8-2ee45e09be52",
            "correlation_id": "a689e2f0-15dc-448d-b2f8-2ee45e09be52",
            "causation_id": "a689e2f0-15dc-448d-b2f8-2ee45e09be52",
            "timestamp": "2020-05-12T12:50:25.983Z",
            "ttl": 100,
            "authentication_bearer": "Bearer: ...Auth...",
            "command_name": "create_person",
            "data": {
                "name": "Foo Bar",
            }
        }
    }
}

I already have the library to handle commands, to parse/serialize them from/to string (json) and I'd like to reuse that in json api, but I can only parse a single attribute at a time AFAIK.

let id =
        define.Id
            .ParsedOpt(
                CommandId.value >> string,
                CommandId.tryParse,
                Command.id
            )

let correlationId =
        define.Attribute
            .SimpleUnsafe("correlation_id")
            .GetSkip(fun _ _ -> Skip)
            .Set(fun id c -> { c with CorrelationId = id })

// ... rest of attributes ...

let post =
        define.Operation
            .Post(fun ctx parser ->
                let require attribute =
                    attribute
                    |> parser.GetRequiredAsync
                    |> AsyncResult.mapError AttributeErrors

                asyncResult {
                    let! id = require id
                    let! correlationId = require correlationId
                    // ... rest of attributes ...
                    
                    return {
                       Id = id
                       CorrelationId = id
                        // ... rest of attributes ...
                    }
                }
                |> parser.ForAsyncRes
            )
            .AfterCreate(fun command -> command)

So is there a way to just parse the whole attribute object as is?

let post =
        define.Operation
            .Post(fun ctx parser ->
                parser.XXX ()
                |> Command.parse
                |> parser.ForAsyncRes
            )
            .AfterCreate(fun command -> command)

Thanks!

@cmeeren
Copy link
Owner

cmeeren commented Dec 4, 2020

Thanks for explaining your use-case and giving code examples. There are several things I notice.


First, to your specific question: I interpret your use-case to mean that you want to parse the data attribute directly as JSON, and that the other attributes are static. You have a few options:

  • If you know that the data object only contains string values (or values of any one particular type), the simplest solution is to deserialize it to e.g. Dictionary<string,string> or similar.
  • If the data object can contain heterogeneous data (e.g. strings, bools, ints, etc) or sub-objects, you need to parse the JSON as you say. You can do this by deserializing it to something like System.Text.Json.JsonElement, which you can then use for parsing the JSON (unrelated to Felicity).

In both of the above cases, I you must use SimpleUnsafe as the attribute type.


Second, since there are no GET operations for commands (and assuming the command is created as-is from the request), it may be better to return 202 Accepted (with no content) which you can do with .Return202Accepted() after calling Post(...). This also seems semantically more correct since what I assume you're doing is starting some work based on the command. (If, contrary to my assumptions, the client needs to see the created command, then the existing 201 Created response is suitable.)


Third, it seems that you use the parser.ForAsyncRes (and AfterCreate) incorrectly. I can't see anywhere that you persist or do anything with the received command (which you should do in AfterCreate), so I guess that you're doing impure stuff in the asyncResult that you pass to parser.ForAsyncRes. This is not correct. Everything before AfterCreate must be pure (in the sense that it must not cause observable state changes; it is OK to read from DB), because the request may generally fail for validation reasons until just before AfterCreate is called. Make sure that you only do impure stuff in AfterCreate, after you have constructed your domain object in the parser above. Relevant sections of the documentation:

@MortalFlesh
Copy link
Author

Hello,

First

I don't need to parse the whole data, but a data.attributes without explicitly defining all of them in the code ideally.

Just to clarify it more, the Command type itself is a more complex than I showed in the example above, it has a different item types and even a nested objects in it. I have a Command.parse function which can parse it from json string (using JsonTypeProvider in it).

So the first option Dictionary<string,string> is not possible. But the second one could be the way, but I'm still not sure how to do it. You said I must use SimpleUnsafe attribute type, which is fine, but I don't know how to set it above the all data.attributes.


Second

202 Accepted is exactly, what I will return, so thank you for pointing it out.


Third

The example above was simplified, not to be confusing with other parts.

In the real app, there will be something like this

.AfterCreateAsyncRes(fun ctx command -> command |> ctx.HandleCommand)

Where HandleCommand is a function that executes some async logic based on a command type and it might even persist something, but it is based on a specific command.

And the praser.ForAsyncRes could really be just parser.ForRes, since it may only fail during a "static" parsing process, there is no more logic in that.

@cmeeren
Copy link
Owner

cmeeren commented Dec 7, 2020

I don't need to parse the whole data, but a data.attributes without explicitly defining all of them in the code ideally.

It seems you understood me to mean that you can parse the whole top-level data. I should have clarified that I was talking about your data attribute, i.e. data.attributes.data in the example body you posted. If you deserialize this to a JsonElement or similar, then you can parse it however you want in your domain logic:

let data = define.Attribute.SimpleUnsafe(...)  // etc., deserialize to `JsonElement` or similar

You put all the really dynamic stuff in this data attribute and parse it however you want.

If you want to do that for the whole attributes object, then that is not possible, nor will it ever be. Strongly typed attributes and relationships is at the core of what Felicity is.

If your model really is that dynamic, then I think that perhaps JSON:API isn't the right solution for your use-case. JSON:API is focused on structurally well-defined resources and semantically well-defined operations on these resources.

Don't hesitate to let me know if anything is unclear.

@MortalFlesh
Copy link
Author

MortalFlesh commented Dec 7, 2020

Oh I see.

Yes I was aware of that option from the beginning.

If the input data could be

{
    "data": {
        "type": "commands",
        "attributes": {
            "command": { ...commandJson... }
        }
    }
}

It would be easy and I could use just a define.Attribute.SimpleUnsafe("command") - I know :) but..

But the format is unfortunately this:

{
    "data": {
        "type": "commands",
        "attributes": { ...commandJson... }
    }
}

So the commandJson is directly spread in the attributes and since this format is shared between multiple services (among a multiple languages), I can't change it.

That is why I'd like to parse the whole data.attribute field as a json string.

And the command model is just partially dynamic. It has defined schema and a generic data section which is specific for a current command.


So if the attributes object is not possible to access directly, I need to define all the attributes as a simple string or json attributes and then probably recreate the whole json out of it to parse it again by Command.parse function...

It seems a bit complex, but in the end, it might be a simpler than "share" the Command schema parsing out of a Command module..

Well thank you for your time!

@cmeeren
Copy link
Owner

cmeeren commented Dec 7, 2020

You know your code base best, and how complex each solution is. But do consider that maybe Felicity isn't the right tool for this particular job. Perhaps another JSON:API framework, or no JSON:API framework at all for that matter (just parsing the body manually), may be simpler.

@cmeeren cmeeren closed this as completed Dec 7, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants