HydraCG / Specifications

Specifications created by the Hydra W3C Community Group
Other
139 stars 26 forks source link

Specifying a list of resources of different types in `hydra:operation` `expects` field #249

Closed hyprdia closed 1 year ago

hyprdia commented 1 year ago

Describe your API requirement

I need to be able to create multiple resources types in one HTTP POST for which a particular endpoint has been defined. We could make an analogy with a file system. Let’s say I have two kinds of resources: folders and files: I would like to be able to tell the client that I expect a list folders and files under a particular folder.

For example, with the endpoint http://api.example.com/folders/, which contains all resources of type Folder, I have defined a sub resource tree e.g. http://api.example.com/folders/folderId/tree which would allow to POST folders and files to be created under a specific folder instance.  

Now, I would like to be able to describe the operation in Hydra using something like this:

...
"operation": [        
    {            
        "method": "POST",            
        "title": "Files addition bulk operation",            
        "description": "Add a tree of resources under the target folder",
        "expects": [
            "http://api.example.com/classes/Folder",
            "http://api.example.com/classes/File",
        ]
    }    
]

which would simply list all requested files and folders to be able to create a file structure. In my particular scenario the user would need to provide a specific files structure known and provided by the API, we could imagine creating a Maven project with a predefined folders structure or something similar. The point of passing all this information is to be able, from the client perspective, to present everything the user needs to create in any way it wants, it could be a form with multiple sub-sections, or a wizard which would present all the information the user needs to provide once at a time.

What would be even better, is if I would be able to use SHACL to define all the requirements for form display, like here:

"operation": [        
    {            
        "method": "POST",            
        "title": "Files addition bulk operation",            
        "description": "Add a tree of resources under the target folder",
        "expects": [
            {
                "@id" : "ex:Folder",
                "@type" : "NodeShape",
                "property" : [
                    {
                        "path" : "ex:name",
                        "datatype" : "xsd:string" ,
                        "pattern" : "^\\d{3}-\\d{2}-\\d{4}$"
                    }
                ]
            },
            {
                "@id" : "ex:File",
                "@type" : "NodeShape",
                "property" : [
                    {
                        "path" : "ex:name",
                        "datatype" : "xsd:string" ,
                        "pattern" : "^\\d{3}-\\d{2}-\\d{4}$"
                    },
                    {
                        "path" : "ex:data",
                        "class" : "ex:Mpeg"
                    }
                ]               
            }
        ]
    }    
]

This is a toy example with no real meaning but I think it describes the essential: I need to pass a list of different resources types to an endpoint to create multiple resources in one POST request.

Solution(s)

  1. I considered defining a custom rdf:Class/sh:NodeShape that would list all the required resources needed and just be pointed at by the operation expects field. This would work but, this is not really elegant as the hypermedia message should in my opinion be responsible for carrying this kind of information, not RDF. Also, this is not a class that would be valid for all folders, to continue the directory example: each directory would need to define it's own particular class since the requirements can change from folder to folder.

  2. I considered a SHACL blank node to describe what to expect a bit like in this answer from @tpluscode but the problem, maybe I miss something, is that the Hydra schema defines the expects field as a Link:

    ...
    {
      "@id": "hydra:expects",
      "@type": "hydra:Link",
      "label": "expects",
      "comment": "The information expected by the Web API.",
      "domainIncludes": ["hydra:Operation", "hydra:RetractedOperationSpecification"],
      "rangeIncludes": ["rdfs:Resource", "hydra:Resource", "rdfs:Class", "hydra:Class"],
      "isDefinedBy": "http://www.w3.org/ns/hydra/core",
      "vs:term_status": "testing"
    },
    ...

In fact, it seems to me that an Hydra API expects the client to send a second request in order to know the value of the parameters for an operation. If this is the case, I also would like to know what motivates this choice in the first place.

Thanks for your assistance,

Sébastien Hamel

P.S. Also, the Hydra Console does not seem to work at the moment, any plan to put it back online?

alien-mcl commented 1 year ago

First of all - I'd split it into several operations each expecting different class. Your example:

"expects": [
    "http://api.example.com/classes/Folder",
    "http://api.example.com/classes/File",
]

would imply a resource that is both Folder and File is expected which I believe is far from your requirements.

