Sign inSign up

semtech/mu-cl-resources

By semtech

Updated 2 months ago

High-level abstractions for generating generic jsonapi compliant resources configured in Common Lisp

Image
4

10K+

semtech/mu-cl-resources repository overview

JSONAPI to SPARQL, and back

mu-cl-resources provides a JSONAPI compatible interface to the content specified in the configuration. Most configuration occurs in the configuration/domain.json or configuration/domain.lisp file (your choice), an examples of which can be found in this repository.

Most configuration happens in the domain file. See configuration/domain.json and configuration/domain.lisp for an introduction. This file defines the glue between the JSON world and the RDF world. When defining a model, be sure to have a good idea on what both worlds will look like.

The documentation offered here is not exhaustive. This component handles a wide variety of use-cases and has support for esotheric features for experimentation, which may land in the core at a later time. As such, some features which the component offers are not documented in this readme.

The domain.json format is still growing, some configuration parameters can only be set in the lisp variant. You can combine both formats, see Tutorial: Combining domain.lisp and domain.json.

Tutorials

Add mu-cl-resources to a stack

Add the following snippet to the services block of your docker-compose.yml:

services:
  resource:
    image: semtech/mu-cl-resources:1.20.0
    links:
      - db:database
    volumes:
      - ./config/resources:/config

Next, copy the configuration files from the examples/ folder in this repository inside the ./config/resources folder of your project. You have to copy either the JSON configuration file examples/domain.json or the Lisp configuration files examples/domain.lisp and examples/repository.lisp.

Finally, add a new rule to the dispatcher configuration of your project in ./config/dispatcher/dispatcher.ex to forward requests to /themes to the new resource service:

  get "/themes/*path", @any do
    forward conn, path, "http://resource/themes/"
  end

Start your stack running docker-compose up -d. Assuming the identifier is published on port 80, sending a request to http://localhost/themes should return an empty array coming from mu-cl-resources.

Since it is common for a mu.semte.ch project to contain mu-cl-resources, the default blueprint for a mu.semte.ch project, mu-semtech/mu-project, contains mu-cl-resources.

Introduction to a configuration through domain.lisp

This service is driven from the domain.lisp file which you should adapt to describe your domain. In this section we briefly describe how everything is wired together, and how you can quickly get an API up and running.

mu-cl-resources is driven from the domain.lisp file. This file describes the connection between the JSONAPI and the semantic model. Secondly, there is the repository.lisp file, in which you can define new prefixes to shorten your domain description. This repository contains an example of both files in the examples folder.

/configuration/domain.lisp

The domain.lisp contains resource definitions for each resource type in the application. These resource definitions provide a three-way connection:

  • It names things to make connections within the domain.lisp file
  • It describes the properties as seen through the json api
  • It describes the semantic model used in order to implement the json api

Each resource definition is a combination of these three views. Let us assume an example using foaf. In our example, we will model a Person, having one or more online accounts. This model can be vizualised using WebVOWL.

Intermezzo: mu-cl-resources is mainly configured in lisp. Lisp uses parens () for grouping content. If a paren is followed by a word, that word tends to indicate the content of the group. If there is no word, it tends to be a list. Other characters, like the backtick (`) or the comma (,) are best copied from examples.

(define-resource person ()
  :class (s-url "http://xmlns.com/foaf/0.1/Person")
  :properties `((:name :string ,(s-url "http://xmlns.com/foaf/0.1/name")))
  :resource-base (s-url "http://my-application.com/people/")
  :on-path "people")

A simple definition of a person uses the foaf vocabulary to write the person and the person name.

  • Line 1 contains define-resource person, which indicates that we'll create a new endpoint which we will name person in this file. It is most customary to use a singular name for this name.
  • Line 2 specifies that the RDF class to which the person belongs in the triplestore is foaf:Person.
  • Line 3 specifies a singular property of the person. The JSONAPI will assume content of type string is stored in the json key data.attributes.name (because of :name). This value is connected to our resource in the triplestore by the predicate foaf:name. Note that this word may contain dashes, but not capitals (capitals are ignored).
  • Line 4 indicates the URI to use in the triplestore when we create new resources of this type. The supplied url is postfixed with a UUID.
  • Line 5 specifies the endpoint on which we can list/create/update our resource. In our case, requests to /people are mapped to this resource.

