lutaml-hal 0.1.0 → 0.1.3

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7ea54d1764e2083f8ba5e232022fa2bae95db1775db65e1be46101c3dc2db3a8
4
- data.tar.gz: 3d519340919d3f8d84b5c6a38bc73796fbf662cc40c4a01cc030b3cfad9b00ea
3
+ metadata.gz: da904351e361ae9657c850d5faf981c6b698583970ead3320f125fdf6078fcbb
4
+ data.tar.gz: 677a8d713e5c2499cd9250497ca2b6f2d9474e545a2d2b2989dbcd33802b5588
5
5
  SHA512:
6
- metadata.gz: 31543a34726a0d12557f07aae9d9f70fba8ea936abb0b3f8792d26741ba4f353fcd7b9fd08e2cbff44e91819e18efcb5b67a3c50714752221c58f6a39c7761f1
7
- data.tar.gz: 2422ddeeb2903e7adce861fce307594c11f5d147b185121e082ad49fe19156016a940ee9199d6591ab203942c4713e48c0ab3d375bf03d944df5faa914a1b4b1
6
+ metadata.gz: d1d55faaa7c1f7d45b07a3d475b6908de807f7bed3bfebdfa32ce0ada2b91a1794f948046e0e0c554216f5e18219c07ee81dfb03a2ee0f5fb19e3367821e40fb
7
+ data.tar.gz: '07085ed8d705dfe5ff1893e1c9d2ab7bc87fe3b22e483dc14edfb1b89b2e54942e21fa38f6c1ff7910030fb57c6ecf198110fa70ef8447a49c377de2935873d8'
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
- === General
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
- In order to interact with a HAL API, the following steps are required:
94
+ At the data definition phase:
94
95
 
95
- . Create a `Client` that points to the API endpoint.
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
- . Fetch resources from the API using the `ModelRegister`.
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
- The `Page` class itself is also implemented as a `Resource`, so you can
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,562 @@ 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:
118
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.
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
+ ====
581
+
582
+
583
+ [[defining_hal_page_models]]
584
+ === Defining HAL page models
585
+
586
+ HAL index APIs often support pagination, which allows clients to retrieve a
587
+ limited number of resources at a time.
588
+
589
+ The `Lutaml::Hal::Page` class is used to handle pagination in HAL APIs. It is a
590
+ subclass of `Resource`, and provides additional attributes and methods for
591
+ handling pagination information
592
+
593
+ The `Page` class by default supports the following attributes:
594
+
595
+ `page`:: The current page number.
596
+ `pages`:: The total number of pages.
597
+ `limit`:: The number of resources per page.
598
+ `total`:: The total number of resources.
599
+
600
+ The way to use the `Page` class is through inheritance from it, where the
601
+ class will automatically create the necessary links for typical page objects.
602
+
603
+ The typical links of a page object are:
604
+
605
+ `self`:: A link to the current page.
606
+ `prev`:: A link to the previous page.
607
+ `next`:: A link to the next page.
608
+ `first`:: A link to the first page.
609
+ `last`:: A link to the last page.
610
+
611
+ The "realize class" of these links are the same as the inherited page
612
+ object, ensuring consistency in the pagination model.
613
+
614
+ Syntax:
615
+
616
+ [source,ruby]
617
+ ----
618
+ class ProductIndex < Lutaml::Hal::Page
619
+ # No attributes necessary
620
+ end
621
+
622
+ register.add_endpoint(
623
+ id: :product_index,
624
+ type: :index,
625
+ url: '/products',
626
+ model: ProductIndex
627
+ )
131
628
 
132
- register.fetch(:product_index)
133
- # => client.get('/products')
629
+ page_1 = register.fetch(:product_index) # Updated to use the correct endpoint id
630
+ page_2_link = page_1.links.next
631
+ # => <#ProductIndexLink href: "/products/2", title: "Next Page">
632
+ ----
134
633
 
