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-modelserialization 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.
NoteOnly 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::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, andtotal, 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:
-
Define the API endpoint using the
Clientclass. -
Create a
ModelRegisterto manage the resource models and their respective endpoints. -
Define the resource models using the
Resourceclass. -
Register the models with the
ModelRegisterand define their relationships using theadd_endpointmethod.
Once data definition is present, the following operations can be performed at runtime:
-
Fetch resources from the API using the
ModelRegisterandLink#realizemethods.-
Once the resources are fetched, you can access their attributes and links and navigate through the resource graph.
-
-
Pagination, such as on "index" type pages, can be handled by subclassing the
Pageclass.NoteThe Pageclass itself is also implemented as aResource, 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:
-
Define HAL resource models.
-
Define the base API URL using the
Clientclass. -
Create a
ModelRegisterto manage the resource models. -
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.
|
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::Serialization, 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
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.
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.
HAL Links
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
_linkssection 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::Linkif 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::Serializableif 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
ProductLinkswhich contains:-
a
selfattribute that is an instance ofProductLink -
a
categoryattribute that is an instance ofCategoryLink
-
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 classProductLinks. -
the link set contains the
selfand thecategorylinks of classLutaml::Hal::Link.
As a result:
-
calling
product.links.selfwill return an instance ofProductLink. -
calling
product.links.self.realize(register)will dynamically fetch and return an instance ofProduct.
Custom link set class
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.
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
Custom link class
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.
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
indexorresource. 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( <b class="conum">(1)</b>
id: :model_index, <b class="conum">(2)</b>
type: :index, <b class="conum">(3)</b>
url: '/url_supporting_interpolation/{param}', <b class="conum">(4)</b>
model: ModelClass <b class="conum">(5)</b>
)
-
The
add_endpointmethod is used to register an endpoint for a model. -
The
idis a unique identifier for the endpoint, which is required to fetch the resource later. -
The
typespecifies the type of endpoint, which can beindexorresource. Theindextype is used for collections, while theresourcetype is used for individual resources. -
The
urlis 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. -
The
modelis the class of the resource that will be fetched from the API. The class must inherit fromLutaml::Hal::Resource.
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
)
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.
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.
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">
# ]>>
Fetching a resource via link realization
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.
Syntax:
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
_linkssection 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.
|
product_2 = product_index.links.products.last.realize(register)
# => 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">
# ]}>
Pagination
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. The
Page class itself is implemented as a Resource, so you can use the same
methods to access the page’s attributes and links.
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.
Syntax:
class MyPage < Lutaml::Hal::Page
# These are typical links given for page objects
hal_link :self, key: 'self', realize_class: 'MyPage'
hal_link :prev, key: 'prev', realize_class: 'MyPage'
hal_link :next, key: 'next', realize_class: 'MyPage'
hal_link :first, key: 'first', realize_class: 'MyPage'
hal_link :last, key: 'last', realize_class: 'MyPage'
end
register.add_endpoint(
id: :my_pages,
type: :index,
url: '/my_pages',
model: MyPage
)
Where,
MyPage-
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.
Declaration:
class MyPage < Lutaml::Hal::Page
hal_link :self, key: 'self', realize_class: 'MyPage'
hal_link :prev, key: 'prev', realize_class: 'MyPage'
hal_link :next, key: 'next', realize_class: 'MyPage'
hal_link :first, key: 'first', realize_class: 'MyPage'
hal_link :last, key: 'last', realize_class: 'MyPage'
end
register.add_endpoint(
id: :my_pages,
type: :index,
url: '/my_pages',
model: MyPage
)
Usage:
page_1 = register.fetch(:my_pages)
# => client.get('/my_pages')
# => {
# "page": 1,
# "pages": 10,
# "limit": 10,
# "total": 100,
# "_links": {
# "self": { "href": "/my_pages" },
# "next": { "href": "/my_pages/2" },
# "last": { "href": "/my_pages/9" }
# }
# }
page_1
# => #<MyPage page: 1, pages: 10, limit: 10, total: 100,
# links: #<MyPageLinks self: <MyPageLink href: "/my_pages">,
# next: <MyPageLink href: "/my_pages/2">,
# last: <MyPageLink href: "/my_pages/9">>>
page_2 = page.links.next.realize(register)
# => client.get('/my_pages/2')
# => #<MyPage page: 2, pages: 10, limit: 10, total: 100,
# links: #<MyPageLinks self: <MyPageLink href: "/my_pages/2">,
# prev: <MyPageLink href: "/my_pages/1">,
# next: <MyPageLink href: "/my_pages/3">,
# first: <MyPageLink href: "/my_pages/1">,
# last: <MyPageLink href: "/my_pages/9">>>
License and Copyright
This project is licensed under the BSD 2-clause License. See the LICENSE.md file for details.
Copyright Ribose.