lutaml-hal 0.1.0 → 0.1.2
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 +723 -70
- data/lib/lutaml/hal/resource.rb +28 -18
- data/lib/lutaml/hal/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e5a918f01a4197929abfdbbdd1e061ecff5d37592ed0c880024ec11f6abe2943
|
4
|
+
data.tar.gz: 032f6ba78ced7adefc4b6a9ce9c2c76acc25eba6b9d3d96398f3f09d330d312b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: '0802c9c05fbb34fc5a8d53a023cdd1cfda2291305b597387a5cc7afc665c9aa7eac542f03840cac494389c49442a030e79e7fdcf2169d992747e7a707fc4d9fa'
|
7
|
+
data.tar.gz: 7ecabcdc778455d9d4fee7b9708c7754891ab64e3da5edb6b88927cc687806b3c5863dde63b89aa785cdcf8174c431b58fa862e0a6443e456faad13fcced0faf
|
data/README.adoc
CHANGED
@@ -86,27 +86,59 @@ defining pagination attributes, such as `page`, `pages`, `limit`, and
|
|
86
86
|
`total`, as well as methods for accessing linked resources within a page.
|
87
87
|
|
88
88
|
|
89
|
-
== Usage
|
89
|
+
== Usage overview
|
90
90
|
|
91
|
-
|
91
|
+
In order to interact with a HAL API using `lutaml-hal`, there are two
|
92
|
+
stages of usage: data definition and runtime.
|
92
93
|
|
93
|
-
|
94
|
+
At the data definition phase:
|
94
95
|
|
95
|
-
.
|
96
|
+
. Define the API endpoint using the `Client` class.
|
96
97
|
. Create a `ModelRegister` to manage the resource models and their
|
97
98
|
respective endpoints.
|
98
99
|
. Define the resource models using the `Resource` class.
|
99
|
-
. Register the models with the `ModelRegister
|
100
|
-
|
100
|
+
. Register the models with the `ModelRegister` and define their
|
101
|
+
relationships using the `add_endpoint` method.
|
102
|
+
|
103
|
+
Once data definition is present, the following operations can be performed at
|
104
|
+
runtime:
|
105
|
+
|
106
|
+
. Fetch resources from the API using the `ModelRegister` and `Link#realize` methods.
|
107
|
+
|
101
108
|
.. Once the resources are fetched, you can access their attributes and links
|
102
109
|
and navigate through the resource graph.
|
110
|
+
|
103
111
|
. Pagination, such as on "index" type pages, can be handled by subclassing the `Page` class.
|
104
|
-
|
112
|
+
+
|
113
|
+
NOTE: The `Page` class itself is also implemented as a `Resource`, so you can
|
105
114
|
use the same methods to access the page's attributes and links.
|
106
115
|
|
107
116
|
|
117
|
+
== Usage: Data definition
|
118
|
+
|
119
|
+
=== General
|
120
|
+
|
121
|
+
HAL resources need to be defined as models to allow data access and serialization.
|
122
|
+
|
123
|
+
The following steps are required:
|
124
|
+
|
125
|
+
. Define HAL resource models.
|
126
|
+
. Define the base API URL using the `Client` class.
|
127
|
+
. Create a `ModelRegister` to manage the resource models.
|
128
|
+
. Define the resource models' respective endpoints on the base API URL.
|
129
|
+
|
130
|
+
|
108
131
|
=== Creating a HAL model register
|
109
132
|
|
133
|
+
The `ModelRegister` class is used to manage the resource models and their
|
134
|
+
respective endpoints on the base API URL.
|
135
|
+
|
136
|
+
It relies on the `Client` class to perform HTTP requests to the API. The base
|
137
|
+
API URL is defined at the `Client` object.
|
138
|
+
|
139
|
+
NOTE: The base API URL is used for all requests made by the `Client` class,
|
140
|
+
including the requests made by the `ModelRegister` class.
|
141
|
+
|
110
142
|
[source,ruby]
|
111
143
|
----
|
112
144
|
require 'lutaml-hal'
|
@@ -115,53 +147,499 @@ require 'lutaml-hal'
|
|
115
147
|
client = Lutaml::Hal::Client.new(api_url: 'https://api.example.com')
|
116
148
|
register = Lutaml::Hal::ModelRegister.new(client: client)
|
117
149
|
# Or set client later, `register.client = client`
|
150
|
+
----
|
151
|
+
|
152
|
+
|
153
|
+
=== Defining HAL resource models
|
154
|
+
|
155
|
+
==== General
|
156
|
+
|
157
|
+
A HAL resource is defined by creating a subclass of the `Resource` class and
|
158
|
+
defining its attributes, links, and key-value mappings.
|
159
|
+
|
160
|
+
The `Resource` class is the base class for defining HAL resource models.
|
161
|
+
It inherits from `Lutaml::Model::Serialization`, which provides data
|
162
|
+
modelling and serialization capabilities.
|
163
|
+
|
164
|
+
The declaration of attributes, links, and key-value mappings for a HAL resource
|
165
|
+
is performed using the `attribute`, `hal_link`, and `key_value` methods.
|
166
|
+
|
167
|
+
There are 3 levels of data modeling in a HAL resource, all of which are necessary
|
168
|
+
for the full usage of a HAL resource:
|
169
|
+
|
170
|
+
* Resource attributes
|
171
|
+
* Serialization mappings
|
172
|
+
* HAL Links
|
173
|
+
|
174
|
+
|
175
|
+
.Integrated example of a resource model
|
176
|
+
[example]
|
177
|
+
====
|
178
|
+
[source,ruby]
|
179
|
+
----
|
180
|
+
module MyApi
|
181
|
+
class Product < Lutaml::Hal::Resource
|
182
|
+
attribute :id, :string
|
183
|
+
attribute :name, :string
|
184
|
+
attribute :price, :float
|
185
|
+
|
186
|
+
hal_link :self, key: 'self', realize_class: 'Product'
|
187
|
+
hal_link :category, key: 'category', realize_class: 'Category'
|
188
|
+
|
189
|
+
key_value do
|
190
|
+
map 'id', to: :id
|
191
|
+
map 'name', to: :name
|
192
|
+
map 'price', to: :price
|
193
|
+
end
|
194
|
+
end
|
195
|
+
end
|
196
|
+
----
|
197
|
+
====
|
198
|
+
|
199
|
+
|
200
|
+
==== Resource attributes
|
201
|
+
|
202
|
+
A resource attribute is a direct property of the HAL resource.
|
203
|
+
|
204
|
+
These attributes typically hold values of simple data types, and are directly
|
205
|
+
serialized into JSON.
|
206
|
+
|
207
|
+
These attributes are declared using the `attribute` method from `lutaml-model`.
|
208
|
+
|
209
|
+
[example]
|
210
|
+
====
|
211
|
+
A HAL resource of class `Product` can have attributes `id`, `name`, and `price`.
|
212
|
+
====
|
213
|
+
|
214
|
+
Please refer to syntax as described in the
|
215
|
+
https://github.com/lutaml/lutaml-model[`lutaml-model`] documentation.
|
216
|
+
|
217
|
+
.Example of a resource model with attributes
|
218
|
+
[example]
|
219
|
+
====
|
220
|
+
[source,ruby]
|
221
|
+
----
|
222
|
+
module MyApi
|
223
|
+
class Product < Lutaml::Hal::Resource
|
224
|
+
attribute :id, :string
|
225
|
+
attribute :name, :string
|
226
|
+
attribute :price, :float
|
227
|
+
# ...
|
228
|
+
end
|
229
|
+
end
|
230
|
+
----
|
231
|
+
====
|
232
|
+
|
233
|
+
==== Serialization mapping of resource attributes
|
234
|
+
|
235
|
+
A serialization mapping defines rules to serialize a HAL resource to and from a
|
236
|
+
serialization format. In HAL, the serialization format is JSON, but other formats
|
237
|
+
can also be supported.
|
238
|
+
|
239
|
+
The mapping between the HAL model attributes and their corresponding JSON
|
240
|
+
serialization is performed using the `key_value do` or `json do` blocks from
|
241
|
+
`lutaml-model`. The mapping of the contents of `_links` is automatically
|
242
|
+
performed using `hal_link`.
|
243
|
+
|
244
|
+
[example]
|
245
|
+
====
|
246
|
+
A HAL resource of class `Product` with attributes `id`, `name`, and `price` will
|
247
|
+
need to declare a `key_value` block to map the attributes to their corresponding
|
248
|
+
JSON keys, namely, `"id"`, `"name"`, and `"price"`.
|
249
|
+
====
|
250
|
+
|
251
|
+
Please refer to syntax as described in the
|
252
|
+
https://github.com/lutaml/lutaml-model[`lutaml-model`] documentation.
|
253
|
+
|
254
|
+
.Example of a resource model with serialization mapping
|
255
|
+
[example]
|
256
|
+
====
|
257
|
+
[source,ruby]
|
258
|
+
----
|
259
|
+
module MyApi
|
260
|
+
class Product < Lutaml::Hal::Resource
|
261
|
+
attribute :id, :string
|
262
|
+
attribute :name, :string
|
263
|
+
attribute :price, :float
|
264
|
+
|
265
|
+
key_value do
|
266
|
+
map 'id', to: :id
|
267
|
+
map 'name', to: :name
|
268
|
+
map 'price', to: :price
|
269
|
+
end
|
270
|
+
end
|
271
|
+
end
|
272
|
+
----
|
273
|
+
====
|
274
|
+
|
275
|
+
|
276
|
+
|
277
|
+
==== HAL Links
|
278
|
+
|
279
|
+
A HAL resource has links to other resources, typically serialized in
|
280
|
+
the `_links` section of the JSON response.
|
281
|
+
|
282
|
+
[example]
|
283
|
+
====
|
284
|
+
A HAL resource of class `Product` can have links `self` (which is a
|
285
|
+
self-referential identifier link) and `category`.
|
286
|
+
====
|
287
|
+
|
288
|
+
HAL links need to be defined in the resource model to allow the resolution of
|
289
|
+
the links to their target resources.
|
290
|
+
|
291
|
+
These links are declared using the `hal_link` method provided by `lutaml-hal`.
|
292
|
+
|
293
|
+
Syntax:
|
294
|
+
|
295
|
+
[source,ruby]
|
296
|
+
----
|
297
|
+
hal_link :link_name,
|
298
|
+
key: 'link_key',
|
299
|
+
realize_class: 'TargetResourceClass',
|
300
|
+
link_class: 'LinkClass',
|
301
|
+
link_set_class: 'LinkSetClass'
|
302
|
+
----
|
303
|
+
|
304
|
+
`:link_name`:: The name of the link, which will be used to access the link in
|
305
|
+
the resource object.
|
306
|
+
|
307
|
+
`key: 'link_key'`:: The key of the link in the JSON response. This is the name
|
308
|
+
of the link as it appears in the `_links` section of the HAL resource.
|
309
|
+
|
310
|
+
`realize_class: 'TargetResourceClass'`:: The class of the target resource that
|
311
|
+
the link points to. This is used to resolve the link to the associated resource.
|
312
|
+
|
313
|
+
`link_class: 'LinkClass'`:: (optional) The class of the link that defines
|
314
|
+
specific behavior or attributes for the link object itself. This is dynamically
|
315
|
+
created and is inherited from `Lutaml::Hal::Link` if not provided.
|
316
|
+
|
317
|
+
`link_set_class: 'LinkSetClass'`:: (optional) The class of the link set object
|
318
|
+
that contains the links. This is dynamically created and is inherited from
|
319
|
+
`Lutaml::Model::Serializable` if not provided.
|
320
|
+
|
321
|
+
|
322
|
+
The `_links` section is modeled as a dynamically created link set class, named
|
323
|
+
after the resource's class name (with an appended `LinkSet` string), which in turn
|
324
|
+
contains the defined links to other resources. The link set class is inherited
|
325
|
+
from `Lutaml::Model::Serializable`.
|
326
|
+
|
327
|
+
[example]
|
328
|
+
====
|
329
|
+
A HAL resource of class `Product` may have a link set of class `ProductLinkSet`
|
330
|
+
which contains the `self` and `category` links as its attributes.
|
331
|
+
====
|
332
|
+
|
333
|
+
|
334
|
+
Each link object of the link set is provided as a `Link` object that is
|
335
|
+
dynamically created for the type of resolved resource. The name of the link
|
336
|
+
class is the same as the resource class name with an appended `Link` string.
|
337
|
+
This Link class is inherited from `Lutaml::Hal::Link`.
|
338
|
+
|
339
|
+
[example]
|
340
|
+
====
|
341
|
+
A HAL resource of class `Product` with a link set that contains the `self`
|
342
|
+
(points to a `Product`) and `category` (points to a `Category`) links will
|
343
|
+
have:
|
344
|
+
|
345
|
+
* a link set of class `ProductLinks` which contains:
|
346
|
+
** a `self` attribute that is an instance of `ProductLink`
|
347
|
+
** a `category` attribute that is an instance of `CategoryLink`
|
348
|
+
====
|
349
|
+
|
350
|
+
|
351
|
+
.Integrated example of a HAL resource model using auto-generated LinkSet and Link classes
|
352
|
+
[example]
|
353
|
+
====
|
354
|
+
For an instance of `Product`:
|
355
|
+
|
356
|
+
[source,ruby]
|
357
|
+
----
|
358
|
+
module MyApi
|
359
|
+
class Product < Lutaml::Hal::Resource
|
360
|
+
attribute :id, :string
|
361
|
+
attribute :name, :string
|
362
|
+
attribute :price, :float
|
363
|
+
|
364
|
+
hal_link :self, key: 'self', realize_class: 'Product'
|
365
|
+
hal_link :category, key: 'category', realize_class: 'Category'
|
366
|
+
|
367
|
+
key_value do
|
368
|
+
map 'id', to: :id
|
369
|
+
map 'name', to: :name
|
370
|
+
map 'price', to: :price
|
371
|
+
end
|
372
|
+
end
|
373
|
+
end
|
374
|
+
----
|
375
|
+
|
376
|
+
The library will provide:
|
377
|
+
|
378
|
+
* the link set (serialized in HAL as JSON `_links`) in the class `ProductLinks`.
|
379
|
+
|
380
|
+
* the link set contains the `self` and the `category` links of class `Lutaml::Hal::Link`.
|
381
|
+
|
382
|
+
As a result:
|
383
|
+
|
384
|
+
* calling `product.links.self` will return an instance of `ProductLink`.
|
385
|
+
|
386
|
+
* calling `product.links.self.realize(register)` will dynamically fetch and
|
387
|
+
return an instance of `Product`.
|
388
|
+
====
|
389
|
+
|
390
|
+
|
391
|
+
|
392
|
+
==== Custom link set class
|
393
|
+
|
394
|
+
When a custom link set class (via `link_set_class:`) is provided, links are no
|
395
|
+
longer automatically added to the link set via `hal_link`. Please ensure that
|
396
|
+
all links are defined as model `attributes` and their `key_value` mappings
|
397
|
+
provided.
|
398
|
+
|
399
|
+
This is useful for the scenario where the link set needs to be
|
400
|
+
customized to provide additional attributes or behavior.
|
401
|
+
|
402
|
+
A LinkSetClass for a resource must implement the following interface:
|
403
|
+
|
404
|
+
[source,ruby]
|
405
|
+
----
|
406
|
+
module MyApi
|
407
|
+
# This represents the link set of a Resource
|
408
|
+
class ResourceLinkSet < Lutaml::Model::Serializable
|
409
|
+
attribute :attribute_name_1, :link_class_1, collection: {true|false}
|
410
|
+
attribute :attribute_name_2, :link_class_2, collection: {true|false}
|
411
|
+
# ...
|
412
|
+
|
413
|
+
key_value do
|
414
|
+
map 'link_key_1', to: :attribute_name_1
|
415
|
+
map 'link_key_2', to: :attribute_name_2
|
416
|
+
# ...
|
417
|
+
end
|
418
|
+
end
|
419
|
+
|
420
|
+
# This represents the basic setup of a Resource with a custom LinkSet class
|
421
|
+
class Resource < Lutaml::Hal::Resource
|
422
|
+
attribute :links, ResourceLinkSet
|
423
|
+
# Define resource attributes
|
424
|
+
|
425
|
+
key_value do
|
426
|
+
# This is the mapping of the `_links` key to the attribute `links`.
|
427
|
+
map '_links', to: :links
|
428
|
+
# Mappings for resource attributes need to be explicitly provided
|
429
|
+
end
|
430
|
+
end
|
431
|
+
end
|
432
|
+
----
|
433
|
+
|
434
|
+
Alternatively, it is possible to re-open the dynamically created link set class
|
435
|
+
and add additional attributes to it.
|
436
|
+
|
437
|
+
.Override the default link set class for Product
|
438
|
+
[source,ruby]
|
439
|
+
----
|
440
|
+
module MyApi
|
441
|
+
class Product < Lutaml::Hal::Resource
|
442
|
+
attribute :id, :string
|
443
|
+
end
|
444
|
+
# The class `MyApi::ProductLinkSet` is created automatically by the library.
|
445
|
+
|
446
|
+
# Re-open the default link set class and add additional attributes
|
447
|
+
class ProductLinkSet < Lutaml::Hal::LinkSet
|
448
|
+
# Add additional attributes to the link set
|
449
|
+
attribute :custom_link_set_attribute, Something, collection: false
|
450
|
+
|
451
|
+
key_value do
|
452
|
+
map 'my_custom_link', to: :custom_link_set_attribute
|
453
|
+
end
|
454
|
+
end
|
455
|
+
end
|
456
|
+
----
|
457
|
+
|
458
|
+
==== Custom link class
|
459
|
+
|
460
|
+
When a custom link class (via `link_class:`) is provided, the custom link class
|
461
|
+
is automatically added into the link set.
|
462
|
+
|
463
|
+
This makes it possible to:
|
464
|
+
|
465
|
+
* supplement the link with additional attributes, or
|
466
|
+
* override the `realize(register)` method to provide custom behavior for the link.
|
467
|
+
|
468
|
+
A Link class pointing to a resource must implement the following interface:
|
469
|
+
|
470
|
+
[source,ruby]
|
471
|
+
----
|
472
|
+
module MyApi
|
473
|
+
# This represents a link set pointing to a Resource
|
474
|
+
class TargetResourceLink < Lutaml::Model::Serializable
|
475
|
+
# This is the link class for the resource class Resource
|
476
|
+
# 'default:' needs to be set to the name of the target resource class
|
477
|
+
attribute :type, :string, default: 'Resource'
|
478
|
+
|
479
|
+
# No specification of key_value block needed since attribute presence
|
480
|
+
# provides a default mapping.
|
481
|
+
end
|
482
|
+
end
|
483
|
+
----
|
484
|
+
|
485
|
+
Alternatively, it is possible to re-open the dynamically created link class and add
|
486
|
+
additional attributes to it.
|
487
|
+
|
488
|
+
.Override the default link class for Product
|
489
|
+
[source,ruby]
|
490
|
+
----
|
491
|
+
module MyApi
|
492
|
+
class Product < Lutaml::Hal::Resource
|
493
|
+
attribute :id, :string
|
494
|
+
hal_link :category, key: 'category', realize_class: 'Category'
|
495
|
+
end
|
496
|
+
# The class `MyApi::CategoryLink` is created automatically by the library.
|
497
|
+
|
498
|
+
# Re-open the default link class and add additional attributes
|
499
|
+
class CategoryLink < Lutaml::Hal::Link
|
500
|
+
# Add additional attributes to the link
|
501
|
+
attribute :language_code, :string, collection: false
|
502
|
+
|
503
|
+
key_value do
|
504
|
+
map 'language_code', to: :language_code
|
505
|
+
end
|
506
|
+
end
|
507
|
+
end
|
508
|
+
----
|
509
|
+
|
510
|
+
|
511
|
+
|
512
|
+
=== Registering resource models and endpoints
|
513
|
+
|
514
|
+
The `ModelRegister` allows you to register resource models and their endpoints.
|
515
|
+
|
516
|
+
You can define endpoints for collections (index) and individual resources
|
517
|
+
(resource) using the `add_endpoint` method.
|
518
|
+
|
519
|
+
The `add_endpoint` method takes the following parameters:
|
520
|
+
|
521
|
+
`id`:: A unique identifier for the endpoint.
|
522
|
+
`type`:: The type of endpoint, which can be `index` or `resource`.
|
523
|
+
`url`:: The URL of the endpoint, which can include path parameters.
|
524
|
+
`model`:: The class of the resource that will be fetched from the API.
|
525
|
+
The class must inherit from `Lutaml::Hal::Resource`.
|
526
|
+
|
527
|
+
In the `url`, you can use interpolation parameters, which will be replaced with
|
528
|
+
the actual values when fetching the resource. The interpolation parameters are
|
529
|
+
defined in the `url` string using curly braces `{}`.
|
530
|
+
|
531
|
+
The `add_endpoint` method will automatically handle the URL resolution and fetch
|
532
|
+
the resource from the API.
|
533
|
+
|
534
|
+
When the `ModelRegister` fetches a resource using the `realize` method, it will
|
535
|
+
match the resource URL against registered paths in order to find the
|
536
|
+
appropriate model class to use for deserialization and resolution.
|
118
537
|
|
538
|
+
Syntax:
|
539
|
+
|
540
|
+
[source,ruby]
|
541
|
+
----
|
542
|
+
register.add_endpoint( <1>
|
543
|
+
id: :model_index, <2>
|
544
|
+
type: :index, <3>
|
545
|
+
url: '/url_supporting_interpolation/{param}', <4>
|
546
|
+
model: ModelClass <5>
|
547
|
+
)
|
548
|
+
----
|
549
|
+
<1> The `add_endpoint` method is used to register an endpoint for a model.
|
550
|
+
<2> The `id` is a unique identifier for the endpoint, which is required to
|
551
|
+
fetch the resource later.
|
552
|
+
<3> The `type` specifies the type of endpoint, which can be `index` or
|
553
|
+
`resource`. The `index` type is used for collections, while the
|
554
|
+
`resource` type is used for individual resources.
|
555
|
+
<4> The `url` is the URL of the endpoint, which can include path
|
556
|
+
parameters. The URL can also include interpolation parameters, which
|
557
|
+
will be replaced with the actual values when fetching the resource.
|
558
|
+
<5> The `model` is the class of the resource that will be fetched from
|
559
|
+
the API. The class must inherit from `Lutaml::Hal::Resource`.
|
560
|
+
|
561
|
+
.Example of registering the Product class to both index and resource endpoints
|
562
|
+
[example]
|
563
|
+
====
|
564
|
+
[source,ruby]
|
565
|
+
----
|
119
566
|
register.add_endpoint(
|
120
567
|
id: :product_index,
|
121
568
|
type: :index,
|
122
569
|
url: '/products',
|
123
570
|
model: Product
|
124
571
|
)
|
572
|
+
|
125
573
|
register.add_endpoint(
|
126
574
|
id: :product_resource,
|
127
575
|
type: :resource,
|
128
576
|
url: '/products/{id}',
|
129
577
|
model: Product
|
130
578
|
)
|
579
|
+
----
|
580
|
+
====
|
131
581
|
|
132
|
-
register.fetch(:product_index)
|
133
|
-
# => client.get('/products')
|
134
582
|
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
583
|
+
== Usage: Runtime
|
584
|
+
|
585
|
+
=== General
|
586
|
+
|
587
|
+
NOTE: The `lutaml-hal` library currently only supports synchronous data fetching.
|
588
|
+
Asynchronous data fetching will be supported in the future.
|
589
|
+
|
590
|
+
NOTE: The `lutaml-hal` library currently only supports data fetching requests
|
591
|
+
(GET) today. Additional features may be provided in the future.
|
592
|
+
|
593
|
+
Once the data definition is complete, you can use the `ModelRegister` to
|
594
|
+
fetch and interact with resources from the API.
|
595
|
+
|
596
|
+
=== Fetching a resource
|
597
|
+
|
598
|
+
The `ModelRegister` allows you to fetch resources from the API using the `fetch`
|
599
|
+
method.
|
600
|
+
|
601
|
+
NOTE: The endpoint of the resource must be already defined through the
|
602
|
+
`add_endpoint` method.
|
603
|
+
|
604
|
+
The `fetch` method will automatically handle the URL resolution and fetch the
|
605
|
+
resource from the API.
|
606
|
+
|
607
|
+
Syntax:
|
149
608
|
|
609
|
+
[source,ruby]
|
610
|
+
----
|
611
|
+
register.fetch(:resource_endpoint_id, {parameters})
|
612
|
+
----
|
613
|
+
|
614
|
+
Where,
|
615
|
+
|
616
|
+
`resource_endpoint_id`:: The ID of the endpoint registered in the
|
617
|
+
`ModelRegister`.
|
618
|
+
`parameters`:: A hash of parameters to be passed to the API. The parameters
|
619
|
+
are used to replace the interpolation parameters in the URL.
|
620
|
+
`register`:: The instance of `ModelRegister`.
|
621
|
+
|
622
|
+
|
623
|
+
.Fetch a resource directly from the API
|
624
|
+
[example]
|
625
|
+
====
|
626
|
+
[source,ruby]
|
627
|
+
----
|
150
628
|
product_1 = register.fetch(:product_resource, id: 1)
|
151
629
|
# => client.get('/products/1')
|
152
630
|
|
153
631
|
# => {
|
154
|
-
#
|
155
|
-
#
|
156
|
-
#
|
157
|
-
#
|
158
|
-
#
|
159
|
-
#
|
160
|
-
#
|
161
|
-
#
|
162
|
-
#
|
163
|
-
#
|
164
|
-
#
|
632
|
+
# "id": 1,
|
633
|
+
# "name": "Product 1",
|
634
|
+
# "price": 10.0,
|
635
|
+
# "_links": {
|
636
|
+
# "self": { "href": "/products/1" },
|
637
|
+
# "category": { "href": "/categories/1", "title": "Category 1" },
|
638
|
+
# "related": [
|
639
|
+
# { "href": "/products/3", "title": "Product 3" },
|
640
|
+
# { "href": "/products/5", "title": "Product 5" }
|
641
|
+
# ]
|
642
|
+
# }
|
165
643
|
# }
|
166
644
|
|
167
645
|
product_1
|
@@ -173,67 +651,242 @@ product_1
|
|
173
651
|
# <ProductLink href: "/products/5", title: "Product 5">
|
174
652
|
# ]}>
|
175
653
|
----
|
654
|
+
====
|
655
|
+
|
656
|
+
|
657
|
+
|
658
|
+
=== Fetching a resource index
|
659
|
+
|
660
|
+
In HAL, collections are provided via the `_links` or the `_embedded` sections of
|
661
|
+
the response.
|
662
|
+
|
663
|
+
NOTE: The `_embedded` section is not yet supported by the `Lutaml::Hal` library.
|
664
|
+
|
665
|
+
The `ModelRegister` allows you to define endpoints for collections and fetch
|
666
|
+
them using the `fetch` method.
|
667
|
+
|
668
|
+
The `fetch` method will automatically handle the URL resolution and fetch the
|
669
|
+
resource index from the API.
|
670
|
+
|
671
|
+
// The `Page` class is used to handle pagination and resource
|
672
|
+
// resolution for collections.
|
176
673
|
|
177
|
-
|
674
|
+
Syntax:
|
178
675
|
|
179
676
|
[source,ruby]
|
180
677
|
----
|
181
|
-
|
182
|
-
|
183
|
-
attribute :id, :string
|
184
|
-
attribute :name, :string
|
185
|
-
attribute :price, :float
|
678
|
+
register.fetch(:index_endpoint_id)
|
679
|
+
----
|
186
680
|
|
187
|
-
|
188
|
-
hal_link :category, key: 'category', realize_class: 'Category'
|
681
|
+
Where,
|
189
682
|
|
190
|
-
|
191
|
-
|
192
|
-
map 'name', to: :name
|
193
|
-
map 'price', to: :price
|
194
|
-
end
|
195
|
-
end
|
683
|
+
`index_endpoint_id`:: The ID of the endpoint registered in the `ModelRegister`.
|
684
|
+
`register`:: The instance of `ModelRegister`.
|
196
685
|
|
197
|
-
|
198
|
-
|
199
|
-
|
686
|
+
|
687
|
+
.Fetch a collection of resources from the API
|
688
|
+
[example]
|
689
|
+
====
|
690
|
+
[source,ruby]
|
691
|
+
----
|
692
|
+
product_index = register.fetch(:product_index)
|
693
|
+
# => client.get('/products')
|
694
|
+
|
695
|
+
# => {
|
696
|
+
# "page": 1,
|
697
|
+
# "pages": 10,
|
698
|
+
# "limit": 10,
|
699
|
+
# "total": 45,
|
700
|
+
# "_links": {
|
701
|
+
# "self": { "href": "/products/1" },
|
702
|
+
# "next": { "href": "/products/2" },
|
703
|
+
# "last": { "href": "/products/5" },
|
704
|
+
# "products": [
|
705
|
+
# { "href": "/products/1", "title": "Product 1" },
|
706
|
+
# { "href": "/products/2", "title": "Product 2" }
|
707
|
+
# ]
|
708
|
+
# }
|
709
|
+
|
710
|
+
product_index
|
711
|
+
# => #<ProductPage page: 1, pages: 10, limit: 10, total: 45,
|
712
|
+
# links: #<ProductLinks self: <ProductLink href: "/products/1">,
|
713
|
+
# next: <ProductLink href: "/products/2">,
|
714
|
+
# last: <ProductLink href: "/products/5">,
|
715
|
+
# products: <ProductLinks
|
716
|
+
# <ProductLink href: "/products/1", title: "Product 1">,
|
717
|
+
# <ProductLink href: "/products/2", title: "Product 2">
|
718
|
+
# ]>>
|
200
719
|
----
|
720
|
+
====
|
721
|
+
|
201
722
|
|
202
|
-
===
|
723
|
+
=== Fetching a resource via link realization
|
203
724
|
|
204
|
-
|
725
|
+
Given a resource index that contains links to resources, the individual resource
|
726
|
+
links can be "realized" as actual model instances through the
|
727
|
+
`Link#realize(register)` method which dynamically retrieves the resource.
|
728
|
+
|
729
|
+
Given a `Link` object, the `realize` method fetches the resource from the API
|
730
|
+
using the provided `register`.
|
731
|
+
|
732
|
+
Syntax:
|
205
733
|
|
206
734
|
[source,ruby]
|
207
735
|
----
|
208
|
-
|
209
|
-
|
210
|
-
#
|
211
|
-
|
212
|
-
|
736
|
+
Lutaml::Model::Link.new(
|
737
|
+
href: 'resource_endpoint_href',
|
738
|
+
# ... other attributes
|
739
|
+
).realize(register)
|
740
|
+
----
|
213
741
|
|
214
|
-
|
215
|
-
|
742
|
+
Where,
|
743
|
+
|
744
|
+
`resource_endpoint_href`:: The href of the resource endpoint. This is the URL of the
|
745
|
+
resource as it appears in the `_links` section of the HAL resource.
|
746
|
+
`register`:: The instance of `ModelRegister`.
|
747
|
+
|
748
|
+
The `realize` method will automatically handle the URL resolution and fetch
|
749
|
+
the resource from the API, and return an instance of the resource class
|
750
|
+
defined in the `ModelRegister` (through the endpoint definition of `realize_class`).
|
751
|
+
|
752
|
+
NOTE: It is possible to use the `realize` method on a link object using another
|
753
|
+
`ModelRegister` instance. This is useful when you want to resolve a link
|
754
|
+
using a different API endpoint or a different set of resource models.
|
755
|
+
|
756
|
+
.Dynamically realizing a resource from the collection using links
|
757
|
+
[example]
|
758
|
+
====
|
759
|
+
[source,ruby]
|
216
760
|
----
|
761
|
+
product_2 = product_index.links.products.last.realize(register)
|
762
|
+
# => client.get('/products/2')
|
763
|
+
# => {
|
764
|
+
# "id": 2,
|
765
|
+
# "name": "Product 2",
|
766
|
+
# "price": 20.0,
|
767
|
+
# "_links": {
|
768
|
+
# "self": { "href": "/products/2" },
|
769
|
+
# "category": { "href": "/categories/2", "title": "Category 2" },
|
770
|
+
# "related": [
|
771
|
+
# { "href": "/products/4", "title": "Product 4" },
|
772
|
+
# { "href": "/products/6", "title": "Product 6" }
|
773
|
+
# ]
|
774
|
+
# }
|
775
|
+
# }
|
776
|
+
|
777
|
+
product_2
|
778
|
+
# => #<Product id: 2, name: "Product 2", price: 20.0, links:
|
779
|
+
# #<ProductLinks self: <ProductLink href: "/products/2">,
|
780
|
+
# category: <ProductLink href: "/categories/2", title: "Category 2">,
|
781
|
+
# related: [
|
782
|
+
# <ProductLink href: "/products/4", title: "Product 4">,
|
783
|
+
# <ProductLink href: "/products/6", title: "Product 6">
|
784
|
+
# ]}>
|
785
|
+
----
|
786
|
+
====
|
787
|
+
|
788
|
+
=== Pagination
|
789
|
+
|
790
|
+
HAL index APIs often support pagination, which allows clients to retrieve a
|
791
|
+
limited number of resources at a time.
|
792
|
+
|
793
|
+
The `Lutaml::Hal::Page` class is used to handle pagination in HAL APIs. The
|
794
|
+
`Page` class itself is implemented as a `Resource`, so you can use the same
|
795
|
+
methods to access the page's attributes and links.
|
796
|
+
|
797
|
+
The `Page` class by default supports the following attributes:
|
217
798
|
|
218
|
-
|
799
|
+
`page`:: The current page number.
|
800
|
+
`pages`:: The total number of pages.
|
801
|
+
`limit`:: The number of resources per page.
|
802
|
+
`total`:: The total number of resources.
|
803
|
+
|
804
|
+
Syntax:
|
219
805
|
|
220
806
|
[source,ruby]
|
221
807
|
----
|
222
|
-
class
|
223
|
-
#
|
808
|
+
class MyPage < Lutaml::Hal::Page
|
809
|
+
# These are typical links given for page objects
|
810
|
+
hal_link :self, key: 'self', realize_class: 'MyPage'
|
811
|
+
hal_link :prev, key: 'prev', realize_class: 'MyPage'
|
812
|
+
hal_link :next, key: 'next', realize_class: 'MyPage'
|
813
|
+
hal_link :first, key: 'first', realize_class: 'MyPage'
|
814
|
+
hal_link :last, key: 'last', realize_class: 'MyPage'
|
224
815
|
end
|
225
816
|
|
226
|
-
|
227
|
-
|
817
|
+
register.add_endpoint(
|
818
|
+
id: :my_pages,
|
819
|
+
type: :index,
|
820
|
+
url: '/my_pages',
|
821
|
+
model: MyPage
|
822
|
+
)
|
823
|
+
----
|
824
|
+
|
825
|
+
Where,
|
228
826
|
|
229
|
-
|
230
|
-
|
827
|
+
`MyPage`:: The class of the page that will be fetched from the API. The class
|
828
|
+
must inherit from `Lutaml::Hal::Page`.
|
829
|
+
`register`:: The instance of `ModelRegister`.
|
830
|
+
`id`:: The ID of the pagination endpoint to be registered in the `ModelRegister`.
|
831
|
+
`url`:: The URL of the pagination endpoint.
|
832
|
+
`model`:: The class of the page that will be fetched from the API.
|
231
833
|
|
232
|
-
|
233
|
-
|
234
|
-
|
834
|
+
|
835
|
+
.Usage example of the Page class
|
836
|
+
[example]
|
837
|
+
====
|
838
|
+
Declaration:
|
839
|
+
|
840
|
+
[source,ruby]
|
841
|
+
----
|
842
|
+
class MyPage < Lutaml::Hal::Page
|
843
|
+
hal_link :self, key: 'self', realize_class: 'MyPage'
|
844
|
+
hal_link :prev, key: 'prev', realize_class: 'MyPage'
|
845
|
+
hal_link :next, key: 'next', realize_class: 'MyPage'
|
846
|
+
hal_link :first, key: 'first', realize_class: 'MyPage'
|
847
|
+
hal_link :last, key: 'last', realize_class: 'MyPage'
|
235
848
|
end
|
849
|
+
|
850
|
+
register.add_endpoint(
|
851
|
+
id: :my_pages,
|
852
|
+
type: :index,
|
853
|
+
url: '/my_pages',
|
854
|
+
model: MyPage
|
855
|
+
)
|
856
|
+
----
|
857
|
+
|
858
|
+
Usage:
|
859
|
+
|
860
|
+
[source,ruby]
|
861
|
+
----
|
862
|
+
page_1 = register.fetch(:my_pages)
|
863
|
+
# => client.get('/my_pages')
|
864
|
+
# => {
|
865
|
+
# "page": 1,
|
866
|
+
# "pages": 10,
|
867
|
+
# "limit": 10,
|
868
|
+
# "total": 100,
|
869
|
+
# "_links": {
|
870
|
+
# "self": { "href": "/my_pages" },
|
871
|
+
# "next": { "href": "/my_pages/2" },
|
872
|
+
# "last": { "href": "/my_pages/9" }
|
873
|
+
# }
|
874
|
+
# }
|
875
|
+
page_1
|
876
|
+
# => #<MyPage page: 1, pages: 10, limit: 10, total: 100,
|
877
|
+
# links: #<MyPageLinks self: <MyPageLink href: "/my_pages">,
|
878
|
+
# next: <MyPageLink href: "/my_pages/2">,
|
879
|
+
# last: <MyPageLink href: "/my_pages/9">>>
|
880
|
+
page_2 = page.links.next.realize(register)
|
881
|
+
# => client.get('/my_pages/2')
|
882
|
+
# => #<MyPage page: 2, pages: 10, limit: 10, total: 100,
|
883
|
+
# links: #<MyPageLinks self: <MyPageLink href: "/my_pages/2">,
|
884
|
+
# prev: <MyPageLink href: "/my_pages/1">,
|
885
|
+
# next: <MyPageLink href: "/my_pages/3">,
|
886
|
+
# first: <MyPageLink href: "/my_pages/1">,
|
887
|
+
# last: <MyPageLink href: "/my_pages/9">>>
|
236
888
|
----
|
889
|
+
====
|
237
890
|
|
238
891
|
|
239
892
|
== License and Copyright
|
data/lib/lutaml/hal/resource.rb
CHANGED
@@ -14,7 +14,7 @@ module Lutaml
|
|
14
14
|
def inherited(subclass)
|
15
15
|
super
|
16
16
|
subclass.class_eval do
|
17
|
-
|
17
|
+
create_link_set_class
|
18
18
|
init_links_definition
|
19
19
|
end
|
20
20
|
end
|
@@ -25,21 +25,31 @@ module Lutaml
|
|
25
25
|
# The "collection" is a boolean indicating if the link
|
26
26
|
# is a collection of resources or a single resource
|
27
27
|
# The "type" is the type of the link (default is :link, can be :resource)
|
28
|
-
def hal_link(attr_key,
|
28
|
+
def hal_link(attr_key,
|
29
|
+
key:,
|
30
|
+
realize_class:,
|
31
|
+
link_class: nil,
|
32
|
+
link_set_class: nil,
|
33
|
+
collection: false,
|
34
|
+
type: :link)
|
29
35
|
# Use the provided "key" as the attribute name
|
30
36
|
attribute_name = attr_key.to_sym
|
31
37
|
|
32
38
|
# Create a dynamic Link subclass name based on "realize_class", the
|
33
|
-
# class to realize for a Link object
|
34
|
-
link_klass = create_link_class(realize_class)
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
39
|
+
# class to realize for a Link object, if `link_class:` is not provided.
|
40
|
+
link_klass = link_class || create_link_class(realize_class)
|
41
|
+
|
42
|
+
# Create a dynamic LinkSet class if `link_set_class:` is not provided.
|
43
|
+
unless link_set_class
|
44
|
+
link_set_klass = link_set_class || get_links_class
|
45
|
+
link_set_klass.class_eval do
|
46
|
+
# Declare the corresponding lutaml-model attribute
|
47
|
+
attribute attribute_name, link_klass, collection: collection
|
48
|
+
|
49
|
+
# Define the mapping for the attribute
|
50
|
+
key_value do
|
51
|
+
map attr_key, to: attribute_name
|
52
|
+
end
|
43
53
|
end
|
44
54
|
end
|
45
55
|
|
@@ -59,7 +69,7 @@ module Lutaml
|
|
59
69
|
# This method obtains the Links class that holds the Link classes
|
60
70
|
def get_links_class
|
61
71
|
parent_klass_name = name.split('::')[0..-2].join('::')
|
62
|
-
child_klass_name = "#{name.split('::').last}
|
72
|
+
child_klass_name = "#{name.split('::').last}LinkSet"
|
63
73
|
klass_name = "#{parent_klass_name}::#{child_klass_name}"
|
64
74
|
|
65
75
|
raise unless Object.const_defined?(klass_name)
|
@@ -71,22 +81,22 @@ module Lutaml
|
|
71
81
|
|
72
82
|
# The "links" class holds the `_links` object which contains
|
73
83
|
# the resource-linked Link classes
|
74
|
-
def
|
84
|
+
def create_link_set_class
|
75
85
|
parent_klass_name = name.split('::')[0..-2].join('::')
|
76
|
-
child_klass_name = "#{name.split('::').last}
|
86
|
+
child_klass_name = "#{name.split('::').last}LinkSet"
|
77
87
|
klass_name = "#{parent_klass_name}::#{child_klass_name}"
|
78
88
|
|
79
|
-
# Check if the
|
89
|
+
# Check if the LinkSet class is already defined, return if so
|
80
90
|
return Object.const_get(klass_name) if Object.const_defined?(klass_name)
|
81
91
|
|
82
|
-
# Define the
|
92
|
+
# Define the LinkSet class dynamically as a normal Lutaml::Model class
|
83
93
|
# since it is not a Resource
|
84
94
|
klass = Class.new(Lutaml::Model::Serializable)
|
85
95
|
Object.const_get(parent_klass_name).tap do |parent_klass|
|
86
96
|
parent_klass.const_set(child_klass_name, klass)
|
87
97
|
end
|
88
98
|
|
89
|
-
# Define the
|
99
|
+
# Define the LinkSet class with mapping inside the current class
|
90
100
|
class_eval do
|
91
101
|
attribute :links, klass
|
92
102
|
key_value do
|
data/lib/lutaml/hal/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: lutaml-hal
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Ribose Inc.
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2025-03-
|
11
|
+
date: 2025-03-12 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: faraday
|