Assuming the foaf prefix is defined, we can make this example slightly easier to read. Note the use of s-prefix.

(define-resource person ()
  :class (s-prefix "foaf:Person")
  :properties `((:name :string ,(s-prefix "foaf:name")))
  :resource-base (s-url "http://my-application.com/people/")
  :on-path "people")

This code sample implements the same functionality as the example above, yet it is easier on the eyes.

You may have noticed the double opening parens on line 3, after the :properties keyword. We can insert multiple properties if desired. Ensuring we have the right amount of opening and closing parens, we can update our example to also contain the age of the person, expressed as a number.

(define-resource person ()
  :class (s-prefix "foaf:Person")
  :properties `((:name :string ,(s-prefix "foaf:name"))
                (:age :number ,(s-prefix "foaf:age")))
  :resource-base (s-url "http://my-application.com/people/")
  :on-path "people")

With this minor change, our person supports the name and age attributes.

Most resources link to other resources. Let's first define a second resouce, an OnlineAccount.

(define-resource account ()
  :class (s-prefix "foaf:OnlineAccount")
  :properties `((:name :string ,(s-prefix "foaf:accountName")))
  :resource-base (s-url "http://my-application.com/accounts/")
  :on-path "accounts")

The definition of this account resource is very similar to that of the person resource. How do we link a person to an account? Assuming the person has many accounts, we link by using the :has-many keyword.

(define-resource person ()
  :class (s-prefix "foaf:Person")
  :properties `((:name :string ,(s-prefix "foaf:name"))
                (:age :number ,(s-prefix "foaf:age")))
  :has-many `((account :via ,(s-prefix "foaf:account")
                       :as "accounts"))
  :resource-base (s-url "http://my-application.com/people/")
  :on-path "people")

The statement on lines 5 and 6 specifies that a person may link to many resources of type account. In the triplestore, the link can be found by following the foaf:account property, originating from the person's URI. This relationship is exposed to the JSON API by using the relationship name "accounts". Hence a GET to /people/42/accounts would yield the accounts of the person with UUID 42.

How about getting the person which links to this account. There is only a single person connected to an account. Hence we can use the has-one keyword to symbolize this. In the semantic model of the triplestore, the relationship uses the foaf:account property going from the person to the account. Finding the person for an account therefore means we have to follow the same relationship in the other direction. We can add the option :inverse t to any relationship to make the semantic model follow the inverse arrow. Here, the key in the json body will be owner rather than person.

(define-resource account ()
  :class (s-prefix "foaf:OnlineAccount")
  :properties `((:name :string ,(s-prefix "foaf:accountName")))
  :has-one `((person :via ,(s-prefix "foaf:account")
                     :inverse t
                     :as "owner"))
  :resource-base (s-url "http://my-application.com/accounts/")
  :on-path "accounts")

The complete setup of our user and account looks as follows:

(define-resource person ()
  :class (s-prefix "foaf:Person")
  :properties `((:name :string ,(s-prefix "foaf:name"))
                (:age :number ,(s-prefix "foaf:age")))
  :has-many `((account :via ,(s-prefix "foaf:account")
                       :as "accounts"))
  :resource-base (s-url "http://my-application.com/people/")
  :on-path "people")

(define-resource account ()
  :class (s-prefix "foaf:OnlineAccount")
  :properties `((:name :string ,(s-prefix "foaf:accountName")))
  :has-one `((person :via ,(s-prefix "foaf:account")
                     :inverse t
                     :as "owner"))
  :resource-base (s-url "http://my-application.com/accounts/")
  :on-path "accounts")
/configuration/repositories.lisp

The previous example used the foaf prefix in order to denote classes and properties. The /configuration/repositories.lisp allows you to specify your own prefixes to use in your definitions. A good source for commonly used abbreviations is prefix.cc.

(add-prefix "foaf" "http://xmlns.com/foaf/0.1/")
Resulting API

We intend to support the full spec of JSONAPI. Support for this API comes out of the box with frameworks such as ember-data. Most of what you read there will work, errors being a notable exception. Here, we list some common calls which you could execute using the resources specified above.

  • # GET /people

  • # GET /people/0b29a57a-d324-4302-9c92-61958e4cf250/accounts

  • # GET /people?filter=John

  • # GET /people?filter[age]=42

  • # GET /people?include=accounts

  • # GET /people?filter[:exact:name]=John%20Doe

  • # GET /people?sort=age

  • # GET /accounts?sort=-person.age

  • # POST /people/0b29a57a-d324-4302-9c92-61958e4cf250

  • # PATCH /people/0b29a57a-d324-4302-9c92-61958e4cf250

  • # PATCH /people/0b29a57a-d324-4302-9c92-61958e4cf250/relationships/accounts

  • # DELETE /people/0b29a57a-d324-4302-9c92-61958e4cf250/relationships/accounts

  • # DELETE /people/0b29a57a-d324-4302-9c92-61958e4cf250

More information on each of these calls can be found throughout this document.

More configuration options

The complete mu-cl-resources instance, a specific resource, as well as a property can have options set to override the default behaviour.

  • mu-cl-resources options: These are specified by the defparameter expression as a top-level form in the domain.lisp file.
  • resource specific options: The keyword :features at the same level as the :class may specify options which alter the behaviour of the specific resource.
  • property options: Symbols following the definition of a single property may give mu-cl-resources extra information on how the property will be used.
Introduction to a configuration through domain.json

mu-cl-resources is driven from the domain.json file. This file describes the connection between the JSONAPI and the semantic model. In this section we briefly describe how everything is wired together, and how you can quickly get an API up and running. This repository contains an example file in the examples folder.

/configuration/domain.json

The domain.json contains resource definitions for each resource type in the application. These resource definitions provide a three-way connection:

  • It names things to make connections within the domain.json file
  • It describes the properties as seen through the json api
  • It describes the semantic model used in order to implement the json api

Each resource definition is a combination of these three views. Let us assume an example using foaf. In our example, we will model a Person, having one or more online accounts. This model can be vizualised using WebVOWL.

{
  "version": "0.1",
  "resources": {
    "people": {
      "name": "person",
      "class": "http://xmlns.com/foaf/0.1/Person",
      "attributes": {
        "name": {
          "type": "string",
          "predicate": "http://xmlns.com/foaf/0.1/name"
        }
      },
      "new-resource-base": "https://my-application.com/people/"
    }
  }
}

A simple definition of a person uses the foaf vocabulary to write the person and the person name.

  • Line 2 specifies the endpoint on which we can list/create/update our resource. In our case, requests to /people are mapped to this resource.
  • Line 3 contains the name person we will use to reference the resource in this file. It is most customary to use a singular name for this name.
  • Line 4 specifies that the RDF class to which the person belongs in the triplestore is foaf:Person.
  • Line 5-10 specifies an attribute of the person. The JSONAPI will assume content of type string is stored in the json key data.attributes.name (because of name as attribute key). This value is connected to our resource in the triplestore by the predicate foaf:name. Note that the attribute key may contain dashes, but not capitals (capitals are ignored).
  • Line 11 indicates the URI to use in the triplestore when we create new resources of this type. The supplied url is postfixed with a UUID.

Assuming the foaf prefix is defined, we can make this example slightly easier to read.

{
  "version": "0.1",
  "prefixes": {
    "foaf": "http://xmlns.com/foaf/0.1/"
  },
  "resources": {
    "people": {
      "name": "person",
      "class": "foaf:Person",
      "attributes": {
        "name": {
          "type": "string",
          "predicate": "foaf:name"
        }
      },
      "new-resource-base": "https://my-application.com/people/"
    }
  }
}

This code sample implements the same functionality as the example above, yet it is easier on the eyes.

We can insert multiple attributes if desired. We can update our example to also contain the age of the person, expressed as a number.

{
  "version": "0.1",
  "prefixes": {
    "foaf": "http://xmlns.com/foaf/0.1/"
  },
  "resources": {
    "people": {
      "name": "person",
      "class": "foaf:Person",
      "attributes": {
        "name": {
          "type": "string",
          "predicate": "foaf:name"
        },
        "age": {
          "type": "number",
          "predicate": "foaf:age"
        }
      },
      "new-resource-base": "https://my-application.com/people/"
    }
  }
}

With this minor change, our person supports the name and age attributes.

Most resources link to other resources. Let's first define a second resouce, an OnlineAccount.

{
  "version": "0.1",
  "prefixes": {
    "foaf": "http://xmlns.com/foaf/0.1/"
  },
  "resources": {
    "people": { ... },
    "accounts": {
      "name": "account",
      "class": "foaf:OnlineAccount",
      "attributes": {
        "name": {
          "type": "string",
          "predicate": "foaf:accountName"
        }
      },
      "new-resource-base": "https://my-application.com/accounts/"
    }
  }
}

The definition of this account resource is very similar to that of the person resource. How do we link a person to an account? Assuming the person has many accounts, we link by defining a relationships block on the person resource.

{
  ...
  "resources": {
    "people": {
      "name": "person",
      "class": "foaf:Person",
      "attributes": {
        "name": {
          "type": "string",
          "predicate": "foaf:name"
        },
        "age": {
          "type": "number",
          "predicate": "foaf:age"
        }
      },
      "relationships": {
        "accounts": {
          "predicate": "foaf:account",
          "target": "account",
          "cardinality": "many"
        }
      },
      "new-resource-base": "https://my-application.com/people/"
    },
    "accounts": {
      "name": "account",
      "class": "foaf:OnlineAccount",
      "attributes": {
        "name": {
          "type": "string",
          "predicate": "foaf:accountName"
        }
      },
      "new-resource-base": "https://my-application.com/accounts/"
    }
  }
}

The relationships object specifies that a person may link to many resources of target type account. In the triplestore, the link can be found by following the foaf:account predicate, originating from the person's URI. This relationship is exposed to the JSON API by using the relationship name "accounts". Hence a GET to /people/42/accounts would yield the accounts of the person with UUID 42.

How about getting the person which links to this account. There is only a single person connected to an account. Hence we can specify a relationship with cardinality one on the account resource. In the semantic model of the triplestore, the relationship uses the foaf:account property going from the person to the account. Finding the person for an account therefore means we have to follow the same relationship in the other direction. We can add the property "inverse": true to any relationship to make the semantic model follow the inverse arrow. Here, the key in the json body will be owner rather than person.

{
  ...
  "resources": {
    "people": { ... },
    "accounts": {
      "name": "account",
      "class": "foaf:OnlineAccount",
      "attributes": {
        "name": {
          "type": "string",
          "predicate": "foaf:accountName"
        }
      },
      "relationships": {
        "owner": {
          "predicate": "foaf:account",
          "target": "person",
          "cardinality": "one",
          "inverse": true
        }
      },
      "new-resource-base": "https://my-application.com/accounts/"
    }
  }
}

The complete setup of our user and account looks as follows:

{
  "version": "0.1",
  "prefixes": {
    "foaf": "http://xmlns.com/foaf/0.1/"
  },
  "resources": {
    "people": {
      "name": "person",
      "class": "foaf:Person",
      "attributes": {
        "name": {
          "type": "string",
          "predicate": "foaf:name"
        },
        "age": {
          "type": "number",
          "predicate": "foaf:age"
        }
      },
      "relationships": {
        "accounts": {
          "predicate": "foaf:account",
          "target": "account",
          "cardinality": "many"
        }
      },
      "new-resource-base": "https://my-application.com/people/"
    },
    "accounts": {
      "name": "account",
      "class": "foaf:OnlineAccount",
      "attributes": {
        "name": {
          "type": "string",
          "predicate": "foaf:accountName"
        }
      },
      "relationships": {
        "owner": {
          "predicate": "foaf:account",
          "target": "person",
          "cardinality": "one",
          "inverse": true
        }
      },
      "new-resource-base": "https://my-application.com/accounts/"
    }
  }
}
Resulting API

We intend to support the full spec of JSONAPI. Support for this API comes out of the box with frameworks such as ember-data. Most of what you read there will work, errors being a notable exception. Here, we list some common calls which you could execute using the resources specified above.

  • # GET /people

  • # GET /people/0b29a57a-d324-4302-9c92-61958e4cf250/accounts

  • # GET /people?filter=John

  • # GET /people?filter[age]=42

  • # GET /people?include=accounts

  • # GET /people?filter[:exact:name]=John%20Doe

  • # GET /people?sort=age

  • # GET /accounts?sort=-person.age

  • # POST /people/0b29a57a-d324-4302-9c92-61958e4cf250

  • # PATCH /people/0b29a57a-d324-4302-9c92-61958e4cf250

  • # PATCH /people/0b29a57a-d324-4302-9c92-61958e4cf250/relationships/accounts

  • # DELETE /people/0b29a57a-d324-4302-9c92-61958e4cf250/relationships/accounts

  • # DELETE /people/0b29a57a-d324-4302-9c92-61958e4cf250

More information on each of these calls can be found throughout this document.

More configuration options

Configuration of the complete mu-cl-resource instance can only be done using a configuration file in Lisp. See Tutorial: Configuring settings using a domain.json file.

Combining domain.lisp and domain.json

For larger applications with a broad domain, defining all resources in one domain file may become clumsy and confusing. Mu-cl-resources supports spreading your domain definitions across multiple files. The root domain files must be in Lisp format, but the included files may be in Lisp or JSON format.

To include additional files in your domain configuration, add read-domain-file statements on top of the domain.lisp file.

(in-package :mu-cl-resources)

(read-domain-file "users.json")
(read-domain-file "publications.lisp")

Restart the service. The additional configuration files will be picked up by mu-cl-resources.

Configuring settings using a domain.json file

Most settings can only be configured in Lisp format. This tutorial describes how to configure the settings if you defined your resources using a domain.json file.

First create a domain.lisp file next to the existing domain.json file.

Next, add the following contents to the domain.lisp file:

(in-package :mu-cl-resources)

(read-domain-file "domain.json")

Add your settings in domain.lisp.

Restart the service. The newly configured settings will be picked up by mu-cl-resources.

Reference

Defining resources in Lisp

As the integration with the frontend data-store is handled automatically, most of your time with mu-cl-resources will be spent configuring resources. This overview provides a non-exhaustive list of the most common features of mu-cl-resources.

Each defined resource is specified by the define-resource construction. An example could look like this:

(define-resource person ()
  :class (s-prefix "foaf:Person")
  :properties `((:name :string ,(s-prefix "foaf:name")
                       :required)
                (:age :number ,(s-url "http://xmlns.com/foaf/0.1/age")))
  :has-one `((location :via ,(s-prefix "foaf:based_near")
                       :as "location"))
  :has-many `((account :via ,(s-prefix "foaf:account")
                       :as "accounts")
              (document :via ,(s-prefix "foaf:publications")
                        :as "publications"))
  :features '(include-uri)
  :resource-base (s-url "https://my-application.com/people/")
  :on-path "people")

We will use this example to explain how various features in mu-cl-resources work.

Overview of keys

Each call to define-resource starts out with the name of the resource (used when referring to the resource internally), a set of empty parens (for future use), and a set of key-value pairs. This section gives a brief overview of the valid keys, and what their use is.

  • :class Sets the RDF Class to which instances should belong. Use s-url when setting the full URL.
  • :properties Describes the properties (currently named attributes in the json response) of the resource.
  • :has-one Describes relationships of which at most one is expected to exist.
  • :has-many Describes relationships of which zero or more are expected to exist.
  • :features Optional features to be used in this resource. Our example indicates the URI should be returned as an attribute.
  • :resource-base An s-url containing the prefix for the URI used when creating new resources.
  • :on-path The path on which the resource is supplied, this corresponds to the type property in the JSON body. JSONAPI advises to use the plural form here.
Simple properties

The properties section in the mu-cl-resources configuration corresponds to the attributes in the JSON payload. This se

Tag summary

Content type

Image

Digest

sha256:693e97e44

Size

114.3 MB

Last updated

about 1 year ago

docker pull semtech/mu-cl-resources