Skip to content

LutaML Models for HAL APIs (Hypertext Application Language)

License

Notifications You must be signed in to change notification settings

lutaml/lutaml-hal

Repository files navigation

LutaML Model for HAL

GitHub Stars GitHub Forks License Build Status RubyGems Version

Purpose

The lutaml-hal gem provides a framework for interacting with HAL-compliant APIs using the power of LutaML Models.

Hypertext Application Language (HAL) (HAL Internet-Draft) is a simple format for representing resources and their relationships in a hypermedia-driven API.

It allows clients to navigate and interact with resources using links, making it easier to build flexible and extensible applications.

This library provides a set of classes and methods for modeling HAL resources, links, and collections, as well as a client for making HTTP requests to HAL APIs.

Features

  • Classes for modeling HAL resources and links

  • A client for making HTTP requests to HAL APIs

  • Tools for pagination and resource resolution

  • Integration with the lutaml-model serialization framework

  • Error handling and response validation for API interactions

Installation

Add this line to your application’s Gemfile:

gem 'lutaml-hal'

And then execute:

$ bundle install

Or install it yourself as:

$ gem install lutaml-hal

Structure

The classes in this library are organized into the following modules:

Lutaml::Hal::Client

A client for making HTTP requests to HAL APIs. It includes methods for setting the API endpoint, making GET requests, and handling responses.

Note
Only GET requests are supported at the moment.
Lutaml::Hal::ModelRegister

A registry for managing HAL resource models and their endpoints. It allows you to register models, define their relationships, and fetch resources from the API.

Lutaml::Hal::GlobalRegister

A global registry (Singleton) for managing ModelRegisters and facilitating model resolution across different resources. Its usage is optional.

Lutaml::Hal::Resource

A base class for defining HAL resource models. It includes methods for defining attributes, links, and key-value mappings for resources.

Lutaml::Hal::Link

A class for defining HAL links. It includes methods for specifying the relationship between resources and their links, as well as methods for resolving links to their target resources.

Lutaml::Hal::Page

A class for handling pagination in HAL APIs. It includes methods for defining pagination attributes, such as page, pages, limit, and total, as well as methods for accessing linked resources within a page.

Usage overview

In order to interact with a HAL API using lutaml-hal, there are two stages of usage: data definition and runtime.

At the data definition phase:

  1. Define the API endpoint using the Client class.

  2. Create a ModelRegister to manage the resource models and their respective endpoints.

  3. (optional) Create a GlobalRegister to manage one or more ModelRegister instances. It is necessary for automatic Link resolution.

  4. Define the resource models using the Resource class.

  5. Register the models with the ModelRegister and define their relationships using the add_endpoint method.

Once data definition is present, the following operations can be performed at runtime:

  1. Fetch resources from the API using the ModelRegister and Link#realize methods.

    1. Once the resources are fetched, you can access their attributes and links and navigate through the resource graph.

  2. Pagination, such as on "index" type pages, can be handled by subclassing the Page class.

    Note
    The Page class itself is also implemented as a Resource, so you can use the same methods to access the page’s attributes and links.

Usage: Data definition

General

HAL resources need to be defined as models to allow data access and serialization.

The following steps are required:

  1. Define HAL resource models.

  2. Define the base API URL using the Client class.

  3. Create a ModelRegister to manage the resource models.

  4. Define the resource models' respective endpoints on the base API URL.

Creating a HAL model register

The ModelRegister class is used to manage the resource models and their respective endpoints on the base API URL.

It relies on the Client class to perform HTTP requests to the API. The base API URL is defined at the Client object.

Note
The base API URL is used for all requests made by the Client class, including the requests made by the ModelRegister class.
require 'lutaml-hal'

# Create a new client with API endpoint
client = Lutaml::Hal::Client.new(api_url: 'https://api.example.com')
register = Lutaml::Hal::ModelRegister.new(name: :my_model_register, client: client)
# Or set client later, `register.client = client`

The name: parameter is used to identify the ModelRegister instance.

Creating a HAL global register

The GlobalRegister class is a singleton that manages one or more ModelRegister instances.

