HydraCG / Specifications

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

Should we introduce a property to associate operations and their target directly to an entity? #3

Open lanthaler opened 11 years ago

lanthaler commented 11 years ago

I think Hydra handles all the use cases presented by Schema.org so far quite nicely, the only thing that we really should discuss is how to handle one-click actions. For example, look at how GMail describes a RSVP action:

{
  "@context": "http://schema.org",
  "@type": "Event",
  "name": "John's Birthday Party",
  ... information about the event ...
  "action": {
    "@type": "RsvpAction",
    "actionHandler": {
      "@type": "HttpActionHandler",
      "url": "https://events-organizer.com/rsvp?eventId=123",
      "method": "POST",
      "requiredProperty": "rsvpStatus",
    }
  }
}

In Hydra, at the moment you would have to do something like

{
  "@context": ... for Hydra and schema.org ...,
  "@type": "Event",
  "name": "John's Birthday Party",
  ... information about the event ...
  "rsvp": { 
    "@id": "https://events-organizer.com/rsvp?eventId=123"
    "operations": {
      "@type": "RsvpAction",
      "method": "POST",
      "expects": {
        "supportedProperties": {
          "property": "rsvpStatus",
          "required": true
        }
      }
    }
  }
}

The expects part could be replaced by a class instead of defining the supported properties inline, something like "expects": "RsvpResponse". So the only thing that isn't really as elegant as GMails example is that Hydra needs a specific property rsvp instead of just actions. This is essentially a specialized link relation. The advantage is that it allows us to associate the supported operations directly to the definition of the link relation so that example could really be simplified to

{
  "@context": ... for Hydra and schema.org ...,
  "@type": "Event",
  "name": "John's Birthday Party",
  ... information about the event ...
  "rsvp": "https://events-organizer.com/rsvp?eventId=123"
}

