lutaml-hal 0.1.7 → 0.1.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.adoc +115 -1145
- data/lib/lutaml/hal/client.rb +24 -0
- data/lib/lutaml/hal/endpoint_configuration.rb +50 -0
- data/lib/lutaml/hal/endpoint_parameter.rb +188 -0
- data/lib/lutaml/hal/global_register.rb +4 -0
- data/lib/lutaml/hal/link.rb +56 -1
- data/lib/lutaml/hal/model_register.rb +170 -63
- data/lib/lutaml/hal/resource.rb +22 -0
- data/lib/lutaml/hal/version.rb +1 -1
- data/lib/lutaml/hal.rb +1 -0
- metadata +4 -2
data/README.adoc
CHANGED
@@ -6,7 +6,6 @@ image:https://img.shields.io/github/license/lutaml/lutaml-hal.svg[License]
|
|
6
6
|
image:https://img.shields.io/github/actions/workflow/status/lutaml/lutaml-hal/test.yml?branch=main[Build Status]
|
7
7
|
image:https://img.shields.io/gem/v/lutaml-hal.svg[RubyGems Version]
|
8
8
|
|
9
|
-
|
10
9
|
== Purpose
|
11
10
|
|
12
11
|
The `lutaml-hal` gem provides a framework for interacting with HAL-compliant
|
@@ -31,7 +30,7 @@ APIs.
|
|
31
30
|
* Tools for pagination and resource resolution
|
32
31
|
* Integration with the `lutaml-model` serialization framework
|
33
32
|
* Error handling and response validation for API interactions
|
34
|
-
|
33
|
+
* Comprehensive embed support for reducing HTTP requests and improving performance
|
35
34
|
|
36
35
|
== Installation
|
37
36
|
|
@@ -56,1212 +55,183 @@ Or install it yourself as:
|
|
56
55
|
$ gem install lutaml-hal
|
57
56
|
----
|
58
57
|
|
59
|
-
==
|
60
|
-
|
61
|
-
The classes in this library are organized into the following modules:
|
62
|
-
|
63
|
-
`Lutaml::Hal::Client`::
|
64
|
-
A client for making HTTP requests to HAL APIs. It includes methods for setting
|
65
|
-
the API endpoint, making GET requests, and handling responses.
|
66
|
-
+
|
67
|
-
NOTE: Only GET requests are supported at the moment.
|
68
|
-
|
69
|
-
`Lutaml::Hal::ModelRegister`::
|
70
|
-
A registry for managing HAL resource models and their endpoints. It allows you
|
71
|
-
to register models, define their relationships, and fetch resources from the
|
72
|
-
API.
|
73
|
-
|
74
|
-
`Lutaml::Hal::GlobalRegister`::
|
75
|
-
A global registry (Singleton) for managing ModelRegisters and facilitating model
|
76
|
-
resolution across different resources. Its usage is optional.
|
77
|
-
|
78
|
-
`Lutaml::Hal::Resource`::
|
79
|
-
A base class for defining HAL resource models. It includes methods for
|
80
|
-
defining attributes, links, and key-value mappings for resources.
|
81
|
-
|
82
|
-
`Lutaml::Hal::Link`::
|
83
|
-
A class for defining HAL links. It includes methods for specifying the
|
84
|
-
relationship between resources and their links, as well as methods for
|
85
|
-
resolving links to their target resources.
|
86
|
-
|
87
|
-
`Lutaml::Hal::Page`::
|
88
|
-
A class for handling pagination in HAL APIs. It includes methods for
|
89
|
-
defining pagination attributes, such as `page`, `pages`, `limit`, and
|
90
|
-
`total`, as well as methods for accessing linked resources within a page.
|
91
|
-
|
92
|
-
|
93
|
-
== Usage overview
|
94
|
-
|
95
|
-
In order to interact with a HAL API using `lutaml-hal`, there are two
|
96
|
-
stages of usage: data definition and runtime.
|
97
|
-
|
98
|
-
At the data definition phase:
|
99
|
-
|
100
|
-
. Define the API endpoint using the `Client` class.
|
101
|
-
. Create a `ModelRegister` to manage the resource models and their
|
102
|
-
respective endpoints.
|
103
|
-
. (optional) Create a `GlobalRegister` to manage one or more `ModelRegister`
|
104
|
-
instances. It is necessary for automatic Link resolution.
|
105
|
-
. Define the resource models using the `Resource` class.
|
106
|
-
. Register the models with the `ModelRegister` and define their
|
107
|
-
relationships using the `add_endpoint` method.
|
108
|
-
|
109
|
-
Once data definition is present, the following operations can be performed at
|
110
|
-
runtime:
|
111
|
-
|
112
|
-
. Fetch resources from the API using the `ModelRegister` and `Link#realize` methods.
|
113
|
-
|
114
|
-
.. Once the resources are fetched, you can access their attributes and links
|
115
|
-
and navigate through the resource graph.
|
116
|
-
|
117
|
-
. Pagination, such as on "index" type pages, can be handled by subclassing the `Page` class.
|
118
|
-
+
|
119
|
-
NOTE: The `Page` class itself is also implemented as a `Resource`, so you can
|
120
|
-
use the same methods to access the page's attributes and links.
|
121
|
-
|
58
|
+
== Quick start
|
122
59
|
|
123
|
-
|
124
|
-
|
125
|
-
=== General
|
126
|
-
|
127
|
-
HAL resources need to be defined as models to allow data access and serialization.
|
128
|
-
|
129
|
-
The following steps are required:
|
130
|
-
|
131
|
-
. Define HAL resource models.
|
132
|
-
. Define the base API URL using the `Client` class.
|
133
|
-
. Create a `ModelRegister` to manage the resource models.
|
134
|
-
. Define the resource models' respective endpoints on the base API URL.
|
135
|
-
|
136
|
-
|
137
|
-
=== Creating a HAL model register
|
138
|
-
|
139
|
-
The `ModelRegister` class is used to manage the resource models and their
|
140
|
-
respective endpoints on the base API URL.
|
141
|
-
|
142
|
-
It relies on the `Client` class to perform HTTP requests to the API. The base
|
143
|
-
API URL is defined at the `Client` object.
|
144
|
-
|
145
|
-
NOTE: The base API URL is used for all requests made by the `Client` class,
|
146
|
-
including the requests made by the `ModelRegister` class.
|
147
|
-
|
148
|
-
[source,ruby]
|
149
|
-
----
|
150
|
-
require 'lutaml-hal'
|
151
|
-
|
152
|
-
# Create a new client with API endpoint
|
153
|
-
client = Lutaml::Hal::Client.new(api_url: 'https://api.example.com')
|
154
|
-
register = Lutaml::Hal::ModelRegister.new(name: :my_model_register, client: client)
|
155
|
-
# Or set client later, `register.client = client`
|
156
|
-
----
|
157
|
-
|
158
|
-
The `name:` parameter is used to identify the `ModelRegister` instance.
|
159
|
-
|
160
|
-
=== Creating a HAL global register
|
161
|
-
|
162
|
-
The `GlobalRegister` class is a singleton that manages one or more
|
163
|
-
`ModelRegister` instances.
|
164
|
-
|
165
|
-
It is optional, but is required for automatic realization of models from Link
|
166
|
-
objects. See <<fetching_resource_via_link_realization>> for more details.
|
60
|
+
Here's a minimal example to get you started:
|
167
61
|
|
168
62
|
[source,ruby]
|
169
63
|
----
|
170
64
|
require 'lutaml-hal'
|
171
65
|
|
172
|
-
#
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
global_register = Lutaml::Hal::GlobalRegister.instance.register(:my_model_register, register)
|
178
|
-
|
179
|
-
# Obtain the global register
|
180
|
-
global_register.get(:my_model_register)
|
181
|
-
|
182
|
-
# Delete a register mapping
|
183
|
-
global_register.delete(:my_model_register)
|
184
|
-
----
|
185
|
-
|
186
|
-
|
187
|
-
=== Defining HAL resource models
|
188
|
-
|
189
|
-
==== General
|
190
|
-
|
191
|
-
A HAL resource is defined by creating a subclass of the `Resource` class and
|
192
|
-
defining its attributes, links, and key-value mappings.
|
193
|
-
|
194
|
-
The `Resource` class is the base class for defining HAL resource models.
|
195
|
-
It inherits from `Lutaml::Model::Serializable`, which provides data
|
196
|
-
modelling and serialization capabilities.
|
197
|
-
|
198
|
-
The declaration of attributes, links, and key-value mappings for a HAL resource
|
199
|
-
is performed using the `attribute`, `hal_link`, and `key_value` methods.
|
200
|
-
|
201
|
-
There are 3 levels of data modeling in a HAL resource, all of which are necessary
|
202
|
-
for the full usage of a HAL resource:
|
203
|
-
|
204
|
-
* Resource attributes
|
205
|
-
* Serialization mappings
|
206
|
-
* HAL Links
|
207
|
-
|
208
|
-
|
209
|
-
.Integrated example of a resource model
|
210
|
-
[example]
|
211
|
-
====
|
212
|
-
[source,ruby]
|
213
|
-
----
|
214
|
-
module MyApi
|
215
|
-
class Product < Lutaml::Hal::Resource
|
216
|
-
attribute :id, :string
|
217
|
-
attribute :name, :string
|
218
|
-
attribute :price, :float
|
219
|
-
|
220
|
-
hal_link :self, key: 'self', realize_class: 'Product'
|
221
|
-
hal_link :category, key: 'category', realize_class: 'Category'
|
222
|
-
|
223
|
-
key_value do
|
224
|
-
map 'id', to: :id
|
225
|
-
map 'name', to: :name
|
226
|
-
map 'price', to: :price
|
227
|
-
end
|
228
|
-
end
|
229
|
-
end
|
230
|
-
----
|
231
|
-
====
|
232
|
-
|
233
|
-
|
234
|
-
==== Resource attributes
|
235
|
-
|
236
|
-
A resource attribute is a direct property of the HAL resource.
|
237
|
-
|
238
|
-
These attributes typically hold values of simple data types, and are directly
|
239
|
-
serialized into JSON.
|
240
|
-
|
241
|
-
These attributes are declared using the `attribute` method from `lutaml-model`.
|
242
|
-
|
243
|
-
[example]
|
244
|
-
====
|
245
|
-
A HAL resource of class `Product` can have attributes `id`, `name`, and `price`.
|
246
|
-
====
|
247
|
-
|
248
|
-
Please refer to syntax as described in the
|
249
|
-
https://github.com/lutaml/lutaml-model[`lutaml-model`] documentation.
|
250
|
-
|
251
|
-
.Example of a resource model with attributes
|
252
|
-
[example]
|
253
|
-
====
|
254
|
-
[source,ruby]
|
255
|
-
----
|
256
|
-
module MyApi
|
257
|
-
class Product < Lutaml::Hal::Resource
|
258
|
-
attribute :id, :string
|
259
|
-
attribute :name, :string
|
260
|
-
attribute :price, :float
|
261
|
-
# ...
|
262
|
-
end
|
263
|
-
end
|
264
|
-
----
|
265
|
-
====
|
266
|
-
|
267
|
-
==== Serialization mapping of resource attributes
|
268
|
-
|
269
|
-
A serialization mapping defines rules to serialize a HAL resource to and from a
|
270
|
-
serialization format. In HAL, the serialization format is JSON, but other formats
|
271
|
-
can also be supported.
|
272
|
-
|
273
|
-
The mapping between the HAL model attributes and their corresponding JSON
|
274
|
-
serialization is performed using the `key_value do` or `json do` blocks from
|
275
|
-
`lutaml-model`. The mapping of the contents of `_links` is automatically
|
276
|
-
performed using `hal_link`.
|
277
|
-
|
278
|
-
[example]
|
279
|
-
====
|
280
|
-
A HAL resource of class `Product` with attributes `id`, `name`, and `price` will
|
281
|
-
need to declare a `key_value` block to map the attributes to their corresponding
|
282
|
-
JSON keys, namely, `"id"`, `"name"`, and `"price"`.
|
283
|
-
====
|
284
|
-
|
285
|
-
Please refer to syntax as described in the
|
286
|
-
https://github.com/lutaml/lutaml-model[`lutaml-model`] documentation.
|
287
|
-
|
288
|
-
.Example of a resource model with serialization mapping
|
289
|
-
[example]
|
290
|
-
====
|
291
|
-
[source,ruby]
|
292
|
-
----
|
293
|
-
module MyApi
|
294
|
-
class Product < Lutaml::Hal::Resource
|
295
|
-
attribute :id, :string
|
296
|
-
attribute :name, :string
|
297
|
-
attribute :price, :float
|
298
|
-
|
299
|
-
key_value do
|
300
|
-
map 'id', to: :id
|
301
|
-
map 'name', to: :name
|
302
|
-
map 'price', to: :price
|
303
|
-
end
|
304
|
-
end
|
305
|
-
end
|
306
|
-
----
|
307
|
-
====
|
308
|
-
|
309
|
-
==== HAL Links
|
310
|
-
|
311
|
-
A HAL resource has links to other resources, typically serialized in
|
312
|
-
the `_links` section of the JSON response.
|
313
|
-
|
314
|
-
[example]
|
315
|
-
====
|
316
|
-
A HAL resource of class `Product` can have links `self` (which is a
|
317
|
-
self-referential identifier link) and `category`.
|
318
|
-
====
|
319
|
-
|
320
|
-
HAL links need to be defined in the resource model to allow the resolution of
|
321
|
-
the links to their target resources.
|
322
|
-
|
323
|
-
These links are declared using the `hal_link` method provided by `lutaml-hal`.
|
324
|
-
|
325
|
-
Syntax:
|
66
|
+
# Define a HAL resource
|
67
|
+
class Product < Lutaml::Hal::Resource
|
68
|
+
attribute :id, :string
|
69
|
+
attribute :name, :string
|
70
|
+
attribute :price, :float
|
326
71
|
|
327
|
-
|
328
|
-
|
329
|
-
hal_link :link_name,
|
330
|
-
key: 'link_key',
|
331
|
-
realize_class: 'TargetResourceClass',
|
332
|
-
link_class: 'LinkClass',
|
333
|
-
link_set_class: 'LinkSetClass'
|
334
|
-
----
|
335
|
-
|
336
|
-
Where,
|
337
|
-
|
338
|
-
`:link_name`:: The name of the link, which will be used to access the link in
|
339
|
-
the resource object.
|
340
|
-
|
341
|
-
`key: 'link_key'`:: The key of the link in the JSON response. This is the name
|
342
|
-
of the link as it appears in the `_links` section of the HAL resource.
|
343
|
-
|
344
|
-
`realize_class: 'TargetResourceClass'`:: The class of the target resource that
|
345
|
-
the link points to. This is used to resolve the link to the associated resource.
|
346
|
-
+
|
347
|
-
The `realize_class` parameter supports two distinct use cases:
|
348
|
-
+
|
349
|
-
--
|
350
|
-
**String reference (recommended)**: Use string class names to delay resolution,
|
351
|
-
especially when classes may be dynamically loaded or not available at definition time:
|
352
|
-
|
353
|
-
[source,ruby]
|
354
|
-
----
|
355
|
-
hal_link :category, key: 'category', realize_class: 'Category'
|
356
|
-
hal_link :products, key: 'products', realize_class: 'ProductIndex'
|
357
|
-
----
|
358
|
-
|
359
|
-
**Class reference**: Use actual class objects when classes are statically available
|
360
|
-
at definition time or via autoload:
|
361
|
-
|
362
|
-
[source,ruby]
|
363
|
-
----
|
364
|
-
hal_link :category, key: 'category', realize_class: Category
|
365
|
-
hal_link :products, key: 'products', realize_class: ProductIndex
|
366
|
-
----
|
367
|
-
|
368
|
-
The framework's lazy resolution mechanism handles both cases seamlessly,
|
369
|
-
automatically resolving string references to actual classes when needed during
|
370
|
-
serialization. This ensures consistent type names in HAL output regardless of
|
371
|
-
class loading order.
|
372
|
-
--
|
373
|
-
|
374
|
-
`link_class: 'LinkClass'`:: (optional) The class of the link that defines
|
375
|
-
specific behavior or attributes for the link object itself. This is dynamically
|
376
|
-
created and is inherited from `Lutaml::Hal::Link` if not provided.
|
377
|
-
+
|
378
|
-
Like `realize_class`, this parameter supports both string and class references:
|
379
|
-
+
|
380
|
-
--
|
381
|
-
**String references (Recommended)**: Use string class names for maximum flexibility:
|
382
|
-
|
383
|
-
[source,ruby]
|
384
|
-
----
|
385
|
-
hal_link :category, key: 'category', realize_class: 'Category', link_class: 'CategoryLink'
|
386
|
-
----
|
387
|
-
|
388
|
-
**Class references**: Use actual class objects when classes are statically available:
|
389
|
-
|
390
|
-
[source,ruby]
|
391
|
-
----
|
392
|
-
hal_link :category, key: 'category', realize_class: Category, link_class: CategoryLink
|
393
|
-
----
|
394
|
-
--
|
395
|
-
|
396
|
-
`link_set_class: 'LinkSetClass'`:: (optional) The class of the link set object
|
397
|
-
that contains the links. This is dynamically created and is inherited from
|
398
|
-
`Lutaml::Hal::LinkSet` if not provided.
|
399
|
-
+
|
400
|
-
Like `realize_class`, this parameter supports both string and class references:
|
401
|
-
+
|
402
|
-
--
|
403
|
-
**String references (Recommended)**: Use string class names for maximum flexibility:
|
404
|
-
|
405
|
-
[source,ruby]
|
406
|
-
----
|
407
|
-
hal_link :category, key: 'category', realize_class: 'Category', link_set_class: 'ProductLinkSet'
|
408
|
-
----
|
409
|
-
|
410
|
-
**Class references**: Use actual class objects when classes are statically available:
|
411
|
-
|
412
|
-
[source,ruby]
|
413
|
-
----
|
414
|
-
hal_link :category, key: 'category', realize_class: Category, link_set_class: ProductLinkSet
|
415
|
-
----
|
416
|
-
--
|
417
|
-
|
418
|
-
|
419
|
-
.Integrated example of a HAL resource model using auto-generated LinkSet and Link classes
|
420
|
-
[example]
|
421
|
-
====
|
422
|
-
For an instance of `Product`:
|
423
|
-
|
424
|
-
[source,ruby]
|
425
|
-
----
|
426
|
-
module MyApi
|
427
|
-
class Product < Lutaml::Hal::Resource
|
428
|
-
attribute :id, :string
|
429
|
-
attribute :name, :string
|
430
|
-
attribute :price, :float
|
431
|
-
|
432
|
-
hal_link :self, key: 'self', realize_class: 'Product'
|
433
|
-
hal_link :category, key: 'category', realize_class: 'Category'
|
434
|
-
|
435
|
-
key_value do
|
436
|
-
map 'id', to: :id
|
437
|
-
map 'name', to: :name
|
438
|
-
map 'price', to: :price
|
439
|
-
end
|
440
|
-
end
|
441
|
-
end
|
442
|
-
----
|
443
|
-
|
444
|
-
The library will provide:
|
445
|
-
|
446
|
-
* the link set (serialized in HAL as JSON `_links`) in the class
|
447
|
-
`ProductLinkSet`.
|
72
|
+
hal_link :self, key: 'self', realize_class: 'Product'
|
73
|
+
hal_link :category, key: 'category', realize_class: 'Category'
|
448
74
|
|
449
|
-
|
450
|
-
|
451
|
-
|
452
|
-
|
453
|
-
|
454
|
-
* calling `product.links.self` will return an instance of `ProductLink`.
|
455
|
-
|
456
|
-
* calling `product.links.self.realize(register)` will dynamically fetch and
|
457
|
-
return an instance of `Product`.
|
458
|
-
====
|
459
|
-
|
460
|
-
|
461
|
-
|
462
|
-
===== Dynamic definition of LinkSet and Link
|
463
|
-
|
464
|
-
The `_links` section is modeled as a dynamically created link set class, named
|
465
|
-
after the resource's class name (with an appended `LinkSet` string), which in turn
|
466
|
-
contains the defined links to other resources. The link set class is automatically
|
467
|
-
inherited from `Lutaml::Hal::LinkSet`.
|
468
|
-
|
469
|
-
Each link in the link set is modeled as a dynamically created link class, named
|
470
|
-
after the resource's class name (with an appended `Link` string). This link class
|
471
|
-
is inherited from `Lutaml::Hal::Link`.
|
472
|
-
|
473
|
-
[example]
|
474
|
-
====
|
475
|
-
A HAL resource of class `Product` may have a link set of class `ProductLinkSet`
|
476
|
-
which contains the `self` and `category` links as its attributes.
|
477
|
-
====
|
478
|
-
|
479
|
-
The framework automatically:
|
480
|
-
|
481
|
-
* Creates the LinkSet class when the resource class is defined
|
482
|
-
* Adds a `links` attribute to the resource class
|
483
|
-
* Maps the `_links` JSON key to the `links` attribute
|
484
|
-
* Ensures consistent type naming regardless of class loading order
|
485
|
-
|
486
|
-
Each link object of the link set is provided as a `Link` object that is
|
487
|
-
dynamically created for the type of resolved resource. The name of the link
|
488
|
-
class is the same as the resource class name with an appended `Link` string.
|
489
|
-
This Link class is inherited from `Lutaml::Hal::Link`.
|
490
|
-
|
491
|
-
[example]
|
492
|
-
====
|
493
|
-
A HAL resource of class `Product` with a link set that contains the `self`
|
494
|
-
(points to a `Product`) and `category` (points to a `Category`) links will
|
495
|
-
have:
|
496
|
-
|
497
|
-
* a link set of class `ProductLinkSet` which contains:
|
498
|
-
** a `self` attribute that is an instance of `ProductLink`
|
499
|
-
** a `category` attribute that is an instance of `CategoryLink`
|
500
|
-
====
|
501
|
-
|
502
|
-
===== Lazy realization class loading and type naming
|
503
|
-
|
504
|
-
The framework implements lazy type resolution of the `realize_class` argument in
|
505
|
-
the `hal_link` command. This allows the instance to be realized on resolution to
|
506
|
-
have its class defined after the definition of the `hal_link` command, for
|
507
|
-
example, in the case when the class to be realized is loaded later in the
|
508
|
-
application lifecycle.
|
509
|
-
|
510
|
-
Technically, it is possible to have all models (the classes to be realized) to
|
511
|
-
be defined before the HAL resource is created to ensure the realization classes
|
512
|
-
are resolved. However, there are cases where classes are dynamically generated,
|
513
|
-
resolved via registers or other mechanisms that make those classes available
|
514
|
-
after the HAL resource is defined.
|
515
|
-
|
516
|
-
This allows for greater flexibility in defining resource relationships and
|
517
|
-
enables the use of dynamic class loading techniques.
|
518
|
-
|
519
|
-
In addition, the definition of the `realize_class` argument in the `hal_link`
|
520
|
-
command becomes useful in the case of polymorphism. The type name is used in
|
521
|
-
Lutaml::Model for polymorphism and potentially serialized (if defined through
|
522
|
-
Lutaml::Model serializatiion methods, as a Hal::Resource is also a
|
523
|
-
Lutaml::Model).
|
524
|
-
|
525
|
-
NOTE: This framework uses base class names (e.g., `ResourceClass`) instead of
|
526
|
-
fully qualified namespaced class names (e.g., `MyModule::ResourceClass`) as the
|
527
|
-
`type` attribute, by default.
|
528
|
-
|
529
|
-
|
530
|
-
|
531
|
-
===== Custom link set class
|
532
|
-
|
533
|
-
When a custom link set class (via `link_set_class:`) is provided, links are no
|
534
|
-
longer automatically added to the link set via `hal_link`. Please ensure that
|
535
|
-
all links are defined as model `attributes` and their `key_value` mappings
|
536
|
-
provided.
|
537
|
-
|
538
|
-
This is useful for the scenario where the link set needs to be
|
539
|
-
customized to provide additional attributes or behavior.
|
540
|
-
|
541
|
-
A LinkSetClass for a resource must implement the following interface:
|
542
|
-
|
543
|
-
[source,ruby]
|
544
|
-
----
|
545
|
-
module MyApi
|
546
|
-
# This represents the link set of a Resource
|
547
|
-
class ResourceLinkSet < Lutaml::Model::Serializable
|
548
|
-
attribute :attribute_name_1, :link_class_1, collection: {true|false}
|
549
|
-
attribute :attribute_name_2, :link_class_2, collection: {true|false}
|
550
|
-
# ...
|
551
|
-
|
552
|
-
key_value do
|
553
|
-
map 'link_key_1', to: :attribute_name_1
|
554
|
-
map 'link_key_2', to: :attribute_name_2
|
555
|
-
# ...
|
556
|
-
end
|
557
|
-
end
|
558
|
-
|
559
|
-
# This represents the basic setup of a Resource with a custom LinkSet class
|
560
|
-
class Resource < Lutaml::Hal::Resource
|
561
|
-
attribute :links, ResourceLinkSet
|
562
|
-
# Define resource attributes
|
563
|
-
|
564
|
-
key_value do
|
565
|
-
# This is the mapping of the `_links` key to the attribute `links`.
|
566
|
-
map '_links', to: :links
|
567
|
-
# Mappings for resource attributes need to be explicitly provided
|
568
|
-
end
|
569
|
-
end
|
570
|
-
end
|
571
|
-
----
|
572
|
-
|
573
|
-
Alternatively, it is possible to re-open the dynamically created link set class
|
574
|
-
and add additional attributes to it.
|
575
|
-
|
576
|
-
.Override the default link set class for Product
|
577
|
-
[source,ruby]
|
578
|
-
----
|
579
|
-
module MyApi
|
580
|
-
class Product < Lutaml::Hal::Resource
|
581
|
-
attribute :id, :string
|
582
|
-
end
|
583
|
-
# The class `MyApi::ProductLinkSet` is created automatically by the library.
|
584
|
-
|
585
|
-
# Re-open the default link set class and add additional attributes
|
586
|
-
class ProductLinkSet < Lutaml::Hal::LinkSet
|
587
|
-
# Add additional attributes to the link set
|
588
|
-
attribute :custom_link_set_attribute, Something, collection: false
|
589
|
-
|
590
|
-
key_value do
|
591
|
-
map 'my_custom_link', to: :custom_link_set_attribute
|
592
|
-
end
|
75
|
+
key_value do
|
76
|
+
map 'id', to: :id
|
77
|
+
map 'name', to: :name
|
78
|
+
map 'price', to: :price
|
593
79
|
end
|
594
80
|
end
|
595
|
-
----
|
596
|
-
|
597
|
-
===== Custom link class
|
598
|
-
|
599
|
-
When a custom link class (via `link_class:`) is provided, the custom link class
|
600
|
-
is automatically added into the link set.
|
601
|
-
|
602
|
-
This makes it possible to:
|
603
|
-
|
604
|
-
* supplement the link with additional attributes, or
|
605
|
-
* override the `realize(register)` method to provide custom behavior for the link.
|
606
|
-
|
607
|
-
A Link class pointing to a resource must implement the following interface:
|
608
81
|
|
609
|
-
|
610
|
-
|
611
|
-
|
612
|
-
# This represents a link set pointing to a Resource
|
613
|
-
class TargetResourceLink < Lutaml::Model::Serializable
|
614
|
-
# This is the link class for the resource class Resource
|
615
|
-
# 'default:' needs to be set to the name of the target resource class
|
616
|
-
attribute :type, :string, default: 'Resource'
|
617
|
-
|
618
|
-
# No specification of key_value block needed since attribute presence
|
619
|
-
# provides a default mapping.
|
620
|
-
end
|
621
|
-
end
|
622
|
-
----
|
623
|
-
|
624
|
-
Alternatively, it is possible to re-open the dynamically created link class and add
|
625
|
-
additional attributes to it.
|
626
|
-
|
627
|
-
.Override the default link class for Product
|
628
|
-
[source,ruby]
|
629
|
-
----
|
630
|
-
module MyApi
|
631
|
-
class Product < Lutaml::Hal::Resource
|
632
|
-
attribute :id, :string
|
633
|
-
hal_link :category, key: 'category', realize_class: 'Category'
|
634
|
-
end
|
635
|
-
# The class `MyApi::CategoryLink` is created automatically by the library.
|
636
|
-
|
637
|
-
# Re-open the default link class and add additional attributes
|
638
|
-
class CategoryLink < Lutaml::Hal::Link
|
639
|
-
# Add additional attributes to the link
|
640
|
-
attribute :language_code, :string, collection: false
|
641
|
-
|
642
|
-
key_value do
|
643
|
-
map 'language_code', to: :language_code
|
644
|
-
end
|
645
|
-
end
|
646
|
-
end
|
647
|
-
----
|
648
|
-
|
649
|
-
=== Registering resource models and endpoints
|
650
|
-
|
651
|
-
The `ModelRegister` allows you to register resource models and their endpoints.
|
652
|
-
|
653
|
-
You can define endpoints for collections (index) and individual resources
|
654
|
-
(resource) using the `add_endpoint` method.
|
655
|
-
|
656
|
-
The `add_endpoint` method takes the following parameters:
|
657
|
-
|
658
|
-
|
659
|
-
`id`:: A unique identifier for the endpoint.
|
660
|
-
|
661
|
-
`type`:: The type of endpoint, which can be `index` or `resource`.
|
662
|
-
|
663
|
-
`url`:: The URL of the endpoint, which can include path parameters.
|
664
|
-
+
|
665
|
-
In the `url`, you can use interpolation parameters, which will be replaced with
|
666
|
-
the actual values when fetching the resource. The interpolation parameters are
|
667
|
-
defined in the `url` string using curly braces `{}`.
|
668
|
-
|
669
|
-
`model`:: The class of the resource that will be fetched from the API.
|
670
|
-
The class must inherit from `Lutaml::Hal::Resource`.
|
671
|
-
|
672
|
-
`query_params`:: (optional) A hash defining query parameters that should be
|
673
|
-
appended to the URL when fetching the resource. Supports parameter templates
|
674
|
-
using curly braces `{}` for dynamic values.
|
675
|
-
+
|
676
|
-
This is essential for APIs that require query parameters for pagination,
|
677
|
-
filtering, or other functionality where the same base URL needs different query
|
678
|
-
parameters to access different resources or views.
|
679
|
-
|
680
|
-
The `add_endpoint` method will automatically handle the URL resolution and fetch
|
681
|
-
the resource from the API.
|
682
|
-
|
683
|
-
When the `ModelRegister` fetches a resource using the `realize` method, it will
|
684
|
-
match the resource URL against registered paths in order to find the
|
685
|
-
appropriate model class to use for deserialization and resolution.
|
686
|
-
|
687
|
-
Syntax:
|
688
|
-
|
689
|
-
[source,ruby]
|
690
|
-
----
|
691
|
-
register.add_endpoint( <1>
|
692
|
-
id: :model_index, <2>
|
693
|
-
type: :index, <3>
|
694
|
-
url: '/url_supporting_interpolation/{param}', <4>
|
695
|
-
model: ModelClass <5>
|
696
|
-
)
|
697
|
-
----
|
698
|
-
<1> The `add_endpoint` method is used to register an endpoint for a model.
|
699
|
-
<2> The `id` is a unique identifier for the endpoint, which is required to
|
700
|
-
fetch the resource later.
|
701
|
-
<3> The `type` specifies the type of endpoint, which can be `index` or
|
702
|
-
`resource`. The `index` type is used for collections, while the
|
703
|
-
`resource` type is used for individual resources.
|
704
|
-
<4> The `url` is the URL of the endpoint, which can include path
|
705
|
-
parameters. The URL can also include interpolation parameters, which
|
706
|
-
will be replaced with the actual values when fetching the resource.
|
707
|
-
<5> The `model` is the class of the resource that will be fetched from
|
708
|
-
the API. The class must inherit from `Lutaml::Hal::Resource`.
|
709
|
-
|
710
|
-
|
711
|
-
.Example of registering and using query parameters
|
712
|
-
[example]
|
713
|
-
====
|
714
|
-
[source,ruby]
|
715
|
-
----
|
716
|
-
# Register an endpoint that supports pagination via query parameters
|
717
|
-
register.add_endpoint(
|
718
|
-
id: :product_index,
|
719
|
-
type: :index,
|
720
|
-
url: '/products',
|
721
|
-
model: ProductIndex,
|
722
|
-
query_params: {
|
723
|
-
'page' => '{page}',
|
724
|
-
'items' => '{items}'
|
725
|
-
}
|
726
|
-
)
|
727
|
-
|
728
|
-
# Fetch the first page with 10 items per page
|
729
|
-
page_1 = register.fetch(:product_index, page: 1, items: 10)
|
730
|
-
# => client.get('/products?page=1&items=10')
|
731
|
-
|
732
|
-
# Fetch the second page with 5 items per page
|
733
|
-
page_2 = register.fetch(:product_index, page: 2, items: 5)
|
734
|
-
# => client.get('/products?page=2&items=5')
|
735
|
-
----
|
736
|
-
====
|
737
|
-
|
738
|
-
|
739
|
-
.Example of registering the Product class to both index and resource endpoints
|
740
|
-
[example]
|
741
|
-
====
|
742
|
-
[source,ruby]
|
743
|
-
----
|
744
|
-
register.add_endpoint(
|
745
|
-
id: :product_index,
|
746
|
-
type: :index,
|
747
|
-
url: '/products',
|
748
|
-
model: Product
|
749
|
-
)
|
82
|
+
# Set up API client and register
|
83
|
+
client = Lutaml::Hal::Client.new(api_url: 'https://api.example.com')
|
84
|
+
register = Lutaml::Hal::ModelRegister.new(name: :my_api, client: client)
|
750
85
|
|
86
|
+
# Register endpoints
|
751
87
|
register.add_endpoint(
|
752
88
|
id: :product_resource,
|
753
89
|
type: :resource,
|
754
90
|
url: '/products/{id}',
|
755
91
|
model: Product
|
756
92
|
)
|
757
|
-
----
|
758
|
-
====
|
759
93
|
|
94
|
+
# Fetch and use resources
|
95
|
+
product = register.fetch(:product_resource, id: '123')
|
96
|
+
puts product.name
|
97
|
+
puts product.price
|
760
98
|
|
761
|
-
|
762
|
-
|
763
|
-
|
764
|
-
[source,ruby]
|
99
|
+
# Navigate to related resources
|
100
|
+
category = product.links.category.realize(register)
|
101
|
+
puts category.name
|
765
102
|
----
|
766
|
-
# Register an endpoint that supports pagination via query parameters
|
767
|
-
register.add_endpoint(
|
768
|
-
id: :product_index_paginated,
|
769
|
-
type: :index,
|
770
|
-
url: '/products',
|
771
|
-
model: ProductIndex,
|
772
|
-
query_params: {
|
773
|
-
'page' => '{page}',
|
774
|
-
'items' => '{items}'
|
775
|
-
}
|
776
|
-
)
|
777
|
-
|
778
|
-
# Fetch the first page with 10 items per page
|
779
|
-
page_1 = register.fetch(:product_index_paginated, page: 1, items: 10)
|
780
|
-
# => client.get('/products?page=1&items=10')
|
781
|
-
|
782
|
-
# Fetch the second page with 5 items per page
|
783
|
-
page_2 = register.fetch(:product_index_paginated, page: 2, items: 5)
|
784
|
-
# => client.get('/products?page=2&items=5')
|
785
|
-
----
|
786
|
-
====
|
787
|
-
|
788
103
|
|
789
|
-
|
790
|
-
=== Defining HAL page models
|
104
|
+
== Documentation
|
791
105
|
|
792
|
-
|
793
|
-
limited number of resources at a time.
|
106
|
+
=== Comprehensive guides
|
794
107
|
|
795
|
-
|
796
|
-
subclass of `Resource`, and provides additional attributes and methods for
|
797
|
-
handling pagination information
|
108
|
+
For detailed documentation, see these comprehensive guides:
|
798
109
|
|
799
|
-
|
110
|
+
* **link:docs/getting-started-guide.adoc[Getting started guide]** - Step-by-step
|
111
|
+
tutorial for your first HAL API client (15 minutes)
|
800
112
|
|
801
|
-
|
802
|
-
|
803
|
-
`limit`:: The number of resources per page.
|
804
|
-
`total`:: The total number of resources.
|
113
|
+
* **link:docs/data-definition-guide.adoc[Data definition guide]** - Complete
|
114
|
+
reference for defining HAL resources, model registers, and API endpoints
|
805
115
|
|
806
|
-
|
807
|
-
|
116
|
+
* **link:docs/runtime-usage-guide.adoc[Runtime usage guide]** - Patterns for
|
117
|
+
fetching resources, navigating links, and handling pagination
|
808
118
|
|
809
|
-
|
119
|
+
* **link:docs/hal-links-reference.adoc[HAL links reference]** - Advanced
|
120
|
+
configuration and customization of HAL links
|
810
121
|
|
811
|
-
|
812
|
-
|
813
|
-
`next`:: A link to the next page.
|
814
|
-
`first`:: A link to the first page.
|
815
|
-
`last`:: A link to the last page.
|
122
|
+
* **link:docs/pagination-guide.adoc[Pagination guide]** - Working with
|
123
|
+
paginated APIs and large datasets
|
816
124
|
|
817
|
-
|
818
|
-
|
125
|
+
* **link:docs/complex-path-patterns.adoc[Complex path patterns]** - Advanced
|
126
|
+
URL patterns and path matching examples
|
819
127
|
|
820
|
-
|
128
|
+
* **link:docs/embedded-resources.adoc[Embedded resources]** - Implementing and
|
129
|
+
using embedded resources in HAL APIs
|
821
130
|
|
822
|
-
[source,ruby]
|
823
|
-
----
|
824
|
-
class ProductIndex < Lutaml::Hal::Page
|
825
|
-
# No attributes necessary
|
826
|
-
end
|
827
131
|
|
828
|
-
|
829
|
-
id: :product_index,
|
830
|
-
type: :index,
|
831
|
-
url: '/products',
|
832
|
-
model: ProductIndex
|
833
|
-
)
|
132
|
+
=== Architecture overview
|
834
133
|
|
835
|
-
|
836
|
-
page_2_link = page_1.links.next
|
837
|
-
# => <#ProductIndexLink href: "/products/2", title: "Next Page">
|
838
|
-
----
|
839
|
-
|
840
|
-
Where,
|
841
|
-
|
842
|
-
`ProductIndex`:: The class of the page that will be fetched from the API. The class
|
843
|
-
must inherit from `Lutaml::Hal::Page`.
|
844
|
-
`register`:: The instance of `ModelRegister`.
|
845
|
-
`id`:: The ID of the pagination endpoint to be registered in the `ModelRegister`.
|
846
|
-
`url`:: The URL of the pagination endpoint.
|
847
|
-
`model`:: The class of the page that will be fetched from the API.
|
848
|
-
|
849
|
-
|
850
|
-
== Usage: Runtime
|
851
|
-
|
852
|
-
=== General
|
853
|
-
|
854
|
-
NOTE: The `lutaml-hal` library currently only supports synchronous data fetching.
|
855
|
-
Asynchronous data fetching will be supported in the future.
|
856
|
-
|
857
|
-
NOTE: The `lutaml-hal` library currently only supports data fetching requests
|
858
|
-
(GET) today. Additional features may be provided in the future.
|
859
|
-
|
860
|
-
Once the data definition is complete, you can use the `ModelRegister` to
|
861
|
-
fetch and interact with resources from the API.
|
862
|
-
|
863
|
-
=== Fetching a resource
|
864
|
-
|
865
|
-
The `ModelRegister` allows you to fetch resources from the API using the `fetch`
|
866
|
-
method.
|
867
|
-
|
868
|
-
NOTE: The endpoint of the resource must be already defined through the
|
869
|
-
`add_endpoint` method.
|
870
|
-
|
871
|
-
The `fetch` method will automatically handle the URL resolution and fetch the
|
872
|
-
resource from the API.
|
873
|
-
|
874
|
-
Syntax:
|
875
|
-
|
876
|
-
[source,ruby]
|
877
|
-
----
|
878
|
-
register.fetch(:resource_endpoint_id, {parameters})
|
879
|
-
----
|
880
|
-
|
881
|
-
Where,
|
882
|
-
|
883
|
-
`resource_endpoint_id`:: The ID of the endpoint registered in the
|
884
|
-
`ModelRegister`.
|
885
|
-
`parameters`:: A hash of parameters to be passed to the API. The parameters
|
886
|
-
are used to replace the interpolation parameters in the URL.
|
887
|
-
`register`:: The instance of `ModelRegister`.
|
888
|
-
|
889
|
-
|
890
|
-
.Fetch a resource directly from the API
|
891
|
-
[example]
|
892
|
-
====
|
893
|
-
[source,ruby]
|
894
|
-
----
|
895
|
-
product_1 = register.fetch(:product_resource, id: 1)
|
896
|
-
# => client.get('/products/1')
|
134
|
+
The library is organized into these main components:
|
897
135
|
|
898
|
-
|
899
|
-
|
900
|
-
|
901
|
-
# "price": 10.0,
|
902
|
-
# "_links": {
|
903
|
-
# "self": { "href": "/products/1" },
|
904
|
-
# "category": { "href": "/categories/1", "title": "Category 1" },
|
905
|
-
# "related": [
|
906
|
-
# { "href": "/products/3", "title": "Product 3" },
|
907
|
-
# { "href": "/products/5", "title": "Product 5" }
|
908
|
-
# ]
|
909
|
-
# }
|
910
|
-
# }
|
911
|
-
|
912
|
-
product_1
|
913
|
-
# => #<Product id: 1, name: "Product 1", price: 10.0, links:
|
914
|
-
# #<ProductLinkSet self: <ProductLink href: "/products/1">,
|
915
|
-
# category: <ProductLink href: "/categories/1", title: "Category 1">,
|
916
|
-
# related: [
|
917
|
-
# <ProductLink href: "/products/3", title: "Product 3">,
|
918
|
-
# <ProductLink href: "/products/5", title: "Product 5">
|
919
|
-
# ]}>
|
920
|
-
----
|
921
|
-
====
|
922
|
-
|
923
|
-
=== Fetching a resource index
|
924
|
-
|
925
|
-
In HAL, collections are provided via the `_links` or the `_embedded` sections of
|
926
|
-
the response.
|
927
|
-
|
928
|
-
NOTE: The `_embedded` section is not yet supported by the `Lutaml::Hal` library.
|
929
|
-
|
930
|
-
The `ModelRegister` allows you to define endpoints for collections and fetch
|
931
|
-
them using the `fetch` method.
|
932
|
-
|
933
|
-
The `fetch` method will automatically handle the URL resolution and fetch the
|
934
|
-
resource index from the API.
|
935
|
-
|
936
|
-
Syntax:
|
937
|
-
|
938
|
-
[source,ruby]
|
939
|
-
----
|
940
|
-
register.fetch(:index_endpoint_id)
|
941
|
-
----
|
942
|
-
|
943
|
-
Where,
|
944
|
-
|
945
|
-
`index_endpoint_id`:: The ID of the endpoint registered in the `ModelRegister`.
|
946
|
-
`register`:: The instance of `ModelRegister`.
|
947
|
-
|
948
|
-
|
949
|
-
.Fetch a collection of resources from the API
|
950
|
-
[example]
|
951
|
-
====
|
952
|
-
[source,ruby]
|
953
|
-
----
|
954
|
-
product_index = register.fetch(:product_index)
|
955
|
-
# => client.get('/products')
|
956
|
-
|
957
|
-
# => {
|
958
|
-
# "page": 1,
|
959
|
-
# "pages": 10,
|
960
|
-
# "limit": 10,
|
961
|
-
# "total": 45,
|
962
|
-
# "_links": {
|
963
|
-
# "self": { "href": "/products/1" },
|
964
|
-
# "next": { "href": "/products/2" },
|
965
|
-
# "last": { "href": "/products/5" },
|
966
|
-
# "products": [
|
967
|
-
# { "href": "/products/1", "title": "Product 1" },
|
968
|
-
# { "href": "/products/2", "title": "Product 2" }
|
969
|
-
# ]
|
970
|
-
# }
|
971
|
-
|
972
|
-
product_index
|
973
|
-
# => #<ProductPage page: 1, pages: 10, limit: 10, total: 45,
|
974
|
-
# links: #<ProductLinkSet self: <ProductLink href: "/products/1">,
|
975
|
-
# next: <ProductLink href: "/products/2">,
|
976
|
-
# last: <ProductLink href: "/products/5">,
|
977
|
-
# products: <ProductLinkSet
|
978
|
-
# <ProductLink href: "/products/1", title: "Product 1">,
|
979
|
-
# <ProductLink href: "/products/2", title: "Product 2">
|
980
|
-
# ]>>
|
981
|
-
----
|
982
|
-
====
|
983
|
-
|
984
|
-
|
985
|
-
[[fetching_resource_via_link_realization]]
|
986
|
-
=== Fetching a resource via link realization
|
987
|
-
|
988
|
-
Given a resource index that contains links to resources, the individual resource
|
989
|
-
links can be "realized" as actual model instances through the
|
990
|
-
`Link#realize(register:)` method which dynamically retrieves the resource.
|
991
|
-
|
992
|
-
Given a `Link` object, the `realize` method fetches the resource from the API
|
993
|
-
using the provided `register`.
|
994
|
-
|
995
|
-
There are two ways a resource gets realized from a `Link` object:
|
996
|
-
|
997
|
-
* If a `Lutaml::Hal::GlobalRegister` is used, and the `Link` object originated
|
998
|
-
from a fetch using a `ModelRegister` then the `realize` method has sufficient
|
999
|
-
information to automatically fetch the resource from the API using the same
|
1000
|
-
`register`.
|
1001
|
-
+
|
1002
|
-
NOTE: This relies on the `Hal::REGISTER_ID_ATTR_NAME` attribute to be set
|
1003
|
-
in the `ModelRegister` class. This attribute is used to identify the
|
1004
|
-
resource endpoint ID in the URL.
|
1005
|
-
|
1006
|
-
* If a `GlobalRegister` is not used, even if the Link object originated
|
1007
|
-
from a fetch using a `ModelRegister`, the `realize` method does not have sufficient
|
1008
|
-
information to fetch the resource from the API using the same
|
1009
|
-
`register`. In this case an explicit `register` must be provided to the
|
1010
|
-
`realize(register: ...)` method.
|
1011
|
-
|
1012
|
-
Syntax for standalone usage:
|
1013
|
-
|
1014
|
-
[source,ruby]
|
1015
|
-
----
|
1016
|
-
Lutaml::Model::Link.new(
|
1017
|
-
href: 'resource_endpoint_href',
|
1018
|
-
# ... other attributes
|
1019
|
-
).realize(register)
|
1020
|
-
----
|
1021
|
-
|
1022
|
-
Where,
|
1023
|
-
|
1024
|
-
`resource_endpoint_href`:: The href of the resource endpoint. This is the URL of the
|
1025
|
-
resource as it appears in the `_links` section of the HAL resource.
|
1026
|
-
`register`:: The instance of `ModelRegister`.
|
1027
|
-
|
1028
|
-
The `realize` method will automatically handle the URL resolution and fetch
|
1029
|
-
the resource from the API, and return an instance of the resource class
|
1030
|
-
defined in the `ModelRegister` (through the endpoint definition of `realize_class`).
|
1031
|
-
|
1032
|
-
NOTE: It is possible to use the `realize` method on a link object using another
|
1033
|
-
`ModelRegister` instance. This is useful when you want to resolve a link
|
1034
|
-
using a different API endpoint or a different set of resource models.
|
1035
|
-
|
1036
|
-
Syntax when using a `GlobalRegister`:
|
1037
|
-
|
1038
|
-
[source,ruby]
|
1039
|
-
----
|
1040
|
-
resource_index = model_register.fetch(:resource_index)
|
1041
|
-
resource_index.links.products.first.realize
|
1042
|
-
# => client.get('/resources/1')
|
1043
|
-
----
|
1044
|
-
|
1045
|
-
.Dynamically realizing a resource from the collection using links
|
1046
|
-
[example]
|
1047
|
-
====
|
1048
|
-
[source,ruby]
|
1049
|
-
----
|
1050
|
-
# Without a GlobalRegister
|
1051
|
-
product_2 = product_index.links.products.last.realize(register)
|
1052
|
-
|
1053
|
-
# With a GlobalRegister
|
1054
|
-
product_2 = product_index.links.products.last.realize
|
1055
|
-
|
1056
|
-
# => client.get('/products/2')
|
1057
|
-
# => {
|
1058
|
-
# "id": 2,
|
1059
|
-
# "name": "Product 2",
|
1060
|
-
# "price": 20.0,
|
1061
|
-
# "_links": {
|
1062
|
-
# "self": { "href": "/products/2" },
|
1063
|
-
# "category": { "href": "/categories/2", "title": "Category 2" },
|
1064
|
-
# "related": [
|
1065
|
-
# { "href": "/products/4", "title": "Product 4" },
|
1066
|
-
# { "href": "/products/6", "title": "Product 6" }
|
1067
|
-
# ]
|
1068
|
-
# }
|
1069
|
-
# }
|
1070
|
-
|
1071
|
-
product_2
|
1072
|
-
# => #<Product id: 2, name: "Product 2", price: 20.0, links:
|
1073
|
-
# #<ProductLinkSet self: <ProductLink href: "/products/2">,
|
1074
|
-
# category: <ProductLink href: "/categories/2", title: "Category 2">,
|
1075
|
-
# related: [
|
1076
|
-
# <ProductLink href: "/products/4", title: "Product 4">,
|
1077
|
-
# <ProductLink href: "/products/6", title: "Product 6">
|
1078
|
-
# ]}>
|
1079
|
-
|
1080
|
-
# Without a GlobalRegister
|
1081
|
-
product_2_related_1 = product_2.links.related.first.realize(register)
|
1082
|
-
|
1083
|
-
# With a GlobalRegister
|
1084
|
-
product_2_related_1 = product_2.links.related.first.realize
|
1085
|
-
----
|
1086
|
-
====
|
1087
|
-
|
1088
|
-
=== Handling HAL pages / pagination
|
1089
|
-
|
1090
|
-
==== General
|
1091
|
-
|
1092
|
-
The `Lutaml::Hal::Page` class is used to handle pagination in HAL APIs.
|
1093
|
-
|
1094
|
-
As described in <<defining_hal_page_models>>, subclassing the `Page` class
|
1095
|
-
provides pagination capabilities, including the management of links to navigate
|
1096
|
-
through pages of resources.
|
1097
|
-
|
1098
|
-
==== Pagination navigation methods
|
1099
|
-
|
1100
|
-
The `Page` class provides several convenience methods for navigating through
|
1101
|
-
paginated results:
|
1102
|
-
|
1103
|
-
`#next_page`:: Returns the next page link if available, `nil` otherwise.
|
1104
|
-
|
1105
|
-
`#prev_page`:: Returns the previous page link if available, `nil` otherwise.
|
1106
|
-
|
1107
|
-
`#first_page`:: Returns the first page link if available, `nil` otherwise.
|
1108
|
-
|
1109
|
-
`#last_page`:: Returns the last page link if available, `nil` otherwise.
|
1110
|
-
|
1111
|
-
These methods return `Link` objects that can be realized using the `realize` method:
|
136
|
+
`Lutaml::Hal::Client`::
|
137
|
+
HTTP client for making requests to HAL APIs. Supports GET requests with
|
138
|
+
automatic response handling.
|
1112
139
|
|
1113
|
-
|
1114
|
-
|
1115
|
-
|
1116
|
-
if current_page.next_page
|
1117
|
-
next_page = current_page.next_page.realize
|
1118
|
-
end
|
140
|
+
`Lutaml::Hal::ModelRegister`::
|
141
|
+
Registry for managing HAL resource models and their API endpoints. Handles URL
|
142
|
+
resolution and resource fetching.
|
1119
143
|
|
1120
|
-
|
1121
|
-
|
1122
|
-
|
1123
|
-
end
|
144
|
+
`Lutaml::Hal::GlobalRegister`::
|
145
|
+
Optional singleton for managing multiple ModelRegisters and enabling automatic
|
146
|
+
link resolution.
|
1124
147
|
|
1125
|
-
|
1126
|
-
|
1127
|
-
|
1128
|
-
----
|
148
|
+
`Lutaml::Hal::Resource`::
|
149
|
+
Base class for defining HAL resource models with attributes, links, and
|
150
|
+
serialization mappings.
|
1129
151
|
|
1130
|
-
|
152
|
+
`Lutaml::Hal::Link`::
|
153
|
+
Represents HAL links with automatic realization capabilities for fetching
|
154
|
+
target resources.
|
1131
155
|
|
1132
|
-
|
1133
|
-
navigation
|
156
|
+
`Lutaml::Hal::Page`::
|
157
|
+
Specialized resource class for handling pagination with navigation methods and
|
158
|
+
helper functions.
|
1134
159
|
|
1135
|
-
|
1136
|
-
otherwise.
|
160
|
+
== Usage workflow
|
1137
161
|
|
1138
|
-
|
1139
|
-
otherwise.
|
162
|
+
The `lutaml-hal` workflow follows a two-phase approach:
|
1140
163
|
|
1141
|
-
|
1142
|
-
otherwise.
|
164
|
+
=== 1. Data definition phase
|
1143
165
|
|
1144
|
-
|
1145
|
-
|
166
|
+
. **Define resource models**: Create classes inheriting from
|
167
|
+
`Lutaml::Hal::Resource`
|
168
|
+
. **Set up client**: Create a `Client` instance pointing to your API
|
169
|
+
. **Create register**: Set up a `ModelRegister` to manage your models
|
170
|
+
. **Register endpoints**: Map your models to specific API URLs
|
1146
171
|
|
1147
|
-
|
1148
|
-
attribute).
|
172
|
+
=== 2. Runtime phase
|
1149
173
|
|
174
|
+
. **Fetch resources**: Use `register.fetch()` to get data from the API
|
175
|
+
. **Access attributes**: Work with resource data as normal Ruby objects
|
176
|
+
. **Navigate links**: Use HAL links to move between related resources
|
177
|
+
. **Realize links**: Convert links to actual resource instances
|
1150
178
|
|
1151
|
-
|
179
|
+
== Path matching specification
|
1152
180
|
|
1153
|
-
|
1154
|
-
|
181
|
+
The library supports sophisticated URL pattern matching for endpoint
|
182
|
+
registration. Patterns use curly braces `{}` for parameter interpolation:
|
1155
183
|
|
1156
184
|
[source,ruby]
|
1157
185
|
----
|
1158
|
-
|
186
|
+
# Simple patterns
|
187
|
+
'/products/{id}'
|
188
|
+
'/users/{user_id}/orders/{order_id}'
|
1159
189
|
|
1160
|
-
|
1161
|
-
|
1162
|
-
|
1163
|
-
|
1164
|
-
|
1165
|
-
|
1166
|
-
|
190
|
+
# With query parameters
|
191
|
+
register.add_endpoint(
|
192
|
+
id: :search_products,
|
193
|
+
type: :index,
|
194
|
+
url: '/products',
|
195
|
+
model: ProductIndex,
|
196
|
+
parameters: [
|
197
|
+
Lutaml::Hal::EndpointParameter.query('category',
|
198
|
+
schema: { type: :string },
|
199
|
+
description: 'Product category filter'
|
200
|
+
),
|
201
|
+
Lutaml::Hal::EndpointParameter.query('page',
|
202
|
+
schema: { type: :integer },
|
203
|
+
description: 'Page number'
|
204
|
+
),
|
205
|
+
Lutaml::Hal::EndpointParameter.query('limit',
|
206
|
+
schema: { type: :integer },
|
207
|
+
description: 'Results per page'
|
208
|
+
)
|
209
|
+
]
|
210
|
+
)
|
1167
211
|
----
|
1168
212
|
|
213
|
+
For complex path pattern examples, see
|
214
|
+
link:docs/complex-path-patterns.adoc[Complex Path Patterns].
|
1169
215
|
|
1170
|
-
|
216
|
+
== Error handling
|
1171
217
|
|
1172
|
-
|
1173
|
-
[example]
|
1174
|
-
====
|
1175
|
-
Declaration:
|
218
|
+
The library provides structured error handling:
|
1176
219
|
|
1177
220
|
[source,ruby]
|
1178
221
|
----
|
1179
|
-
|
1180
|
-
|
222
|
+
begin
|
223
|
+
product = register.fetch(:product_resource, id: '123')
|
224
|
+
rescue Lutaml::Hal::Errors::NotFoundError => e
|
225
|
+
puts "Product not found: #{e.message}"
|
226
|
+
rescue Lutaml::Hal::Errors::ApiError => e
|
227
|
+
puts "API Error: #{e.message}"
|
1181
228
|
end
|
1182
|
-
|
1183
|
-
register.add_endpoint(
|
1184
|
-
id: :resource_index,
|
1185
|
-
type: :index,
|
1186
|
-
url: '/resources',
|
1187
|
-
model: ResourceIndex
|
1188
|
-
)
|
1189
|
-
----
|
1190
|
-
|
1191
|
-
Usage:
|
1192
|
-
|
1193
|
-
[source,ruby]
|
1194
229
|
----
|
1195
|
-
page_1 = register.fetch(:resource_index)
|
1196
|
-
# => client.get('/resources')
|
1197
|
-
# => {
|
1198
|
-
# "page": 1,
|
1199
|
-
# "pages": 10,
|
1200
|
-
# "limit": 10,
|
1201
|
-
# "total": 100,
|
1202
|
-
# "_links": {
|
1203
|
-
# "self": {
|
1204
|
-
# "href": "https://api.example.com/resources?page=1&items=10"
|
1205
|
-
# },
|
1206
|
-
# "first": {
|
1207
|
-
# "href": "https://api.example.com/resources?page=1&items=10"
|
1208
|
-
# },
|
1209
|
-
# "last": {
|
1210
|
-
# "href": "https://api.example.com/resources?page=10&items=10"
|
1211
|
-
# },
|
1212
|
-
# "next": {
|
1213
|
-
# "href": "https://api.example.com/resources?page=2&items=10"
|
1214
|
-
# }
|
1215
|
-
# }
|
1216
|
-
# }
|
1217
|
-
|
1218
|
-
page_1
|
1219
|
-
# => #<ResourceIndex page: 1, pages: 10, limit: 10, total: 100,
|
1220
|
-
# links: #<ResourceIndexLinks
|
1221
|
-
# self: #<ResourceIndexLink href: "/resources?page=1&items=10">,
|
1222
|
-
# next: #<ResourceIndexLink href: "/resources?page=2&items=10">,
|
1223
|
-
# last: #<ResourceIndexLink href: "/resources?page=10&items=10">>>
|
1224
|
-
|
1225
|
-
# Check if navigation is available
|
1226
|
-
page_1.has_next? # => true
|
1227
|
-
page_1.has_prev? # => false
|
1228
|
-
page_1.total_pages # => 10
|
1229
|
-
|
1230
|
-
# Navigate using convenience methods
|
1231
|
-
page_2 = page_1.next
|
1232
|
-
# => client.get('/resources?page=2&items=10')
|
1233
|
-
# => #<ResourceIndex page: 2, pages: 10, limit: 10, total: 100, ...>
|
1234
230
|
|
1235
|
-
|
1236
|
-
page_2.has_next? # => true
|
1237
|
-
|
1238
|
-
# Navigate back to first page
|
1239
|
-
first_page = page_2.first
|
1240
|
-
# => client.get('/resources?page=1&items=10')
|
1241
|
-
|
1242
|
-
# Jump to last page
|
1243
|
-
last_page = page_2.last
|
1244
|
-
# => client.get('/resources?page=10&items=10')
|
1245
|
-
|
1246
|
-
# Alternative: using link realization (original method)
|
1247
|
-
# Without a GlobalRegister
|
1248
|
-
page_2 = page_1.links.next.realize(register)
|
1249
|
-
|
1250
|
-
# With a GlobalRegister
|
1251
|
-
page_2 = page_1.links.next.realize
|
1252
|
-
|
1253
|
-
# => client.get('/resources?page=2&items=10')
|
1254
|
-
# => #<ResourceIndex page: 2, pages: 10, limit: 10, total: 100,
|
1255
|
-
# links: #<ResourceIndexLinks
|
1256
|
-
# self: #<ResourceIndexLink href: "/resources?page=2&items=10">,
|
1257
|
-
# prev: #<ResourceIndexLink href: "/resources?page=1&items=10">,
|
1258
|
-
# next: #<ResourceIndexLink href: "/resources?page=3&items=10">,
|
1259
|
-
# first: #<ResourceIndexLink href: "/resources?page=1&items=10">,
|
1260
|
-
# last: #<ResourceIndexLink href: "/resources?page=10&items=10">>>,
|
1261
|
-
# prev: #<ResourceIndexLink href: "/resources?page=1&items=10">>>
|
1262
|
-
----
|
1263
|
-
====
|
231
|
+
== Contributing
|
1264
232
|
|
233
|
+
Bug reports and pull requests are welcome on GitHub at
|
234
|
+
https://github.com/lutaml/lutaml-hal.
|
1265
235
|
|
1266
236
|
== License and Copyright
|
1267
237
|
|