It is optional, but is required for automatic realization of models from Link objects. See Fetching a resource via link realization for more details.

require 'lutaml-hal'

# Create a new client with API endpoint
client = Lutaml::Hal::Client.new(api_url: 'https://api.example.com')
register = Lutaml::Hal::ModelRegister.new(name: :my_model_register, client: client)

# Register the ModelRegister with the global register
global_register = Lutaml::Hal::GlobalRegister.instance.register(:my_model_register, register)

# Obtain the global register
global_register.get(:my_model_register)

# Delete a register mapping
global_register.delete(:my_model_register)

Defining HAL resource models

General

A HAL resource is defined by creating a subclass of the Resource class and defining its attributes, links, and key-value mappings.

The Resource class is the base class for defining HAL resource models. It inherits from Lutaml::Model::Serializable, which provides data modelling and serialization capabilities.

The declaration of attributes, links, and key-value mappings for a HAL resource is performed using the attribute, hal_link, and key_value methods.

There are 3 levels of data modeling in a HAL resource, all of which are necessary for the full usage of a HAL resource:

  • Resource attributes

  • Serialization mappings

  • HAL Links

Example 1. Integrated example of a resource model
module MyApi
  class Product < Lutaml::Hal::Resource
    attribute :id, :string
    attribute :name, :string
    attribute :price, :float

    hal_link :self, key: 'self', realize_class: 'Product'
    hal_link :category, key: 'category', realize_class: 'Category'

    key_value do
      map 'id', to: :id
      map 'name', to: :name
      map 'price', to: :price
    end
  end
end

Resource attributes

A resource attribute is a direct property of the HAL resource.

These attributes typically hold values of simple data types, and are directly serialized into JSON.

These attributes are declared using the attribute method from lutaml-model.

A HAL resource of class Product can have attributes id, name, and price.

Please refer to syntax as described in the lutaml-model documentation.

Example 2. Example of a resource model with attributes
module MyApi
  class Product < Lutaml::Hal::Resource
    attribute :id, :string
    attribute :name, :string
    attribute :price, :float
    # ...
  end
end

Serialization mapping of resource attributes

A serialization mapping defines rules to serialize a HAL resource to and from a serialization format. In HAL, the serialization format is JSON, but other formats can also be supported.

The mapping between the HAL model attributes and their corresponding JSON serialization is performed using the key_value do or json do blocks from lutaml-model. The mapping of the contents of _links is automatically performed using hal_link.

A HAL resource of class Product with attributes id, name, and price will need to declare a key_value block to map the attributes to their corresponding JSON keys, namely, "id", "name", and "price".

Please refer to syntax as described in the lutaml-model documentation.

Example 3. Example of a resource model with serialization mapping
module MyApi
  class Product < Lutaml::Hal::Resource
    attribute :id, :string
    attribute :name, :string
    attribute :price, :float

    key_value do
      map 'id', to: :id
      map 'name', to: :name
      map 'price', to: :price
    end
  end
end

A HAL resource has links to other resources, typically serialized in the _links section of the JSON response.

A HAL resource of class Product can have links self (which is a self-referential identifier link) and category.

HAL links need to be defined in the resource model to allow the resolution of the links to their target resources.

These links are declared using the hal_link method provided by lutaml-hal.

Syntax:

hal_link :link_name,
  key: 'link_key',
  realize_class: 'TargetResourceClass',
  link_class: 'LinkClass',
  link_set_class: 'LinkSetClass'
:link_name

The name of the link, which will be used to access the link in the resource object.

key: 'link_key'

The key of the link in the JSON response. This is the name of the link as it appears in the _links section of the HAL resource.

realize_class: 'TargetResourceClass'

The class of the target resource that the link points to. This is used to resolve the link to the associated resource.

link_class: 'LinkClass'

(optional) The class of the link that defines specific behavior or attributes for the link object itself. This is dynamically created and is inherited from Lutaml::Hal::Link if not provided.

link_set_class: 'LinkSetClass'

(optional) The class of the link set object that contains the links. This is dynamically created and is inherited from Lutaml::Model::Serializable if not provided.

