lutaml-hal 0.1.7 → 0.1.8

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