hypermedia-app / Alcaeus

Hydra core hypermedia library
https://alcaeus.hydra.how
MIT License
61 stars 3 forks source link

How to iterate over properties on the resource that are not supported properties #293

Open lambdakris opened 2 years ago

lambdakris commented 2 years ago

Hey @tpluscode , is there a way to iterate over the properties on a resource if they are not listed as supported in the api doc? I did try resorting to using clownface, but I could only manage to get the property values and not the property names.

This is essentially what I tried with clownface

...
response.representation.root.pointer.out().forEach(x => {
  console.log(x)
})
...

And just in case...

Here is the resource I'm trying to consume

@base <http://localhost:8080/>.

@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>.
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#>.
@prefix xsd: <http://www.w3.org/2001/XMLSchema#>.
@prefix : <http://localhost:8080/api-onto#>.
@prefix owl: <http://www.w3.org/2002/07/owl#>.
@prefix hydra: <http://www.w3.org/ns/hydra/core#>.

<http://localhost:8080/> <http://localhost:8080/api-doc#lexicalResultSet> <http://localhost:8080/lexical-result-set>;
                         <http://localhost:8080/api-doc#semanticResultSet> <http://localhost:8080/semantic-result-set>;
                         a <http://localhost:8080/api-doc#EntryPoint>.

Here is the documentation

@base <http://localhost:8080/>.

@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>.
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#>.
@prefix xsd: <http://www.w3.org/2001/XMLSchema#>.
@prefix : <http://localhost:8080/api-onto#>.
@prefix owl: <http://www.w3.org/2002/07/owl#>.
@prefix hydra: <http://www.w3.org/ns/hydra/core#>.

<http://localhost:8080/api-doc> a hydra:ApiDocumentation;
                                hydra:entrypoint <http://localhost:8080/entry-point>;
                                hydra:supportedClass <http://localhost:8080/api-doc#EntryPoint>,
                                                     <http://localhost:8080/api-doc#LexicalResultSet>,
                                                     <http://localhost:8080/api-doc#SemanticResultSet>.
<http://localhost:8080/api-doc#EntryPoint> a hydra:Class;
                                           hydra:supportedProperty <http://localhost:8080/api-doc#lexicalResultSet>,
                                                                   <http://localhost:8080/api-doc#semanticResultSet>.
<http://localhost:8080/api-doc#LexicalResultSet> a hydra:Class;
                                                 hydra:supportedOperation <http://localhost:8080/api-doc#LexicalResultSetQuery>.
<http://localhost:8080/api-doc#LexicalResultSetQuery> a hydra:Operation;
                                                      hydra:expects owl:Nothing;
                                                      hydra:method "GET";
                                                      hydra:returns <http://localhost:8080/api-doc#LexicalResultSet>;
                                                      hydra:title "Perform a Lexical Result Set Query".
<http://localhost:8080/api-doc#SemanticResultSet> a hydra:Class;
                                                  hydra:supportedOperation <http://localhost:8080/api-doc#SemanticResultSetQuery>.
<http://localhost:8080/api-doc#SemanticResultSetQuery> a hydra:Operation;
                                                       hydra:expects owl:Nothing;
                                                       hydra:method "GET";
                                                       hydra:returns <http://localhost:8080/api-doc#LexicalResultSet>;
                                                       hydra:title "Perform a Semantic Result Set Query".
<http://localhost:8080/api-doc#lexicalResultSet> a hydra:Link;
                                                 rdfs:range <http://localhost:8080/lexical-result-set>;
                                                 hydra:property <http://localhost:8080/api-doc#lexicalResultSet>.
<http://localhost:8080/api-doc#semanticResultSet> a hydra:Link;
                                                  rdfs:range <http://localhost:8080/semantic-result-set>;
                                                  hydra:property <http://localhost:8080/api-doc#semanticResultSet>.
tpluscode commented 2 years ago