The _links section is modeled as a dynamically created link set class, named after the resource’s class name (with an appended LinkSet string), which in turn contains the defined links to other resources. The link set class is inherited from Lutaml::Model::Serializable.

A HAL resource of class Product may have a link set of class ProductLinkSet which contains the self and category links as its attributes.

Each link object of the link set is provided as a Link object that is dynamically created for the type of resolved resource. The name of the link class is the same as the resource class name with an appended Link string. This Link class is inherited from Lutaml::Hal::Link.

A HAL resource of class Product with a link set that contains the self (points to a Product) and category (points to a Category) links will have:

  • a link set of class ProductLinks which contains:

    • a self attribute that is an instance of ProductLink

    • a category attribute that is an instance of CategoryLink

Example 4. Integrated example of a HAL resource model using auto-generated LinkSet and Link classes

For an instance of Product:

module MyApi
  class Product < Lutaml::Hal::Resource
    attribute :id, :string
    attribute :name, :string
    attribute :price, :float

    hal_link :self, key: 'self', realize_class: 'Product'
    hal_link :category, key: 'category', realize_class: 'Category'

    key_value do
      map 'id', to: :id
      map 'name', to: :name
      map 'price', to: :price
    end
  end
end

The library will provide:

  • the link set (serialized in HAL as JSON _links) in the class ProductLinks.

  • the link set contains the self and the category links of class Lutaml::Hal::Link.

As a result:

  • calling product.links.self will return an instance of ProductLink.

  • calling product.links.self.realize(register) will dynamically fetch and return an instance of Product.

When a custom link set class (via link_set_class:) is provided, links are no longer automatically added to the link set via hal_link. Please ensure that all links are defined as model attributes and their key_value mappings provided.

This is useful for the scenario where the link set needs to be customized to provide additional attributes or behavior.

A LinkSetClass for a resource must implement the following interface:

module MyApi
  # This represents the link set of a Resource
  class ResourceLinkSet < Lutaml::Model::Serializable
    attribute :attribute_name_1, :link_class_1, collection: {true|false}
    attribute :attribute_name_2, :link_class_2, collection: {true|false}
    # ...

    key_value do
      map 'link_key_1', to: :attribute_name_1
      map 'link_key_2', to: :attribute_name_2
      # ...
    end
  end

  # This represents the basic setup of a Resource with a custom LinkSet class
  class Resource < Lutaml::Hal::Resource
    attribute :links, ResourceLinkSet
    # Define resource attributes

    key_value do
      # This is the mapping of the `_links` key to the attribute `links`.
      map '_links', to: :links
      # Mappings for resource attributes need to be explicitly provided
    end
  end
end

Alternatively, it is possible to re-open the dynamically created link set class and add additional attributes to it.

Override the default link set class for Product
module MyApi
  class Product < Lutaml::Hal::Resource
    attribute :id, :string
  end
  # The class `MyApi::ProductLinkSet` is created automatically by the library.

  # Re-open the default link set class and add additional attributes
  class ProductLinkSet < Lutaml::Hal::LinkSet
    # Add additional attributes to the link set
    attribute :custom_link_set_attribute, Something, collection: false

    key_value do
      map 'my_custom_link', to: :custom_link_set_attribute
    end
  end
end

When a custom link class (via link_class:) is provided, the custom link class is automatically added into the link set.

This makes it possible to:

  • supplement the link with additional attributes, or

  • override the realize(register) method to provide custom behavior for the link.

A Link class pointing to a resource must implement the following interface:

module MyApi
  # This represents a link set pointing to a Resource
  class TargetResourceLink < Lutaml::Model::Serializable
    # This is the link class for the resource class Resource
    # 'default:' needs to be set to the name of the target resource class
    attribute :type, :string, default: 'Resource'

    # No specification of key_value block needed since attribute presence
    # provides a default mapping.
  end
end

Alternatively, it is possible to re-open the dynamically created link class and add additional attributes to it.