135
- # => {
136
- # "page": 1,
137
- # "pages": 10,
138
- # "limit": 10,
139
- # "total": 45,
140
- # "_links": {
141
- # "self": { "href": "/products/1" },
142
- # "next": { "href": "/products/2" },
143
- # "last": { "href": "/products/5" },
144
- # "products": [
145
- # { "id": 1, "name": "Product 1", "price": 10.0 },
146
- # { "id": 2, "name": "Product 2", "price": 15.0 }
147
- # ]
148
- # }
634
+ Where,
635
+
636
+ `ProductIndex`:: The class of the page that will be fetched from the API. The class
637
+ must inherit from `Lutaml::Hal::Page`.
638
+ `register`:: The instance of `ModelRegister`.
639
+ `id`:: The ID of the pagination endpoint to be registered in the `ModelRegister`.
640
+ `url`:: The URL of the pagination endpoint.
641
+ `model`:: The class of the page that will be fetched from the API.
642
+
643
+
644
+
645
+
646
+ == Usage: Runtime
647
+
648
+ === General
649
+
650
+ NOTE: The `lutaml-hal` library currently only supports synchronous data fetching.
651
+ Asynchronous data fetching will be supported in the future.
652
+
653
+ NOTE: The `lutaml-hal` library currently only supports data fetching requests
654
+ (GET) today. Additional features may be provided in the future.
149
655
 
656
+ Once the data definition is complete, you can use the `ModelRegister` to
657
+ fetch and interact with resources from the API.
658
+
659
+ === Fetching a resource
660
+
661
+ The `ModelRegister` allows you to fetch resources from the API using the `fetch`
662
+ method.
663
+
664
+ NOTE: The endpoint of the resource must be already defined through the
665
+ `add_endpoint` method.
666
+
667
+ The `fetch` method will automatically handle the URL resolution and fetch the
668
+ resource from the API.
669
+
670
+ Syntax:
671
+
672
+ [source,ruby]
673
+ ----
674
+ register.fetch(:resource_endpoint_id, {parameters})
675
+ ----
676
+
677
+ Where,
678
+
679
+ `resource_endpoint_id`:: The ID of the endpoint registered in the
680
+ `ModelRegister`.
681
+ `parameters`:: A hash of parameters to be passed to the API. The parameters
682
+ are used to replace the interpolation parameters in the URL.
683
+ `register`:: The instance of `ModelRegister`.
684
+
685
+
686
+ .Fetch a resource directly from the API
687
+ [example]
688
+ ====
689
+ [source,ruby]
690
+ ----
150
691
  product_1 = register.fetch(:product_resource, id: 1)
151
692
  # => client.get('/products/1')
152
693
 
153
694
  # => {
154
- # "id": 1,
155
- # "name": "Product 1",
156
- # "price": 10.0,
157
- # "_links": {
158
- # "self": { "href": "/products/1" },
159
- # "category": { "href": "/categories/1", "title": "Category 1" },
160
- # "related": [
161
- # { "href": "/products/3", "title": "Product 3" },
162
- # { "href": "/products/5", "title": "Product 5" }
163
- # ]
164
- # }
695
+ # "id": 1,
696
+ # "name": "Product 1",
697
+ # "price": 10.0,
698
+ # "_links": {
699
+ # "self": { "href": "/products/1" },
700
+ # "category": { "href": "/categories/1", "title": "Category 1" },
701
+ # "related": [
702
+ # { "href": "/products/3", "title": "Product 3" },
703
+ # { "href": "/products/5", "title": "Product 5" }
704
+ # ]
705
+ # }
165
706
  # }
166
707
 
167
708
  product_1
@@ -173,67 +714,211 @@ product_1
173
714
  # <ProductLink href: "/products/5", title: "Product 5">
174
715
  # ]}>
175
716
  ----
717
+ ====
718
+
719
+
720
+
721
+ === Fetching a resource index
176
722
 
177
- === Defining resource models
723
+ In HAL, collections are provided via the `_links` or the `_embedded` sections of
724
+ the response.
725
+
726
+ NOTE: The `_embedded` section is not yet supported by the `Lutaml::Hal` library.
727
+
728
+ The `ModelRegister` allows you to define endpoints for collections and fetch
729
+ them using the `fetch` method.
730
+
731
+ The `fetch` method will automatically handle the URL resolution and fetch the
732
+ resource index from the API.
733
+
734
+ Syntax:
178
735
 
179
736
  [source,ruby]
180
737
  ----
