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

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

Open
lambdakris opened this issue May 24, 2022 · 6 comments

Comments

@lambdakris
Copy link

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
Copy link
Member

Indeed, clownface doe not have a feature to get edges in or out of a node (but watch zazuko/clownface#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
Copy link
Author

lambdakris commented May 25, 2022

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
Copy link
Member

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:

  • add more query params to allow filtering more fine-grained filtering of the fetched shapes
  • load all shapes to memory rather than looking up a collection
  • use a different modeling vocab altogether instead of SHACL

@tpluscode
Copy link
Member

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
Copy link
Author

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
Copy link
Member

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)

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