Override the default link class for Product
module MyApi
  class Product < Lutaml::Hal::Resource
    attribute :id, :string
    hal_link :category, key: 'category', realize_class: 'Category'
  end
  # The class `MyApi::CategoryLink` is created automatically by the library.

  # Re-open the default link class and add additional attributes
  class CategoryLink < Lutaml::Hal::Link
    # Add additional attributes to the link
    attribute :language_code, :string, collection: false

    key_value do
      map 'language_code', to: :language_code
    end
  end
end

Registering resource models and endpoints

The ModelRegister allows you to register resource models and their endpoints.

You can define endpoints for collections (index) and individual resources (resource) using the add_endpoint method.

The add_endpoint method takes the following parameters:

id

A unique identifier for the endpoint.

type

The type of endpoint, which can be index or resource.

url

The URL of the endpoint, which can include path parameters.

model

The class of the resource that will be fetched from the API. The class must inherit from Lutaml::Hal::Resource.

In the url, you can use interpolation parameters, which will be replaced with the actual values when fetching the resource. The interpolation parameters are defined in the url string using curly braces {}.

The add_endpoint method will automatically handle the URL resolution and fetch the resource from the API.

When the ModelRegister fetches a resource using the realize method, it will match the resource URL against registered paths in order to find the appropriate model class to use for deserialization and resolution.

Syntax:

register.add_endpoint( (1)
  id: :model_index, (2)
  type: :index, (3)
  url: '/url_supporting_interpolation/{param}', (4)
  model: ModelClass (5)
)
  1. The add_endpoint method is used to register an endpoint for a model.

  2. The id is a unique identifier for the endpoint, which is required to fetch the resource later.

  3. The type specifies the type of endpoint, which can be index or resource. The index type is used for collections, while the resource type is used for individual resources.

  4. The url is the URL of the endpoint, which can include path parameters. The URL can also include interpolation parameters, which will be replaced with the actual values when fetching the resource.

  5. The model is the class of the resource that will be fetched from the API. The class must inherit from Lutaml::Hal::Resource.

Example 5. Example of registering the Product class to both index and resource endpoints
register.add_endpoint(
  id: :product_index,
  type: :index,
  url: '/products',
  model: Product
)

register.add_endpoint(
  id: :product_resource,
  type: :resource,
  url: '/products/{id}',
  model: Product
)

Defining HAL page models

HAL index APIs often support pagination, which allows clients to retrieve a limited number of resources at a time.

The Lutaml::Hal::Page class is used to handle pagination in HAL APIs. It is a subclass of Resource, and provides additional attributes and methods for handling pagination information

The Page class by default supports the following attributes:

page

The current page number.

pages

The total number of pages.

limit

The number of resources per page.

total

The total number of resources.

The way to use the Page class is through inheritance from it, where the class will automatically create the necessary links for typical page objects.

The typical links of a page object are:

self

A link to the current page.

prev

A link to the previous page.

next

A link to the next page.

first

A link to the first page.

last

A link to the last page.

The "realize class" of these links are the same as the inherited page object, ensuring consistency in the pagination model.

Syntax:

class ProductIndex < Lutaml::Hal::Page
  # No attributes necessary
end

register.add_endpoint(
  id: :product_index,
  type: :index,
  url: '/products',
  model: ProductIndex
)

page_1 = register.fetch(:product_index)  # Updated to use the correct endpoint id
page_2_link = page_1.links.next
# => <#ProductIndexLink href: "/products/2", title: "Next Page">

Where,

ProductIndex

The class of the page that will be fetched from the API. The class must inherit from Lutaml::Hal::Page.

register

The instance of ModelRegister.

id

The ID of the pagination endpoint to be registered in the ModelRegister.

url

The URL of the pagination endpoint.

model

The class of the page that will be fetched from the API.

Usage: Runtime

General

Note
The lutaml-hal library currently only supports synchronous data fetching. Asynchronous data fetching will be supported in the future.
Note
The lutaml-hal library currently only supports data fetching requests (GET) today. Additional features may be provided in the future.

Once the data definition is complete, you can use the ModelRegister to fetch and interact with resources from the API.

Fetching a resource

The ModelRegister allows you to fetch resources from the API using the fetch method.