Indeed, clownface doe not have a feature to get edges in or out of a node (but watch https://github.com/zazuko/clownface/pull/62)

Alcaeus also doe not have a built-in feature for this I'm afraid. You could access the underlying data as raw triples but there may be 🐉

For the time being, a little bit more hackish way could be to turn your resource into a JS object by calling response.representation.root.toJSON() and iterate the property names natively

lambdakris commented 2 years ago

Ah ok, so here is a broader question then. I'm trying to figure out how to build what one might call an ontology driven app. What I'm thinking is that I would retrieve an api resource, it's linked api documentation, and it's referenced ontologies, and sort of iterate through the metadata to basically build up the UI. Is that crazy talk or does that sound like a legitimate semantic technology scenario and would you have anything you could point me to for reference?

tpluscode commented 2 years ago

Ah, now we're talking! I have been trying to answer similar questions myself and so far I have concluded that Hydra maybe not the right medium for data models. I would like to see the hydra:supportedClass and hydra:supportedProperty predicates only used for actual hypermedia.

There are existing vocabularies for building ontologies and data models: OWL, SHACL, ShEx. I would suggest you build a UI based on those instead. This is what I have been doing lately, namely with SHACL.

This gives you additional options. First, any given resource can be represented in multiple models schemes. I typically have a top-level hydra:collection which lets the app discover the shapes of a resource fetched from the API

# Entrypoint resource
</> hydra:collection </api/shapes> .

# Shapes collection
</api/shapes>
  hydra:memberAssertion
    [
      hydra:property rdf:type ;
      hydra:object sh:NodeShape ;
    ] ;
  ] ;
  hydra:search
  [
    hydra:template "{?resource}" ;
    hydra:mapping
    [
      hydra:variable "resource" ;
      hydra:property sh:targetNode ;
    ] ;
  ] ;
.

So, when the app want to display a UI for resource /foo/bar/baz, it would find the collection and then fetch /api/shapes?resource=/foo/bar/baz. Discovery is done with Alcaeus

import type { Resource } from 'alcaeus'
import { rdf, sh } from '@tpluscode/rdf-ns-builders/strict'

let entrypoint: Resource

const [shapesCollection] = entrypoint.getCollections({
    predicate: rdf.type,
    object: sh.NodeShape,
  }) || []

cons shapes = await shapesCollection.load()

Of course, you could could adapt this approach to specific needs:

tpluscode commented 2 years ago

It's not perfect yet. In fact, inspired by your question I figured a way to slightly improve what I have been doing.

Here's the code which does the discovery of shapes in my latest development: https://github.com/wikibus/wikibus/blob/master/apps/www/src/lib/shapeLoader/dereferenced.ts

And here's the actual collection resource: https://github.com/wikibus/wikibus/blob/master/apps/api/resources/api/shapes.ttl

Notice that the filterByTargetNode function not only filters by sh:targetNode but will also match on the resource's types and sh:targetClass/dash:applicableToClass. The filter is not relying on the hydra:property sh:targetNode annotation verbatim

lambdakris commented 2 years ago

Very interesting! Still being fairly new to this, I got a couple of follow up questions. In your first example, where/when does the "resource" variable replacement occur for the template? In your second example, is that a sort of remote code execution? More generally, would something like the shapes collection be necessary if using OWL or would one be able to simply dereference the types of the resource to get its different representations?

tpluscode commented 2 years ago

In your first example, where/when does the "resource" variable replacement occur for the template?

This is still a bit in flux. I just pushed my latest update which simplifies that process. This uses the IRITemplate feature of alcaeus. In this line I set the sh:targetNode property to an in-memory representation of the template params. It gets fed into the template according to the hydra:mapping.

In your second example, is that a sort of remote code execution?

Depending how you want to view that. In essence, I simple dereference a resource. Yes, it does execute code remotely, although that fact is irrelevant to a REST client. The important part is that the application does not have too much hard-coded. By using the hydra:collection, a client only knows that it should look for such a collection whose members are shapes ([ hydra:property rdf:type ; hydra:object sh:NodeShape ])

As I mentioned above, SHACL is one option. OWL could totally be another.

And yes, to dereference the resource types could totally work too. The reason I prefer shapes though, is that external vocabulary terms are often not dereferencabe or at least unstable. They are also outside of your control, which may or may not be a good thing. On the other hand, shapes are purpose-built. And I like them that way. For example, you might have different views, of varying level of detail. Each described with its own shape. Or shapes to display a collection of items in multiple ways: table, grid, map? This goes way beyond simply defining domain models. Domain models, which have a different purpose.

Ok, but I realise I digress now. Coming back to your original question

would one be able to simply dereference the types of the resource to get its different representations?

I think we're looking at a very similar thing. However, given a resource such as <> a schema:Person you cannot dereference at all. You might introduce a new class and make it <> a schema:Person, </api/Person>. The latter will now dereference. But how do you define client behaviour?

I find it easier to codify and replicate that a client always does the same thing:

  1. Find hydra:collection with member assertion of rdf:type+sh:NodeShape (or OWL, or whatever)
  2. Set the template variable to find relevant models
  3. Send GET request.

This removes the necessity for exposing the shapes/model up-front and does not burden the client with unnecessary decision making about what and when dereference (or worst of all, try and fail)