181
- module MyApi
182
- class Product < Lutaml::Hal::Resource
183
- attribute :id, :string
184
- attribute :name, :string
185
- attribute :price, :float
738
+ register.fetch(:index_endpoint_id)
739
+ ----
186
740
 
187
- hal_link :self, key: 'self', realize_class: 'Product'
188
- hal_link :category, key: 'category', realize_class: 'Category'
741
+ Where,
189
742
 
190
- key_value do
191
- map 'id', to: :id
192
- map 'name', to: :name
193
- map 'price', to: :price
194
- end
195
- end
743
+ `index_endpoint_id`:: The ID of the endpoint registered in the `ModelRegister`.
744
+ `register`:: The instance of `ModelRegister`.
196
745
 
197
- # Register the model with the registry
198
- Lutaml::Hal::ModelRegister.register(Product, '/products/{id}')
199
- end
746
+
747
+ .Fetch a collection of resources from the API
748
+ [example]
749
+ ====
750
+ [source,ruby]
751
+ ----
752
+ product_index = register.fetch(:product_index)
753
+ # => client.get('/products')
754
+
755
+ # => {
756
+ # "page": 1,
757
+ # "pages": 10,
758
+ # "limit": 10,
759
+ # "total": 45,
760
+ # "_links": {
761
+ # "self": { "href": "/products/1" },
762
+ # "next": { "href": "/products/2" },
763
+ # "last": { "href": "/products/5" },
764
+ # "products": [
765
+ # { "href": "/products/1", "title": "Product 1" },
766
+ # { "href": "/products/2", "title": "Product 2" }
767
+ # ]
768
+ # }
769
+
770
+ product_index
771
+ # => #<ProductPage page: 1, pages: 10, limit: 10, total: 45,
772
+ # links: #<ProductLinks self: <ProductLink href: "/products/1">,
773
+ # next: <ProductLink href: "/products/2">,
774
+ # last: <ProductLink href: "/products/5">,
775
+ # products: <ProductLinks
776
+ # <ProductLink href: "/products/1", title: "Product 1">,
777
+ # <ProductLink href: "/products/2", title: "Product 2">
778
+ # ]>>
779
+ ----
780
+ ====
781
+
782
+
783
+ === Fetching a resource via link realization
784
+
785
+ Given a resource index that contains links to resources, the individual resource
786
+ links can be "realized" as actual model instances through the
787
+ `Link#realize(register)` method which dynamically retrieves the resource.
788
+
789
+ Given a `Link` object, the `realize` method fetches the resource from the API
790
+ using the provided `register`.
791
+
792
+ Syntax:
793
+
794
+ [source,ruby]
795
+ ----
796
+ Lutaml::Model::Link.new(
797
+ href: 'resource_endpoint_href',
798
+ # ... other attributes
799
+ ).realize(register)
200
800
  ----
201
801
 
202
- === Registering endpoints
802
+ Where,
803
+
804
+ `resource_endpoint_href`:: The href of the resource endpoint. This is the URL of the
805
+ resource as it appears in the `_links` section of the HAL resource.
806
+ `register`:: The instance of `ModelRegister`.
203
807
 
204
- === Fetching Resources
808
+ The `realize` method will automatically handle the URL resolution and fetch
809
+ the resource from the API, and return an instance of the resource class
810
+ defined in the `ModelRegister` (through the endpoint definition of `realize_class`).
205
811
 
812
+ NOTE: It is possible to use the `realize` method on a link object using another
813
+ `ModelRegister` instance. This is useful when you want to resolve a link
814
+ using a different API endpoint or a different set of resource models.
815
+
816
+ .Dynamically realizing a resource from the collection using links
817
+ [example]
818
+ ====
206
819
  [source,ruby]
207
820
  ----
208
- # Assume that the client is already created and registered at
209
- # the ModelRegister
210
- # Get a resource
211
- product = client.get('products/123')
212
- product_resource = MyApi::Product.from_json(product.to_json)
821
+ product_2 = product_index.links.products.last.realize(register)
822
+ # => client.get('/products/2')
823
+ # => {
824
+ # "id": 2,
825
+ # "name": "Product 2",
826
+ # "price": 20.0,
827
+ # "_links": {
828
+ # "self": { "href": "/products/2" },
829
+ # "category": { "href": "/categories/2", "title": "Category 2" },
830
+ # "related": [
831
+ # { "href": "/products/4", "title": "Product 4" },
832
+ # { "href": "/products/6", "title": "Product 6" }
833
+ # ]
834
+ # }
835
+ # }
213
836
 