Note
The endpoint of the resource must be already defined through the add_endpoint method.

The fetch method will automatically handle the URL resolution and fetch the resource from the API.

Syntax:

register.fetch(:resource_endpoint_id, {parameters})

Where,

resource_endpoint_id

The ID of the endpoint registered in the ModelRegister.

parameters

A hash of parameters to be passed to the API. The parameters are used to replace the interpolation parameters in the URL.

register

The instance of ModelRegister.

Example 6. Fetch a resource directly from the API
product_1 = register.fetch(:product_resource, id: 1)
# => client.get('/products/1')

# => {
#   "id": 1,
#   "name": "Product 1",
#   "price": 10.0,
#   "_links": {
#     "self": { "href": "/products/1" },
#     "category": { "href": "/categories/1", "title": "Category 1" },
#     "related": [
#        { "href": "/products/3", "title": "Product 3" },
#        { "href": "/products/5", "title": "Product 5" }
#     ]
#   }
# }

product_1
# => #<Product id: 1, name: "Product 1", price: 10.0, links:
#      #<ProductLinks self: <ProductLink href: "/products/1">,
#                     category: <ProductLink href: "/categories/1", title: "Category 1">,
#                     related: [
#                         <ProductLink href: "/products/3", title: "Product 3">,
#                         <ProductLink href: "/products/5", title: "Product 5">
#                     ]}>

Fetching a resource index

In HAL, collections are provided via the _links or the _embedded sections of the response.

Note
The _embedded section is not yet supported by the Lutaml::Hal library.

The ModelRegister allows you to define endpoints for collections and fetch them using the fetch method.

The fetch method will automatically handle the URL resolution and fetch the resource index from the API.

Syntax:

register.fetch(:index_endpoint_id)

Where,

index_endpoint_id

The ID of the endpoint registered in the ModelRegister.

register

The instance of ModelRegister.

Example 7. Fetch a collection of resources from the API
product_index = register.fetch(:product_index)
# => client.get('/products')

# => {
# "page": 1,
# "pages": 10,
# "limit": 10,
# "total": 45,
# "_links": {
#   "self": { "href": "/products/1" },
#   "next": { "href": "/products/2" },
#   "last": { "href": "/products/5" },
#   "products": [
#     { "href": "/products/1", "title": "Product 1" },
#     { "href": "/products/2", "title": "Product 2" }
#   ]
# }

product_index
# => #<ProductPage page: 1, pages: 10, limit: 10, total: 45,
#      links: #<ProductLinks self: <ProductLink href: "/products/1">,
#                     next: <ProductLink href: "/products/2">,
#                     last: <ProductLink href: "/products/5">,
#                     products: <ProductLinks
#                         <ProductLink href: "/products/1", title: "Product 1">,
#                         <ProductLink href: "/products/2", title: "Product 2">
#                     ]>>

Given a resource index that contains links to resources, the individual resource links can be "realized" as actual model instances through the Link#realize(register:) method which dynamically retrieves the resource.

Given a Link object, the realize method fetches the resource from the API using the provided register.

There are two ways a resource gets realized from a Link object:

  • If a Lutaml::Hal::GlobalRegister is used, and the Link object originated from a fetch using a ModelRegister then the realize method has sufficient information to automatically fetch the resource from the API using the same register.

    Note
    This relies on the Hal::REGISTER_ID_ATTR_NAME attribute to be set in the ModelRegister class. This attribute is used to identify the resource endpoint ID in the URL.
  • If a GlobalRegister is not used, even if the Link object originated from a fetch using a ModelRegister, the realize method does not have sufficient information to fetch the resource from the API using the same register. In this case an explicit register must be provided to the realize(register: …​) method.

Syntax for standalone usage:

Lutaml::Model::Link.new(
  href: 'resource_endpoint_href',
  # ... other attributes
).realize(register)

Where,

resource_endpoint_href

The href of the resource endpoint. This is the URL of the resource as it appears in the _links section of the HAL resource.

register

The instance of ModelRegister.

