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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7ea54d1764e2083f8ba5e232022fa2bae95db1775db65e1be46101c3dc2db3a8
4
- data.tar.gz: 3d519340919d3f8d84b5c6a38bc73796fbf662cc40c4a01cc030b3cfad9b00ea
3
+ metadata.gz: e5a918f01a4197929abfdbbdd1e061ecff5d37592ed0c880024ec11f6abe2943
4
+ data.tar.gz: 032f6ba78ced7adefc4b6a9ce9c2c76acc25eba6b9d3d96398f3f09d330d312b
5
5
  SHA512:
6
- metadata.gz: 31543a34726a0d12557f07aae9d9f70fba8ea936abb0b3f8792d26741ba4f353fcd7b9fd08e2cbff44e91819e18efcb5b67a3c50714752221c58f6a39c7761f1
7
- data.tar.gz: 2422ddeeb2903e7adce861fce307594c11f5d147b185121e082ad49fe19156016a940ee9199d6591ab203942c4713e48c0ab3d375bf03d944df5faa914a1b4b1
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
- === 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,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
- # "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
- # }
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
- # "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
- # }
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
- === Defining resource models
674
+ Syntax:
178
675
 
179
676
  [source,ruby]
180
677
  ----
181
- module MyApi
182
- class Product < Lutaml::Hal::Resource
183
- attribute :id, :string
184
- attribute :name, :string
185
- attribute :price, :float
678
+ register.fetch(:index_endpoint_id)
679
+ ----
186
680
 
187
- hal_link :self, key: 'self', realize_class: 'Product'
188
- hal_link :category, key: 'category', realize_class: 'Category'
681
+ Where,
189
682
 
190
- key_value do
191
- map 'id', to: :id
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
- # Register the model with the registry
198
- Lutaml::Hal::ModelRegister.register(Product, '/products/{id}')
199
- end
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
- === Registering endpoints
723
+ === Fetching a resource via link realization
203
724
 
204
- === Fetching Resources
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
- # 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)
736
+ Lutaml::Model::Link.new(
737
+ href: 'resource_endpoint_href',
738
+ # ... other attributes
739
+ ).realize(register)
740
+ ----
213
741
 
214
- # Follow a link
215
- category = product_resource.category.realize(register)
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
- === Working with Collections
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 ProductPage < Lutaml::Hal::Page
223
- # Define the relationship between page and items
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
- response = client.get('/products')
227
- products = ProductPage.from_json(response.to_json)
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
- # Access pagination info
230
- puts "Page #{products.page} of #{products.pages}, total: #{products.total}"
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
- # Access linked items
233
- products.links.products.each do |product|
234
- puts "#{product.name}: $#{product.price}"
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
@@ -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,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}Links"
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 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"
86
+ child_klass_name = "#{name.split('::').last}LinkSet"
77
87
  klass_name = "#{parent_klass_name}::#{child_klass_name}"
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
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 Links class with mapping inside the current class
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
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Lutaml
4
4
  module Hal
5
- VERSION = '0.1.0'
5
+ VERSION = '0.1.2'
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.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-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