214
- # Follow a link
215
- category = product_resource.category.realize(register)
837
+ product_2
838
+ # => #<Product id: 2, name: "Product 2", price: 20.0, links:
839
+ # #<ProductLinks self: <ProductLink href: "/products/2">,
840
+ # category: <ProductLink href: "/categories/2", title: "Category 2">,
841
+ # related: [
842
+ # <ProductLink href: "/products/4", title: "Product 4">,
843
+ # <ProductLink href: "/products/6", title: "Product 6">
844
+ # ]}>
216
845
  ----
846
+ ====
847
+
848
+ === Handling HAL pages / pagination
849
+
850
+ The `Lutaml::Hal::Page` class is used to handle pagination in HAL APIs.
851
+
852
+ As described in <<defining_hal_page_models>>, subclassing the `Page` class
853
+ provides pagination capabilities, including the management of links to navigate
854
+ through pages of resources.
217
855
 
218
- === Working with Collections
856
+
857
+ .Usage example of the Page class
858
+ [example]
859
+ ====
860
+ Declaration:
219
861
 
220
862
  [source,ruby]
221
863
  ----
222
- class ProductPage < Lutaml::Hal::Page
223
- # Define the relationship between page and items
864
+ class ResourceIndex < Lutaml::Hal::Page
865
+ # No attribute definition necessary
224
866
  end
225
867
 
226
- response = client.get('/products')
227
- products = ProductPage.from_json(response.to_json)
868
+ register.add_endpoint(
869
+ id: :resource_index,
870
+ type: :index,
871
+ url: '/resources',
872
+ model: ResourceIndex
873
+ )
874
+ ----
228
875
 
229
- # Access pagination info
230
- puts "Page #{products.page} of #{products.pages}, total: #{products.total}"
876
+ Usage:
231
877
 
232
- # Access linked items
233
- products.links.products.each do |product|
234
- puts "#{product.name}: $#{product.price}"
235
- end
878
+ [source,ruby]
879
+ ----
880
+ page_1 = register.fetch(:resource_index)
881
+ # => client.get('/resources')
882
+ # => {
883
+ # "page": 1,
884
+ # "pages": 10,
885
+ # "limit": 10,
886
+ # "total": 100,
887
+ # "_links": {
888
+ # "self": {
889
+ # "href": "https://api.example.com/resources?page=1&items=10"
890
+ # },
891
+ # "first": {
892
+ # "href": "https://api.example.com/resources?page=1&items=10"
893
+ # },
894
+ # "last": {
895
+ # "href": "https://api.example.com/resources?page=10&items=10"
896
+ # },
897
+ # "next": {
898
+ # "href": "https://api.example.com/resources?page=2&items=10"
899
+ # }
900
+ # }
901
+ # }
902
+
903
+ page_1
904
+ # => #<ResourceIndex page: 1, pages: 10, limit: 10, total: 100,
905
+ # links: #<ResourceIndexLinks
906
+ # self: #<ResourceIndexLink href: "/resources?page=1&items=10">,
907
+ # next: #<ResourceIndexLink href: "/resources?page=2&items=10">,
908
+ # last: #<ResourceIndexLink href: "/resources?page=10&items=10">>>
909
+
910
+ page_2 = page.links.next.realize(register)
911
+ # => client.get('/resources?page=2&items=10')
912
+ # => #<ResourceIndex page: 2, pages: 10, limit: 10, total: 100,
913
+ # links: #<ResourceIndexLinks
914
+ # self: #<ResourceIndexLink href: "/resources?page=2&items=10">,
915
+ # prev: #<ResourceIndexLink href: "/resources?page=1&items=10">,
916
+ # next: #<ResourceIndexLink href: "/resources?page=3&items=10">,
917
+ # first: #<ResourceIndexLink href: "/resources?page=1&items=10">,
918
+ # last: #<ResourceIndexLink href: "/resources?page=10&items=10">>>,
919
+ # prev: #<ResourceIndexLink href: "/resources?page=1&items=10">>>
236
920
  ----