The realize method will automatically handle the URL resolution and fetch the resource from the API, and return an instance of the resource class defined in the ModelRegister (through the endpoint definition of realize_class).

Note
It is possible to use the realize method on a link object using another ModelRegister instance. This is useful when you want to resolve a link using a different API endpoint or a different set of resource models.

Syntax when using a GlobalRegister:

resource_index = model_register.fetch(:resource_index)
resource_index.links.products.first.realize
# => client.get('/resources/1')
Example 8. Dynamically realizing a resource from the collection using links
# Without a GlobalRegister
product_2 = product_index.links.products.last.realize(register)

# With a GlobalRegister
product_2 = product_index.links.products.last.realize

# => client.get('/products/2')
# => {
#   "id": 2,
#   "name": "Product 2",
#   "price": 20.0,
#   "_links": {
#     "self": { "href": "/products/2" },
#     "category": { "href": "/categories/2", "title": "Category 2" },
#     "related": [
#        { "href": "/products/4", "title": "Product 4" },
#        { "href": "/products/6", "title": "Product 6" }
#     ]
#   }
# }

product_2
# => #<Product id: 2, name: "Product 2", price: 20.0, links:
#      #<ProductLinks self: <ProductLink href: "/products/2">,
#                     category: <ProductLink href: "/categories/2", title: "Category 2">,
#                     related: [
#                         <ProductLink href: "/products/4", title: "Product 4">,
#                         <ProductLink href: "/products/6", title: "Product 6">
#                     ]}>

# Without a GlobalRegister
product_2_related_1 = product_2.links.related.first.realize(register)

# With a GlobalRegister
product_2_related_1 = product_2.links.related.first.realize

Handling HAL pages / pagination

The Lutaml::Hal::Page class is used to handle pagination in HAL APIs.

As described in Defining HAL page models, subclassing the Page class provides pagination capabilities, including the management of links to navigate through pages of resources.

Example 9. Usage example of the Page class

Declaration:

class ResourceIndex < Lutaml::Hal::Page
  # No attribute definition necessary
end

register.add_endpoint(
  id: :resource_index,
  type: :index,
  url: '/resources',
  model: ResourceIndex
)

Usage:

page_1 = register.fetch(:resource_index)
# => client.get('/resources')
# => {
#   "page": 1,
#   "pages": 10,
#   "limit": 10,
#   "total": 100,
#   "_links": {
#     "self": {
#       "href": "https://api.example.com/resources?page=1&items=10"
#     },
#     "first": {
#       "href": "https://api.example.com/resources?page=1&items=10"
#     },
#     "last": {
#       "href": "https://api.example.com/resources?page=10&items=10"
#     },
#     "next": {
#       "href": "https://api.example.com/resources?page=2&items=10"
#     }
#   }
# }

page_1
# => #<ResourceIndex page: 1, pages: 10, limit: 10, total: 100,
#      links: #<ResourceIndexLinks
#                self: #<ResourceIndexLink href: "/resources?page=1&items=10">,
#                next: #<ResourceIndexLink href: "/resources?page=2&items=10">,
#                last: #<ResourceIndexLink href: "/resources?page=10&items=10">>>

# Without a GlobalRegister
page_2 = page.links.next.realize(register)

# With a GlobalRegister
page_2 = page.links.next.realize

# => client.get('/resources?page=2&items=10')
# => #<ResourceIndex page: 2, pages: 10, limit: 10, total: 100,
#      links: #<ResourceIndexLinks
#                self: #<ResourceIndexLink href: "/resources?page=2&items=10">,
#                prev: #<ResourceIndexLink href: "/resources?page=1&items=10">,
#                next: #<ResourceIndexLink href: "/resources?page=3&items=10">,
#                first: #<ResourceIndexLink href: "/resources?page=1&items=10">,
#                last: #<ResourceIndexLink href: "/resources?page=10&items=10">>>,
#                prev: #<ResourceIndexLink href: "/resources?page=1&items=10">>>

This project is licensed under the BSD 2-clause License. See the LICENSE.md file for details.

Copyright Ribose.

About

LutaML Models for HAL APIs (Hypertext Application Language)

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Languages