As for additional constraints on those expectations i see three solutions:

Both may put additional stress on the client, but the former feels better for this purpose. Both constructs may be invisible for generic clients understanding pure hydra, yet still the client may be instructed that the server uses extensions via extension property attached to ApiDocumentation

You could also consider typeahead-like or list of possible values in order to constrain user from providing any kind of details. The supportedClass/supportedProperty and search might come in handy. There are some examples in the release-candidate spec sitting in this PR

Hydra schema defines the expects field as a Link

The hydra:expects is a predicate declared as link so the client can see a notion that the operation is somehow linked to the resource in expects relation, but the predicate can be used in between whatever is in the domainIncludes (left part of the relation) and rangeIncludes (right part of the relation), i.e.:

some:Operation hydra:expects some:Resource
hyprdia commented 1 year ago

would imply a resource that is both Folder and File is expected which I believe is far from your requirements.

The goal here was to interact with multiple resources in one request. This was not to be interpreted the same as JSON-LD@type:

{
    "@type": [
        "http://api.example.com/classes/Folder",
        "http://api.example.com/classes/File",
    ]
}

The question was the result of certain incomprehensions of the REST architecture style from my part, and basically, it seems that Hydra enforces it in a way by using the expects to link to one resource, which, in retrospect, is a good safeguard for mistakes from people new to REST.

First of all - I'd split it into several operations each expecting different class. Your example:

I closed the question because I am in the process of developing another way to do this using the expects. The best design to achieve my purpose is still pending, but one of the solutions involves changing the state of a resource e.g. a Folder from draft to complete. This would imply a PATCH sent to only modify one field of a resource. Maybe I miss something but I cannot see any way to describe this operation. The expects seems to allow to describe a full Resource, same as your answer:

some:Operation hydra:expects some:Resource

Let's say my resource is the following:

{
    "@context": {
    ...
    },
    "@id": "folders/tgbF5312Kwnb9fdsteDhG6",
    "@type": "classes:Folder",
    "createdAt": "2008-10-31T15:07:38.6875000-05:00",
    "state": "draft",
    "operation": [
        {
          "title": "Change the folder state to `complete`",
          "method": "PATCH",
          "expects": ????
      ...
    }
      ]
}

how would we describe the PATCH to only change the state of the Folder?

As for using SHACL, I indeed intend to use it, Integrations exist with client libraries that allows to use SHACL to generate forms from it. I still need to use them and make sure they do what I need but at least I have something to start with if I need something not provided already. Thanks for pointing out these possible solutions.

hyprdia commented 1 year ago

I decided to go with json schema in my expectations using the JSON Schema in RDF which describes JSON schema in RDF and allows to use a tool like JSON Forms or any other tool that supports json schemas which are plentiful compared SHACL.

Since expects covers anything that be described using rdfs:

    {
      "@id": "hydra:expects",
      "@type": "hydra:Link",
      "label": "expects",
      "comment": "The information expected by the Web API.",
      "domainIncludes": ["hydra:Operation", "hydra:RetractedOperationSpecification"],
      "rangeIncludes": ["rdfs:Resource", "hydra:Resource", "rdfs:Class", "hydra:Class"],
      "isDefinedBy": "http://www.w3.org/ns/hydra/core",
      "vs:term_status": "testing"
    }

we can use Json schema RDF description to define the value of expects.

{
    "@context": {
        "jsonschema": "https://www.w3.org/2019/wot/json-schema"
    ...
    },
    "@id": "folders/tgbF5312Kwnb9fdsteDhG6",
    "@type": "classes:Folder",
    "createdAt": "2008-10-31T15:07:38.6875000-05:00",
    "state": "draft",
    "operation": [
        {
          "title": "Change the folder state to `complete`",
          "method": "PATCH",
          "name": "folder:change-state",
     "expects": {
         "jsonschema:type": "object",
             "jsonschema:properties": {
                 "jsonschema:oneOfEnum": {
                 "jsonschema:type": "string",
                 "jsonschema:oneOf": [
                     {
                     "jsonschema:const": "complete"
                },
                {
                     "jsonschema:const": "other-state"
                        }
                         ]
                    }      
        }
      ]
}

The requirements here would be to inform the client that I use a jsonschema extension.