921
+ ====
237
922
 
238
923
 
239
924
  == License and Copyright
@@ -19,6 +19,19 @@ module Lutaml
19
19
  map 'pages', to: :pages
20
20
  map 'total', to: :total
21
21
  end
22
+
23
+ def self.inherited(subclass)
24
+ super
25
+
26
+ page_links_symbols = %i[self next prev first last]
27
+ subclass_name = subclass.name
28
+ subclass.class_eval do
29
+ # Define common page links
30
+ page_links_symbols.each do |link_symbol|
31
+ hal_link link_symbol, key: link_symbol.to_s, realize_class: subclass_name
32
+ end
33
+ end
34
+ end
22
35
  end
23
36
  end
24
37
  end
@@ -14,7 +14,7 @@ module Lutaml
14
14
  def inherited(subclass)
15
15
  super
16
16
  subclass.class_eval do
17
- create_links_class
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, key:, realize_class:, collection: false, type: :link)
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
- links_klass = get_links_class
36
- links_klass.class_eval do
37
- # Declare the corresponding lutaml-model attribute
38
- attribute attribute_name, link_klass, collection: collection
39
-
40
- # Define the mapping for the attribute
41
- key_value do
42
- map attr_key, to: attribute_name
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,8 +69,8 @@ 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}Links"
63
- klass_name = "#{parent_klass_name}::#{child_klass_name}"
72
+ child_klass_name = "#{name.split('::').last}LinkSet"
73
+ klass_name = [parent_klass_name, child_klass_name].join('::')
64
74
 
65
75
  raise unless Object.const_defined?(klass_name)
66
76
 
@@ -71,22 +81,21 @@ module Lutaml
71
81
 
72
82
  # The "links" class holds the `_links` object which contains
73
83
  # the resource-linked Link classes
74
- def create_links_class
84
+ def create_link_set_class
75
85
  parent_klass_name = name.split('::')[0..-2].join('::')
76
- child_klass_name = "#{name.split('::').last}Links"
77
- klass_name = "#{parent_klass_name}::#{child_klass_name}"
86
+ child_klass_name = "#{name.split('::').last}LinkSet"
87
+ klass_name = [parent_klass_name, child_klass_name].join('::')
78
88
 
79
- # Check if the Links class is already defined, return if so
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 links class dynamically as a normal Lutaml::Model class
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
- Object.const_get(parent_klass_name).tap do |parent_klass|
86
- parent_klass.const_set(child_klass_name, klass)
87
- end
95
+ parent_klass = !parent_klass_name.empty? ? Object.const_get(parent_klass_name) : Object
96
+ parent_klass.const_set(child_klass_name, klass)
88
97
 
89
- # Define the Links class with mapping inside the current class
98
+ # Define the LinkSet class with mapping inside the current class
90
99
  class_eval do
91
100
  attribute :links, klass
92
101
  key_value do
@@ -103,7 +112,7 @@ module Lutaml
103
112
  def create_link_class(realize_class_name)
104
113
  parent_klass_name = name.split('::')[0..-2].join('::')
105
114
  child_klass_name = "#{name.split('::').last}Link"
106
- klass_name = "#{parent_klass_name}::#{child_klass_name}"
115
+ klass_name = [parent_klass_name, child_klass_name].join('::')
107
116
 
108
117
  return Object.const_get(klass_name) if Object.const_defined?(klass_name)
109
118
 
@@ -112,9 +121,9 @@ module Lutaml
112
121
  # Define the link class with the specified key and class
113
122
  attribute :type, :string, default: realize_class_name
114
123
  end
115
- Object.const_get(parent_klass_name).tap do |parent_klass|
116
- parent_klass.const_set(child_klass_name, klass)
117
- end
124
+
125
+ parent_klass = !parent_klass_name.empty? ? Object.const_get(parent_klass_name) : Object
126
+ parent_klass.const_set(child_klass_name, klass)
118
127
 
119
128
  klass
120
129
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Lutaml
4
4
  module Hal
5
- VERSION = '0.1.0'
5
+ VERSION = '0.1.3'
6
6
  end
7
7
  end
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.0
4
+ version: 0.1.3
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-10 00:00:00.000000000 Z
11
+ date: 2025-03-12 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: faraday