But that means that a client either needs to know how rsvp is defined or look it up (which generally shouldn't be a problem). Some may wonder why we can't use hydra:operation or hydra:supportedOperations in a similar way to GMail's action property. The reason is that hydra:operation is defined to specify the operation of the current resource as the example above illustrates. On the other hand, hydra:supportedOperations is defined to describe the operations allowed by instances of a specific class or values of specific properties (link targets).

Do we want to introduce a property similar to GMail's action?

lanthaler commented 10 years ago

PROPOSAL: Do nothing as it can easily be achieved by reversing "operation". Add a reverse property "target" to Hydra's JSON-LD context to simplify it's usage in practice and describe it in the specification:

{
  "@type": "hydra:Operation",
  "target": { "@id": "/some-resource" },
  "method": "POST",
  "expects": {
    "supportedProperties": {
      "property": "rsvpStatus",
      "required": true
    }
  }
}

The only issue I see with this solution is that "target" was recently introduced into schema.org but that's probably not that much of an issues in practice. In fact, we could also encourage (or at least support) schema.org/target as alternative. Thoughts?

tpluscode commented 7 years ago

Is that how a complete example would look like?

{
  "@context": {},
  "@type": "Event",
  "name": "John's Birthday Party",
  "operations": {
    "@type": "RsvpAction",
    "target": "https://events-organizer.com/rsvp?eventId=123",
    "method": "POST",
    "expects": {
      "supportedProperties": {
        "property": "rsvpStatus",
        "required": true
      }
    }
  }
}
tpluscode commented 7 years ago

In general agree with adding target. I've been thinking about something similar which I would use internally to uniformly represent operations.

I would model it so that any operation without an explicit target assumed the parent resource as one. So

{
  "@id": "http://example.com/resource",
  "operations": {
    // details
  }
}

was equivalent to

{
  "@id": "http://example.com/resource",
  "operations": {
    "target": "http://example.com/resource",
    // details
  }
}

Is that aligned with your idea?

lanthaler commented 7 years ago

Mostly. I envisioned the target property to be a simple inverse of the operation property. Thus you couldn’t use both at the same time. We would need another mechanism to link from resource to the operation. I was thinking about action. I would call the abstract thing that happens (becoming a friend of someone, ordering something) an Action and the bytes on the wire (HTTP or other request) an Operation. WDYT?

tpluscode commented 7 years ago

This distinction between Action and Operation makes sense.

Regarding target, I was first thinking about a hidden artifact. Something the a client library produces when applying the ApiDocumentation to a representation. Mostly to simplify processing down the line.

I think that the other use of target is what Jay Zawar asked about in his email. Currently the only option is to have an additional artificial link similarly to the rsvp example above, right?

lanthaler commented 7 years ago

Regarding target, I was first thinking about a hidden artifact.

I prefer to make things explicit than to require each client to infer such information. In other words, I prefer the smarts in the message rather then algorithms

Yes, without something like an action property, an additional artificial link similarly to the rsvp example above would be required.

tpluscode commented 7 years ago

I prefer to make things explicit than to require each client to infer such information

I agree. Explicit is better. But I haven't found an elegant way anyway when processing supported operations. Consider

{
  "@id": "some:collection",
  "member": [
    { "@id": "member:one", "@type": "Person" },
    { "@id": "member:two", "@type": "Person" }
  ]
}

and

{
  "@type": "ApiDocumentation",
  "supportedClass": [{
    "@id": "Person",
    "supportedOperation": [{
      "@id": "PokePerson"
    }]
  }]
}

In this example each Person has a supported operation but it will be invoked on the Person's URI.

In Heracles, I process supported operations and return an Operation object which has the correct target URL. This way the Operation can be then passed to other code detached from the actual Person object. It's just one step away from allowing explicit targets

asbjornu commented 6 years ago

As discussed in #134, we have the alternatives href, target, targetUrl and endpoint mentioned. Are there more alternatives? What are their pros and cons? My preference so far is target as I feel targetUrl is unnecessary since its @type is going to be @id (which is an IRI).

elf-pavlik commented 6 years ago

In case of create with PUT #141 we need to also handle URI Templates #118 no just IRIs. Unless we explore direction similar to hydra:search for cases where we need URI Templates. I don't like it since this wouldn't allow to model create with POST and create with PUT (based on URI Template) in the same way eg. schema:CreateAction.

tpluscode commented 6 years ago

I don't like it since this wouldn't allow to model create with POST and create with PUT

I don't follow. Creating is fine both with PUT and POST. The former in the case when the server doesn't know the resource's URI up front.

alien-mcl commented 6 years ago

POST != create. Only PUT creates resourcers in terms of HTTP terms from those two.

tpluscode commented 6 years ago

I know you're right about the spec but it only means that PUT is used to create or update a resource identified by the requested URL. Otherwise nothing forbids new resources from being created by side effects of a POST. In fact it's such a common pattern

POST a new representation to a collection

POST /members HTTP/1.1

{
  "name": "Tomasz",
  "description": "I want to be a member"
}

The server returns a link to the new resource

HTTP/1.1 201 Created
Link: </members/tomasz>

There you go. I created a new resource using POST.

asbjornu commented 6 years ago

Using POST to create resources is much more common than using PUT, since PUT requires the client to know the URI beforehand. The POST method can be used to mean anything according to HTTP semantics and "create" is one such, very common meaning.

elf-pavlik commented 6 years ago

In my comment I just wanted to propose that operation doesn't always have to specify target - an IRI, which we can think of as a resource with an @id (denoted by that IRI) As @lanthaler suggested we don't need to define a new property for that but just rely on using hydra:operation in reverse direction - JSON-LD @context in spec non-normative appendix can include an alias for that. In cases like CreateAction with PUT #141 sometimes client will not get IRI but Uri Template / hydra:IriTemplate, based on which clients constructs the IRI which denotes the resource on which to perform the operation. In that case I don't think we can use hydra:operation in reverse direction

The most common example of using hydra:IriTemplate comes with hydra:search, the only property which definition includes hydra:search rdfs:range hydra:IriTemplate. Also already widely deployed thanks to Triple Pattern Fragments http://www.hydra-cg.com/spec/latest/triple-pattern-fragments/#controls

What I don't like about trying to use similar approach to creating a new resource (something like hydra:create). It seems to result in two different ways of advertising the create operation

    {
      "@id": "/api/events",
      "title": "List of events",
      "@type": "hydra:Collection",
      "manages": {
        "property": "rdf:type",
        "object": "schema:Event"
      },
      "operation": {
        "@type": ["hydra:Operation", "schema:CreateAction"],
        "title": "Create new event",
        "method": "POST",
        "expects": "schema:Event"
      }
    }

and following how hydra:search works, something like:

    {
      "@id": "/api/events",
      "title": "List of events",
      "@type": "hydra:Collection",
      "manages": {
        "property": "rdf:type",
        "object": "schema:Event"
      },
      "hydra:create": {
        "@type": "hydra:IriTemplate",
        "hydra:template": "/api/events/{uuid}",
        "hydra:mapping": {
          "hydra:variable": "uuid",
          "hydra:property": "id:uuid",
          "hydra:required": true
        }
      }
    }

While I would like something aligned with the first example using schema:CreateAction

    {
      "@id": "/api/events",
      "title": "List of events",
      "@type": "hydra:Collection",
      "manages": {
        "property": "rdf:type",
        "object": "schema:Event"
      },
      "operation": {
        "@type": ["hydra:Operation", "schema:CreateAction"],
        "title": "Create new event",
        "method": "PUT",
        "expects": "schema:Event",
        "targetTemplate": {
          "@type": "hydra:IriTemplate",
          "hydra:template": "/api/events/{?uuid}",
          "hydra:mapping": {
            "hydra:variable": "uuid",
            "hydra:property": "id:uuid",
            "hydra:required": true
        }
        }
      }
    }

But then if we consider (an alias) @reverse of hydra:operation as target, it seems to conflict with targetTemplate in a way.

@lanthaler: Mostly. I envisioned the target property to be a simple inverse of the operation property. Thus you couldn’t use both at the same time. We would need another mechanism to link from resource to the operation. I was thinking about action. I would call the abstract thing that happens (becoming a friend of someone, ordering something) an Action and the bytes on the wire (HTTP or other request) an Operation. WDYT?

I also like this distinction and we also have related conversation in #2 in case above if we would state

    {
      "@id": "/api/events",
      "action": {
        "@type": ["hydra:Operation", "schema:CreateAction"]
      }
    }

We would have to provide an explicit target or targetTemplate, in that case it might make sense to define target property and for simple cases use it in @reverse direction (possibly making operation an alias). This also would allow use of schema:SearchAction UC#7 Searching events

{
    "@context": "/api/context.jsonld",
    "@id": "/api/events",
    "@type": "hydra:Collection",
    "manages": {
      "property": "rdf:type",
      "object": "schema:Event"
    },
    "totalItems": 100,
    "members": [ ... ],
     "action": [
        {
          "@type": ["hydra:Operation", "schema:CreateAction"],
          "title": "Create new event",
          "method": "POST",
          "expects": "schema:Event",
          "target": "/api/events"
       }, {
          "@type": ["hydra:Operation", "schema:SearchAction"],
          "title": "Search evenst",
          "method": "GET",
          "returns": "hydra:PartialCollectionView",
          "targetTemplate": {
            "@type": "IriTemplate",
            "hydra:template": "/api/events{?search}",
            "hydra:variableRepresentation": "hydra:BasicRepresentation",
            "hydra:mapping": {
              "@type": "hydra:IriTemplateMapping",
              "variable": "search",
              "property": "hydra:freetextQuery",
              "required": true
            }
         }
       }
     ]
}

At the same time it would mean breaking change to hydra:search which @RubenVerborgh may strongly object since TPF deployments rely on it.

alien-mcl commented 6 years ago

I know you're right about the spec but it only means that PUT is used to create or update a resource >identified by the requested URL. Otherwise nothing forbids new resources from being created by side >effects of a POST. In fact it's such a common pattern

This not a rule - it's just happens it is used in some circumstances. I imagine that POST'ing a resource to /members may end up with a Location header point to a URL that is not related to the newly created resource in any way - it may point client to a resource from which it can obtain i.e. status of the task submitted for processing. In case of members, it may be a process of user verification, etc.

elf-pavlik commented 6 years ago

I think we should work on this issue taking into account conversations in #100 and #118 and solution should resolve all 3 issues.

elf-pavlik commented 6 years ago

continuing from: https://github.com/HydraCG/Specifications/issues/141#issuecomment-336729288

@elf-pavlik: since hydra:Operation doesn't provide a way to specify hydra:IriTemplate it seems that we may not use schema:CreateAction with both HTTP POST and HTTP PUT

@lanthaler: I realize it is not properly documented but the intention was always that operations would work on IriTemplates` just as they work on resources...

Does it mean that if we start from an instance of hydra:Operation and follow hydra:operation in reverse direction, then 1) if we get an instance of hydra:Resource we use IRI that denotes that resource (JSON-LD @id) 2) if we get an instance of hydra:IriTemplate, we ignore IRI that denotes that template (JSON-LD @id) and instead use IRI based on hydra:template and hydra:mapping

@lanthaler: otherwise things like hydra:search wouldn't work.

Looking at example of hydra:search http://www.hydra-cg.com/spec/latest/triple-pattern-fragments/#controls I don't see hydra:Operation or hydra:operation used there at all. I see hydra:search relating an instance of hydra:Resource directly to an instance of hydra:IriTemplate.

tpluscode commented 6 years ago

Does it mean that if we start from an instance of hydra:Operation and follow hydra:operation in reverse direction, then [...]

That is how I understand it. Speaking in terms of triple patterns the client looks for

?resource hydra:operation ?operation

Like you say ?resource either has an identifier or template and mapping. A blank node which isn't a template naturally must result in an error.