lutaml-hal 0.1.5 → 0.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/Gemfile +4 -3
- data/README.adoc +312 -48
- data/lib/lutaml/hal/link_class_factory.rb +100 -0
- data/lib/lutaml/hal/link_set_class_factory.rb +92 -0
- data/lib/lutaml/hal/model_register.rb +98 -8
- data/lib/lutaml/hal/page.rb +141 -0
- data/lib/lutaml/hal/resource.rb +39 -55
- data/lib/lutaml/hal/type_resolver.rb +59 -0
- data/lib/lutaml/hal/version.rb +1 -1
- metadata +5 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 037e6d60811bd1a095b05e04c6b8edfe73cce7103332e52f38f25d5f66ee2733
|
4
|
+
data.tar.gz: cdbb38106ef138c2a48407f763e046bdd1956749bf5a55f238d563cc1384058b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 683dba3496d46acd61610ad606e59a015b3cf4d041fa8e6efd7a405584571dfd1b6338579e36d603a0c5e6609feabdcbbb3ee4257261ae4319b971b335457367
|
7
|
+
data.tar.gz: 47f54aa3d93c6dd34baa6994c5e7102e282f2589abbea7eee5aa95164f17739a67d47331dceb82fdbc220feb9e9bc47ff9fc274fb05fdd5a21bf68fc41c729fa
|
data/Gemfile
CHANGED
data/README.adoc
CHANGED
@@ -306,8 +306,6 @@ end
|
|
306
306
|
----
|
307
307
|
====
|
308
308
|
|
309
|
-
|
310
|
-
|
311
309
|
==== HAL Links
|
312
310
|
|
313
311
|
A HAL resource has links to other resources, typically serialized in
|
@@ -335,6 +333,8 @@ hal_link :link_name,
|
|
335
333
|
link_set_class: 'LinkSetClass'
|
336
334
|
----
|
337
335
|
|
336
|
+
Where,
|
337
|
+
|
338
338
|
`:link_name`:: The name of the link, which will be used to access the link in
|
339
339
|
the resource object.
|
340
340
|
|
@@ -343,43 +343,77 @@ of the link as it appears in the `_links` section of the HAL resource.
|
|
343
343
|
|
344
344
|
`realize_class: 'TargetResourceClass'`:: The class of the target resource that
|
345
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
|
+
--
|
346
373
|
|
347
374
|
`link_class: 'LinkClass'`:: (optional) The class of the link that defines
|
348
375
|
specific behavior or attributes for the link object itself. This is dynamically
|
349
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:
|
350
382
|
|
351
|
-
|
352
|
-
|
353
|
-
|
383
|
+
[source,ruby]
|
384
|
+
----
|
385
|
+
hal_link :category, key: 'category', realize_class: 'Category', link_class: 'CategoryLink'
|
386
|
+
----
|
354
387
|
|
388
|
+
**Class references**: Use actual class objects when classes are statically available:
|
355
389
|
|
356
|
-
|
357
|
-
|
358
|
-
|
359
|
-
|
390
|
+
[source,ruby]
|
391
|
+
----
|
392
|
+
hal_link :category, key: 'category', realize_class: Category, link_class: CategoryLink
|
393
|
+
----
|
394
|
+
--
|
360
395
|
|
361
|
-
|
362
|
-
|
363
|
-
|
364
|
-
|
365
|
-
|
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:
|
366
404
|
|
405
|
+
[source,ruby]
|
406
|
+
----
|
407
|
+
hal_link :category, key: 'category', realize_class: 'Category', link_set_class: 'ProductLinkSet'
|
408
|
+
----
|
367
409
|
|
368
|
-
|
369
|
-
dynamically created for the type of resolved resource. The name of the link
|
370
|
-
class is the same as the resource class name with an appended `Link` string.
|
371
|
-
This Link class is inherited from `Lutaml::Hal::Link`.
|
410
|
+
**Class references**: Use actual class objects when classes are statically available:
|
372
411
|
|
373
|
-
[
|
374
|
-
|
375
|
-
|
376
|
-
|
377
|
-
|
378
|
-
|
379
|
-
* a link set of class `ProductLinks` which contains:
|
380
|
-
** a `self` attribute that is an instance of `ProductLink`
|
381
|
-
** a `category` attribute that is an instance of `CategoryLink`
|
382
|
-
====
|
412
|
+
[source,ruby]
|
413
|
+
----
|
414
|
+
hal_link :category, key: 'category', realize_class: Category, link_set_class: ProductLinkSet
|
415
|
+
----
|
416
|
+
--
|
383
417
|
|
384
418
|
|
385
419
|
.Integrated example of a HAL resource model using auto-generated LinkSet and Link classes
|
@@ -409,9 +443,11 @@ end
|
|
409
443
|
|
410
444
|
The library will provide:
|
411
445
|
|
412
|
-
* the link set (serialized in HAL as JSON `_links`) in the class
|
446
|
+
* the link set (serialized in HAL as JSON `_links`) in the class
|
447
|
+
`ProductLinkSet`.
|
413
448
|
|
414
|
-
* the link set contains the `self`
|
449
|
+
* the link set contains the `self` link (as `ProductLink`) and the `category`
|
450
|
+
link (as `CategoryLink`).
|
415
451
|
|
416
452
|
As a result:
|
417
453
|
|
@@ -423,7 +459,76 @@ return an instance of `Product`.
|
|
423
459
|
|
424
460
|
|
425
461
|
|
426
|
-
|
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
|
427
532
|
|
428
533
|
When a custom link set class (via `link_set_class:`) is provided, links are no
|
429
534
|
longer automatically added to the link set via `hal_link`. Please ensure that
|
@@ -489,7 +594,7 @@ module MyApi
|
|
489
594
|
end
|
490
595
|
----
|
491
596
|
|
492
|
-
|
597
|
+
===== Custom link class
|
493
598
|
|
494
599
|
When a custom link class (via `link_class:`) is provided, the custom link class
|
495
600
|
is automatically added into the link set.
|
@@ -541,8 +646,6 @@ module MyApi
|
|
541
646
|
end
|
542
647
|
----
|
543
648
|
|
544
|
-
|
545
|
-
|
546
649
|
=== Registering resource models and endpoints
|
547
650
|
|
548
651
|
The `ModelRegister` allows you to register resource models and their endpoints.
|
@@ -552,16 +655,28 @@ You can define endpoints for collections (index) and individual resources
|
|
552
655
|
|
553
656
|
The `add_endpoint` method takes the following parameters:
|
554
657
|
|
658
|
+
|
555
659
|
`id`:: A unique identifier for the endpoint.
|
660
|
+
|
556
661
|
`type`:: The type of endpoint, which can be `index` or `resource`.
|
557
|
-
`url`:: The URL of the endpoint, which can include path parameters.
|
558
|
-
`model`:: The class of the resource that will be fetched from the API.
|
559
|
-
The class must inherit from `Lutaml::Hal::Resource`.
|
560
662
|
|
663
|
+
`url`:: The URL of the endpoint, which can include path parameters.
|
664
|
+
+
|
561
665
|
In the `url`, you can use interpolation parameters, which will be replaced with
|
562
666
|
the actual values when fetching the resource. The interpolation parameters are
|
563
667
|
defined in the `url` string using curly braces `{}`.
|
564
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
|
+
|
565
680
|
The `add_endpoint` method will automatically handle the URL resolution and fetch
|
566
681
|
the resource from the API.
|
567
682
|
|
@@ -592,6 +707,35 @@ register.add_endpoint( <1>
|
|
592
707
|
<5> The `model` is the class of the resource that will be fetched from
|
593
708
|
the API. The class must inherit from `Lutaml::Hal::Resource`.
|
594
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
|
+
|
595
739
|
.Example of registering the Product class to both index and resource endpoints
|
596
740
|
[example]
|
597
741
|
====
|
@@ -614,6 +758,34 @@ register.add_endpoint(
|
|
614
758
|
====
|
615
759
|
|
616
760
|
|
761
|
+
.Example of using query_params for pagination
|
762
|
+
[example]
|
763
|
+
====
|
764
|
+
[source,ruby]
|
765
|
+
----
|
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
|
+
|
617
789
|
[[defining_hal_page_models]]
|
618
790
|
=== Defining HAL page models
|
619
791
|
|
@@ -675,8 +847,6 @@ must inherit from `Lutaml::Hal::Page`.
|
|
675
847
|
`model`:: The class of the page that will be fetched from the API.
|
676
848
|
|
677
849
|
|
678
|
-
|
679
|
-
|
680
850
|
== Usage: Runtime
|
681
851
|
|
682
852
|
=== General
|
@@ -741,7 +911,7 @@ product_1 = register.fetch(:product_resource, id: 1)
|
|
741
911
|
|
742
912
|
product_1
|
743
913
|
# => #<Product id: 1, name: "Product 1", price: 10.0, links:
|
744
|
-
# #<
|
914
|
+
# #<ProductLinkSet self: <ProductLink href: "/products/1">,
|
745
915
|
# category: <ProductLink href: "/categories/1", title: "Category 1">,
|
746
916
|
# related: [
|
747
917
|
# <ProductLink href: "/products/3", title: "Product 3">,
|
@@ -750,8 +920,6 @@ product_1
|
|
750
920
|
----
|
751
921
|
====
|
752
922
|
|
753
|
-
|
754
|
-
|
755
923
|
=== Fetching a resource index
|
756
924
|
|
757
925
|
In HAL, collections are provided via the `_links` or the `_embedded` sections of
|
@@ -803,10 +971,10 @@ product_index = register.fetch(:product_index)
|
|
803
971
|
|
804
972
|
product_index
|
805
973
|
# => #<ProductPage page: 1, pages: 10, limit: 10, total: 45,
|
806
|
-
# links: #<
|
974
|
+
# links: #<ProductLinkSet self: <ProductLink href: "/products/1">,
|
807
975
|
# next: <ProductLink href: "/products/2">,
|
808
976
|
# last: <ProductLink href: "/products/5">,
|
809
|
-
# products: <
|
977
|
+
# products: <ProductLinkSet
|
810
978
|
# <ProductLink href: "/products/1", title: "Product 1">,
|
811
979
|
# <ProductLink href: "/products/2", title: "Product 2">
|
812
980
|
# ]>>
|
@@ -902,7 +1070,7 @@ product_2 = product_index.links.products.last.realize
|
|
902
1070
|
|
903
1071
|
product_2
|
904
1072
|
# => #<Product id: 2, name: "Product 2", price: 20.0, links:
|
905
|
-
# #<
|
1073
|
+
# #<ProductLinkSet self: <ProductLink href: "/products/2">,
|
906
1074
|
# category: <ProductLink href: "/categories/2", title: "Category 2">,
|
907
1075
|
# related: [
|
908
1076
|
# <ProductLink href: "/products/4", title: "Product 4">,
|
@@ -917,15 +1085,89 @@ product_2_related_1 = product_2.links.related.first.realize
|
|
917
1085
|
----
|
918
1086
|
====
|
919
1087
|
|
920
|
-
|
921
1088
|
=== Handling HAL pages / pagination
|
922
1089
|
|
1090
|
+
==== General
|
1091
|
+
|
923
1092
|
The `Lutaml::Hal::Page` class is used to handle pagination in HAL APIs.
|
924
1093
|
|
925
1094
|
As described in <<defining_hal_page_models>>, subclassing the `Page` class
|
926
1095
|
provides pagination capabilities, including the management of links to navigate
|
927
1096
|
through pages of resources.
|
928
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:
|
1112
|
+
|
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
|
1119
|
+
|
1120
|
+
# Navigate to previous page
|
1121
|
+
if current_page.prev_page
|
1122
|
+
prev_page = current_page.prev_page.realize
|
1123
|
+
end
|
1124
|
+
|
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
|
+
----
|
1129
|
+
|
1130
|
+
==== Pagination helper methods
|
1131
|
+
|
1132
|
+
The `Page` class also provides helper methods to check the availability of
|
1133
|
+
navigation links:
|
1134
|
+
|
1135
|
+
`#has_next?`:: Returns `true` if there is a next page available, `false`
|
1136
|
+
otherwise.
|
1137
|
+
|
1138
|
+
`#has_prev?`:: Returns `true` if there is a previous page available, `false`
|
1139
|
+
otherwise.
|
1140
|
+
|
1141
|
+
`#has_first?`:: Returns `true` if there is a first page link available, `false`
|
1142
|
+
otherwise.
|
1143
|
+
|
1144
|
+
`#has_last?`:: Returns `true` if there is a last page link available, `false`
|
1145
|
+
otherwise.
|
1146
|
+
|
1147
|
+
`#total_pages`:: Returns the total number of pages (alias for the `pages`
|
1148
|
+
attribute).
|
1149
|
+
|
1150
|
+
|
1151
|
+
==== Exhaustive pagination
|
1152
|
+
|
1153
|
+
For scenarios where you need to process all pages of results, you can combine
|
1154
|
+
the pagination methods:
|
1155
|
+
|
1156
|
+
[source,ruby]
|
1157
|
+
----
|
1158
|
+
current_page = register.fetch(:resource_index)
|
1159
|
+
|
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
|
1167
|
+
----
|
1168
|
+
|
1169
|
+
|
1170
|
+
==== Usage
|
929
1171
|
|
930
1172
|
.Usage example of the Page class
|
931
1173
|
[example]
|
@@ -980,11 +1222,33 @@ page_1
|
|
980
1222
|
# next: #<ResourceIndexLink href: "/resources?page=2&items=10">,
|
981
1223
|
# last: #<ResourceIndexLink href: "/resources?page=10&items=10">>>
|
982
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
|
+
|
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)
|
983
1247
|
# Without a GlobalRegister
|
984
|
-
page_2 =
|
1248
|
+
page_2 = page_1.links.next.realize(register)
|
985
1249
|
|
986
1250
|
# With a GlobalRegister
|
987
|
-
page_2 =
|
1251
|
+
page_2 = page_1.links.next.realize
|
988
1252
|
|
989
1253
|
# => client.get('/resources?page=2&items=10')
|
990
1254
|
# => #<ResourceIndex page: 2, pages: 10, limit: 10, total: 100,
|
@@ -0,0 +1,100 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'link'
|
4
|
+
require_relative 'type_resolver'
|
5
|
+
|
6
|
+
module Lutaml
|
7
|
+
module Hal
|
8
|
+
# Factory class responsible for creating dynamic Link classes
|
9
|
+
class LinkClassFactory
|
10
|
+
def self.create_for(resource_class, realize_class_name)
|
11
|
+
new(resource_class, realize_class_name).create
|
12
|
+
end
|
13
|
+
|
14
|
+
def initialize(resource_class, realize_class_name)
|
15
|
+
@resource_class = resource_class
|
16
|
+
@realize_class_name = realize_class_name
|
17
|
+
end
|
18
|
+
|
19
|
+
def create
|
20
|
+
return create_anonymous_link_class if anonymous_class?
|
21
|
+
|
22
|
+
class_names = build_class_names
|
23
|
+
return existing_class(class_names[:full_name]) if class_exists?(class_names[:full_name])
|
24
|
+
|
25
|
+
klass = create_named_link_class(class_names)
|
26
|
+
register_constant(klass, class_names)
|
27
|
+
klass
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def anonymous_class?
|
33
|
+
@resource_class.name.nil?
|
34
|
+
end
|
35
|
+
|
36
|
+
def create_anonymous_link_class
|
37
|
+
create_link_class_with_type_resolution
|
38
|
+
end
|
39
|
+
|
40
|
+
def create_named_link_class(class_names)
|
41
|
+
Hal.debug_log "Creating link class #{class_names[:full_name]} for #{@realize_class_name}"
|
42
|
+
create_link_class_with_type_resolution
|
43
|
+
end
|
44
|
+
|
45
|
+
def create_link_class_with_type_resolution
|
46
|
+
realize_class_name = @realize_class_name
|
47
|
+
|
48
|
+
Class.new(Link) do
|
49
|
+
include TypeResolver
|
50
|
+
setup_type_resolution(realize_class_name)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def build_class_names
|
55
|
+
parent_namespace = @resource_class.name.split('::')[0..-2].join('::')
|
56
|
+
child_class_name = "#{@realize_class_name.split('::').last}Link"
|
57
|
+
full_class_name = [parent_namespace, child_class_name].reject(&:empty?).join('::')
|
58
|
+
|
59
|
+
{
|
60
|
+
parent_namespace: parent_namespace,
|
61
|
+
child_name: child_class_name,
|
62
|
+
full_name: full_class_name
|
63
|
+
}
|
64
|
+
end
|
65
|
+
|
66
|
+
def class_exists?(class_name)
|
67
|
+
Object.const_defined?(class_name)
|
68
|
+
end
|
69
|
+
|
70
|
+
def existing_class(class_name)
|
71
|
+
Object.const_get(class_name)
|
72
|
+
end
|
73
|
+
|
74
|
+
def register_constant(klass, class_names)
|
75
|
+
parent_klass = resolve_parent_class(class_names[:parent_namespace])
|
76
|
+
|
77
|
+
# Avoid registering constants on Object in test scenarios
|
78
|
+
# This prevents conflicts with test mocks that expect specific const_set calls
|
79
|
+
return if parent_klass == Object && in_test_environment?
|
80
|
+
|
81
|
+
parent_klass.const_set(class_names[:child_name], klass)
|
82
|
+
end
|
83
|
+
|
84
|
+
def resolve_parent_class(parent_namespace)
|
85
|
+
return Object if parent_namespace.empty?
|
86
|
+
|
87
|
+
begin
|
88
|
+
Object.const_get(parent_namespace)
|
89
|
+
rescue NameError
|
90
|
+
Object
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
def in_test_environment?
|
95
|
+
# Check if we're in a test environment by looking for RSpec or test-related constants
|
96
|
+
defined?(RSpec) || defined?(Test::Unit) || ENV['RAILS_ENV'] == 'test' || ENV['RACK_ENV'] == 'test'
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
@@ -0,0 +1,92 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'link_set'
|
4
|
+
|
5
|
+
module Lutaml
|
6
|
+
module Hal
|
7
|
+
# Factory class responsible for creating dynamic LinkSet classes
|
8
|
+
class LinkSetClassFactory
|
9
|
+
def self.create_for(resource_class)
|
10
|
+
new(resource_class).create
|
11
|
+
end
|
12
|
+
|
13
|
+
def initialize(resource_class)
|
14
|
+
@resource_class = resource_class
|
15
|
+
end
|
16
|
+
|
17
|
+
def create
|
18
|
+
return create_anonymous_link_set_class if anonymous_class?
|
19
|
+
|
20
|
+
class_names = build_class_names
|
21
|
+
return existing_class(class_names[:full_name]) if class_exists?(class_names[:full_name])
|
22
|
+
|
23
|
+
klass = create_named_link_set_class(class_names)
|
24
|
+
register_constant(klass, class_names)
|
25
|
+
setup_resource_mapping(klass)
|
26
|
+
klass
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def anonymous_class?
|
32
|
+
@resource_class.name.nil?
|
33
|
+
end
|
34
|
+
|
35
|
+
def create_anonymous_link_set_class
|
36
|
+
klass = Class.new(LinkSet)
|
37
|
+
setup_resource_mapping(klass)
|
38
|
+
klass
|
39
|
+
end
|
40
|
+
|
41
|
+
def create_named_link_set_class(class_names)
|
42
|
+
Hal.debug_log "Creating link set class #{class_names[:full_name]}"
|
43
|
+
Class.new(LinkSet)
|
44
|
+
end
|
45
|
+
|
46
|
+
def build_class_names
|
47
|
+
parent_namespace = @resource_class.name.split('::')[0..-2].join('::')
|
48
|
+
child_class_name = "#{@resource_class.name.split('::').last}LinkSet"
|
49
|
+
full_class_name = [parent_namespace, child_class_name].reject(&:empty?).join('::')
|
50
|
+
|
51
|
+
{
|
52
|
+
parent_namespace: parent_namespace,
|
53
|
+
child_name: child_class_name,
|
54
|
+
full_name: full_class_name
|
55
|
+
}
|
56
|
+
end
|
57
|
+
|
58
|
+
def class_exists?(class_name)
|
59
|
+
Object.const_defined?(class_name)
|
60
|
+
end
|
61
|
+
|
62
|
+
def existing_class(class_name)
|
63
|
+
Object.const_get(class_name)
|
64
|
+
end
|
65
|
+
|
66
|
+
def register_constant(klass, class_names)
|
67
|
+
parent_klass = resolve_parent_class(class_names[:parent_namespace])
|
68
|
+
parent_klass.const_set(class_names[:child_name], klass)
|
69
|
+
end
|
70
|
+
|
71
|
+
def resolve_parent_class(parent_namespace)
|
72
|
+
return Object if parent_namespace.empty?
|
73
|
+
|
74
|
+
begin
|
75
|
+
Object.const_get(parent_namespace)
|
76
|
+
rescue NameError
|
77
|
+
Object
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def setup_resource_mapping(link_set_class)
|
82
|
+
# Define the LinkSet class with mapping inside the resource class
|
83
|
+
@resource_class.class_eval do
|
84
|
+
attribute :links, link_set_class
|
85
|
+
key_value do
|
86
|
+
map '_links', to: :links
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
@@ -16,11 +16,11 @@ module Lutaml
|
|
16
16
|
end
|
17
17
|
|
18
18
|
# Register a model with its base URL pattern
|
19
|
-
def add_endpoint(id:, type:, url:, model:)
|
19
|
+
def add_endpoint(id:, type:, url:, model:, query_params: nil)
|
20
20
|
@models ||= {}
|
21
21
|
|
22
22
|
raise "Model with ID #{id} already registered" if @models[id]
|
23
|
-
if @models.values.any? { |m| m[:url] == url && m[:type] == type }
|
23
|
+
if @models.values.any? { |m| m[:url] == url && m[:type] == type && m[:query_params] == query_params }
|
24
24
|
raise "Duplicate URL pattern #{url} for type #{type}"
|
25
25
|
end
|
26
26
|
|
@@ -28,7 +28,8 @@ module Lutaml
|
|
28
28
|
id: id,
|
29
29
|
type: type,
|
30
30
|
url: url,
|
31
|
-
model: model
|
31
|
+
model: model,
|
32
|
+
query_params: query_params
|
32
33
|
}
|
33
34
|
end
|
34
35
|
|
@@ -38,7 +39,7 @@ module Lutaml
|
|
38
39
|
raise 'Client not configured' unless client
|
39
40
|
|
40
41
|
url = interpolate_url(endpoint[:url], params)
|
41
|
-
response = client.get(url)
|
42
|
+
response = client.get(build_url_with_query_params(url, endpoint[:query_params], params))
|
42
43
|
|
43
44
|
realized_model = endpoint[:model].from_json(response.to_json)
|
44
45
|
|
@@ -103,10 +104,98 @@ module Lutaml
|
|
103
104
|
end
|
104
105
|
end
|
105
106
|
|
107
|
+
def build_url_with_query_params(base_url, query_params_template, params)
|
108
|
+
return base_url unless query_params_template
|
109
|
+
|
110
|
+
query_params = []
|
111
|
+
query_params_template.each do |param_name, param_template|
|
112
|
+
# If the template is like {page}, look for the param in the passed params
|
113
|
+
if param_template.is_a?(String) && param_template.match?(/\{(.+)\}/)
|
114
|
+
param_key = param_template.match(/\{(.+)\}/)[1]
|
115
|
+
query_params << "#{param_name}=#{params[param_key.to_sym]}" if params[param_key.to_sym]
|
116
|
+
else
|
117
|
+
# Fixed parameter - always include it
|
118
|
+
query_params << "#{param_name}=#{param_template}"
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
query_params.any? ? "#{base_url}?#{query_params.join('&')}" : base_url
|
123
|
+
end
|
124
|
+
|
106
125
|
def find_matching_model_class(href)
|
107
|
-
|
108
|
-
|
109
|
-
|
126
|
+
# Find all matching patterns and select the most specific one (longest pattern)
|
127
|
+
matching_models = @models.values.select do |model_data|
|
128
|
+
matches = matches_url_with_params?(model_data, href)
|
129
|
+
matches
|
130
|
+
end
|
131
|
+
|
132
|
+
return nil if matching_models.empty?
|
133
|
+
|
134
|
+
# Sort by pattern length (descending) to get the most specific match first
|
135
|
+
result = matching_models.max_by { |model_data| model_data[:url].length }
|
136
|
+
|
137
|
+
result[:model]
|
138
|
+
end
|
139
|
+
|
140
|
+
def matches_url_with_params?(model_data, href)
|
141
|
+
pattern = model_data[:url]
|
142
|
+
query_params = model_data[:query_params]
|
143
|
+
|
144
|
+
return false unless pattern && href
|
145
|
+
|
146
|
+
uri = parse_href_uri(href)
|
147
|
+
pattern_path = extract_pattern_path(pattern)
|
148
|
+
|
149
|
+
path_match_result = path_matches?(pattern_path, uri.path)
|
150
|
+
return false unless path_match_result
|
151
|
+
|
152
|
+
return true unless query_params
|
153
|
+
|
154
|
+
parsed_query = parse_query_params(uri.query)
|
155
|
+
query_params_match?(query_params, parsed_query)
|
156
|
+
end
|
157
|
+
|
158
|
+
def parse_href_uri(href)
|
159
|
+
full_href = href.start_with?('http') ? href : "#{client&.api_url}#{href}"
|
160
|
+
URI.parse(full_href)
|
161
|
+
end
|
162
|
+
|
163
|
+
def extract_pattern_path(pattern)
|
164
|
+
pattern.split('?').first
|
165
|
+
end
|
166
|
+
|
167
|
+
def path_matches?(pattern_path, href_path)
|
168
|
+
pattern_match?(pattern_path, href_path)
|
169
|
+
end
|
170
|
+
|
171
|
+
def query_params_match?(expected_params, actual_params)
|
172
|
+
# Query parameters should be optional - if they're template parameters (like {page}),
|
173
|
+
# they don't need to be present in the actual URL
|
174
|
+
expected_params.all? do |param_name, param_pattern|
|
175
|
+
actual_value = actual_params[param_name]
|
176
|
+
|
177
|
+
# If it's a template parameter (like {page}), it's optional
|
178
|
+
if template_param?(param_pattern)
|
179
|
+
# Template parameters are always considered matching (they're optional)
|
180
|
+
true
|
181
|
+
else
|
182
|
+
# Non-template parameters must match exactly if present
|
183
|
+
actual_value == param_pattern.to_s
|
184
|
+
end
|
185
|
+
end
|
186
|
+
end
|
187
|
+
|
188
|
+
def template_param?(param_pattern)
|
189
|
+
param_pattern.is_a?(String) && param_pattern.match?(/\{.+\}/)
|
190
|
+
end
|
191
|
+
|
192
|
+
def parse_query_params(query_string)
|
193
|
+
return {} unless query_string
|
194
|
+
|
195
|
+
query_string.split('&').each_with_object({}) do |param, hash|
|
196
|
+
key, value = param.split('=', 2)
|
197
|
+
hash[key] = value if key
|
198
|
+
end
|
110
199
|
end
|
111
200
|
|
112
201
|
def matches_url?(pattern, href)
|
@@ -134,7 +223,8 @@ module Lutaml
|
|
134
223
|
|
135
224
|
# Convert {param} to wildcards for matching
|
136
225
|
pattern_with_wildcards = pattern.gsub(/\{[^}]+\}/, '*')
|
137
|
-
# Convert * wildcards to regex pattern
|
226
|
+
# Convert * wildcards to regex pattern - use [^/]+ to match path segments, not across slashes
|
227
|
+
# This ensures that {param} only matches a single path segment
|
138
228
|
regex = Regexp.new("^#{pattern_with_wildcards.gsub('*', '[^/]+')}$")
|
139
229
|
|
140
230
|
Hal.debug_log("pattern_match?: regex: #{regex.inspect}")
|
data/lib/lutaml/hal/page.rb
CHANGED
@@ -23,6 +23,9 @@ module Lutaml
|
|
23
23
|
def self.inherited(subclass)
|
24
24
|
super
|
25
25
|
|
26
|
+
# Skip automatic link creation for anonymous classes (used in tests)
|
27
|
+
return unless subclass.name
|
28
|
+
|
26
29
|
page_links_symbols = %i[self next prev first last up]
|
27
30
|
subclass_name = subclass.name
|
28
31
|
subclass.class_eval do
|
@@ -32,6 +35,144 @@ module Lutaml
|
|
32
35
|
end
|
33
36
|
end
|
34
37
|
end
|
38
|
+
|
39
|
+
# Returns the next page of results, or nil if on the last page
|
40
|
+
#
|
41
|
+
# @return [Object, nil] The next page instance or nil
|
42
|
+
def next
|
43
|
+
return nil unless links.next
|
44
|
+
|
45
|
+
links.next.realize
|
46
|
+
end
|
47
|
+
|
48
|
+
# Returns the previous page of results, or nil if on the first page
|
49
|
+
#
|
50
|
+
# @return [Object, nil] The previous page instance or nil
|
51
|
+
def prev
|
52
|
+
# If the API provides a prev link, use it
|
53
|
+
return links.prev.realize if links.prev
|
54
|
+
|
55
|
+
# If we're on page 1, there's no previous page
|
56
|
+
return nil if page <= 1
|
57
|
+
|
58
|
+
# Construct the previous page URL manually
|
59
|
+
prev_page_url = construct_page_url(page - 1)
|
60
|
+
return nil unless prev_page_url
|
61
|
+
|
62
|
+
# Use the HAL register to fetch the previous page
|
63
|
+
register_name = instance_variable_get("@#{Lutaml::Hal::REGISTER_ID_ATTR_NAME}")
|
64
|
+
return nil unless register_name
|
65
|
+
|
66
|
+
hal_register = Lutaml::Hal::GlobalRegister.instance.get(register_name)
|
67
|
+
return nil unless hal_register
|
68
|
+
|
69
|
+
hal_register.resolve_and_cast(nil, prev_page_url)
|
70
|
+
end
|
71
|
+
|
72
|
+
# Returns the first page of results
|
73
|
+
#
|
74
|
+
# @return [Object, nil] The first page instance or nil
|
75
|
+
def first
|
76
|
+
return nil unless links.first
|
77
|
+
|
78
|
+
links.first.realize
|
79
|
+
end
|
80
|
+
|
81
|
+
# Returns the last page of results
|
82
|
+
#
|
83
|
+
# @return [Object, nil] The last page instance or nil
|
84
|
+
def last
|
85
|
+
return nil unless links.last
|
86
|
+
|
87
|
+
links.last.realize
|
88
|
+
end
|
89
|
+
|
90
|
+
# Returns the total number of pages
|
91
|
+
#
|
92
|
+
# @return [Integer] The total number of pages
|
93
|
+
def total_pages
|
94
|
+
pages
|
95
|
+
end
|
96
|
+
|
97
|
+
# Checks if there is a next page available
|
98
|
+
#
|
99
|
+
# @return [Boolean] true if next page exists, false otherwise
|
100
|
+
def next?
|
101
|
+
!links.next.nil?
|
102
|
+
end
|
103
|
+
|
104
|
+
# Checks if there is a previous page available
|
105
|
+
#
|
106
|
+
# @return [Boolean] true if previous page exists, false otherwise
|
107
|
+
def prev?
|
108
|
+
!links.prev.nil?
|
109
|
+
end
|
110
|
+
|
111
|
+
# Checks if there is a first page link available
|
112
|
+
#
|
113
|
+
# @return [Boolean] true if first page link exists, false otherwise
|
114
|
+
def first?
|
115
|
+
!links.first.nil?
|
116
|
+
end
|
117
|
+
|
118
|
+
# Checks if there is a last page link available
|
119
|
+
#
|
120
|
+
# @return [Boolean] true if last page link exists, false otherwise
|
121
|
+
def last?
|
122
|
+
!links.last.nil?
|
123
|
+
end
|
124
|
+
|
125
|
+
# Returns the next page link, or nil if on the last page
|
126
|
+
#
|
127
|
+
# @return [Object, nil] The next page link or nil
|
128
|
+
def next_page
|
129
|
+
links.next
|
130
|
+
end
|
131
|
+
|
132
|
+
# Returns the previous page link, or nil if on the first page
|
133
|
+
#
|
134
|
+
# @return [Object, nil] The previous page link or nil
|
135
|
+
def prev_page
|
136
|
+
links.prev
|
137
|
+
end
|
138
|
+
|
139
|
+
# Returns the first page link
|
140
|
+
#
|
141
|
+
# @return [Object, nil] The first page link or nil
|
142
|
+
def first_page
|
143
|
+
links.first
|
144
|
+
end
|
145
|
+
|
146
|
+
# Returns the last page link
|
147
|
+
#
|
148
|
+
# @return [Object, nil] The last page link or nil
|
149
|
+
def last_page
|
150
|
+
links.last
|
151
|
+
end
|
152
|
+
|
153
|
+
private
|
154
|
+
|
155
|
+
# Constructs a URL for a specific page based on the current page's URL pattern
|
156
|
+
#
|
157
|
+
# @param target_page [Integer] The page number to construct URL for
|
158
|
+
# @return [String, nil] The constructed URL or nil if unable to construct
|
159
|
+
def construct_page_url(target_page)
|
160
|
+
# Try to get a reference URL from next, first, or last links
|
161
|
+
reference_url = links.next&.href || links.first&.href || links.last&.href
|
162
|
+
return nil unless reference_url
|
163
|
+
|
164
|
+
# Parse the reference URL and modify the page parameter
|
165
|
+
uri = URI.parse(reference_url)
|
166
|
+
query_params = URI.decode_www_form(uri.query || '')
|
167
|
+
|
168
|
+
# Update the page parameter
|
169
|
+
query_params = query_params.reject { |key, _| key == 'page' }
|
170
|
+
query_params << ['page', target_page.to_s]
|
171
|
+
|
172
|
+
# Reconstruct the URL
|
173
|
+
uri.query = URI.encode_www_form(query_params)
|
174
|
+
uri.to_s
|
175
|
+
end
|
35
176
|
end
|
36
177
|
end
|
37
178
|
end
|
data/lib/lutaml/hal/resource.rb
CHANGED
@@ -2,6 +2,8 @@
|
|
2
2
|
|
3
3
|
require 'lutaml/model'
|
4
4
|
require_relative 'link'
|
5
|
+
require_relative 'link_class_factory'
|
6
|
+
require_relative 'link_set_class_factory'
|
5
7
|
|
6
8
|
module Lutaml
|
7
9
|
module Hal
|
@@ -18,7 +20,6 @@ module Lutaml
|
|
18
20
|
def inherited(subclass)
|
19
21
|
super
|
20
22
|
subclass.class_eval do
|
21
|
-
create_link_set_class
|
22
23
|
init_links_definition
|
23
24
|
end
|
24
25
|
end
|
@@ -36,21 +37,47 @@ module Lutaml
|
|
36
37
|
link_set_class: nil,
|
37
38
|
collection: false,
|
38
39
|
type: :link)
|
40
|
+
# Validate required parameters
|
41
|
+
raise ArgumentError, 'realize_class parameter is required' if realize_class.nil?
|
42
|
+
|
39
43
|
# Use the provided "key" as the attribute name
|
40
44
|
attribute_name = attr_key.to_sym
|
41
45
|
|
42
46
|
Hal.debug_log "Defining HAL link for `#{attr_key}` with realize class `#{realize_class}`"
|
43
47
|
|
48
|
+
# Normalize realize_class to a string for consistent handling
|
49
|
+
# Support both Class objects (when autoload is available) and strings (for delayed interpretation)
|
50
|
+
realize_class_name = case realize_class
|
51
|
+
when Class
|
52
|
+
realize_class.name.split('::').last # Use simple name from actual class
|
53
|
+
when String
|
54
|
+
realize_class # Use string as-is for lazy resolution
|
55
|
+
else
|
56
|
+
raise ArgumentError,
|
57
|
+
"realize_class must be a Class or String, got #{realize_class.class}"
|
58
|
+
end
|
59
|
+
|
60
|
+
# Create a dynamic LinkSet class if `link_set_class:` is not provided.
|
61
|
+
# This must happen BEFORE creating the Link class to ensure proper order
|
62
|
+
link_set_klass = link_set_class || create_link_set_class
|
63
|
+
|
64
|
+
# Ensure it was actually created
|
65
|
+
raise 'Failed to create LinkSet class' if link_set_klass.nil?
|
66
|
+
|
44
67
|
# Create a dynamic Link subclass name based on "realize_class", the
|
45
68
|
# class to realize for a Link object, if `link_class:` is not provided.
|
46
|
-
link_klass = link_class || create_link_class(
|
69
|
+
link_klass = link_class || create_link_class(realize_class_name)
|
47
70
|
|
48
|
-
#
|
71
|
+
# Now add the link to the LinkSet class
|
49
72
|
unless link_set_class
|
50
|
-
link_set_klass = link_set_class || get_link_set_class
|
51
73
|
link_set_klass.class_eval do
|
52
74
|
# Declare the corresponding lutaml-model attribute
|
53
|
-
|
75
|
+
# Pass collection parameter correctly to the attribute definition
|
76
|
+
if collection
|
77
|
+
attribute attribute_name, link_klass, collection: true
|
78
|
+
else
|
79
|
+
attribute attribute_name, link_klass
|
80
|
+
end
|
54
81
|
|
55
82
|
# Define the mapping for the attribute
|
56
83
|
key_value do
|
@@ -73,69 +100,26 @@ module Lutaml
|
|
73
100
|
end
|
74
101
|
|
75
102
|
# This method obtains the Links class that holds the Link classes
|
103
|
+
# Delegates to LinkSetClassFactory for simplified implementation
|
76
104
|
def get_link_set_class
|
77
|
-
|
78
|
-
child_klass_name = "#{name.split('::').last}LinkSet"
|
79
|
-
klass_name = [parent_klass_name, child_klass_name].join('::')
|
80
|
-
|
81
|
-
raise unless Object.const_defined?(klass_name)
|
82
|
-
|
83
|
-
Object.const_get(klass_name)
|
105
|
+
create_link_set_class
|
84
106
|
end
|
85
107
|
|
86
|
-
private
|
87
|
-
|
88
108
|
# The "links" class holds the `_links` object which contains
|
89
109
|
# the resource-linked Link classes
|
110
|
+
# Delegates to LinkSetClassFactory for simplified implementation
|
90
111
|
def create_link_set_class
|
91
|
-
|
92
|
-
child_klass_name = "#{name.split('::').last}LinkSet"
|
93
|
-
klass_name = [parent_klass_name, child_klass_name].join('::')
|
94
|
-
|
95
|
-
Hal.debug_log "Creating link set class #{klass_name}"
|
96
|
-
|
97
|
-
# Check if the LinkSet class is already defined, return if so
|
98
|
-
return Object.const_get(klass_name) if Object.const_defined?(klass_name)
|
99
|
-
|
100
|
-
# Define the LinkSet class dynamically as a normal Lutaml::Model class
|
101
|
-
# since it is not a Resource.
|
102
|
-
klass = Class.new(Lutaml::Hal::LinkSet)
|
103
|
-
parent_klass = !parent_klass_name.empty? ? Object.const_get(parent_klass_name) : Object
|
104
|
-
parent_klass.const_set(child_klass_name, klass)
|
105
|
-
|
106
|
-
# Define the LinkSet class with mapping inside the current class
|
107
|
-
class_eval do
|
108
|
-
attribute :links, klass
|
109
|
-
key_value do
|
110
|
-
map '_links', to: :links
|
111
|
-
end
|
112
|
-
end
|
112
|
+
LinkSetClassFactory.create_for(self)
|
113
113
|
end
|
114
114
|
|
115
115
|
def init_links_definition
|
116
116
|
@link_definitions = {}
|
117
117
|
end
|
118
118
|
|
119
|
-
#
|
119
|
+
# Creates a Link class that helps us realize the targeted class
|
120
|
+
# Delegates to LinkClassFactory for simplified implementation
|
120
121
|
def create_link_class(realize_class_name)
|
121
|
-
|
122
|
-
child_klass_name = "#{realize_class_name.split('::').last}Link"
|
123
|
-
klass_name = [parent_klass_name, child_klass_name].join('::')
|
124
|
-
|
125
|
-
Hal.debug_log "Creating link class #{klass_name} for #{realize_class_name}"
|
126
|
-
|
127
|
-
return Object.const_get(klass_name) if Object.const_defined?(klass_name)
|
128
|
-
|
129
|
-
# Define the link class dynamically
|
130
|
-
klass = Class.new(Link) do
|
131
|
-
# Define the link class with the specified key and class
|
132
|
-
attribute :type, :string, default: realize_class_name
|
133
|
-
end
|
134
|
-
|
135
|
-
parent_klass = !parent_klass_name.empty? ? Object.const_get(parent_klass_name) : Object
|
136
|
-
parent_klass.const_set(child_klass_name, klass)
|
137
|
-
|
138
|
-
klass
|
122
|
+
LinkClassFactory.create_for(self, realize_class_name)
|
139
123
|
end
|
140
124
|
end
|
141
125
|
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Lutaml
|
4
|
+
module Hal
|
5
|
+
# Module that provides lazy type resolution functionality for dynamically created classes
|
6
|
+
# This solves the class loading order problem where HAL type names would be inconsistent
|
7
|
+
# depending on file loading order.
|
8
|
+
module TypeResolver
|
9
|
+
def self.included(base)
|
10
|
+
base.extend(ClassMethods)
|
11
|
+
end
|
12
|
+
|
13
|
+
module ClassMethods
|
14
|
+
attr_reader :realize_class_name
|
15
|
+
|
16
|
+
def setup_type_resolution(realize_class_name)
|
17
|
+
@realize_class_name = realize_class_name
|
18
|
+
@resolved_type_name = nil
|
19
|
+
end
|
20
|
+
|
21
|
+
# Lazy resolution at class level - only happens once per class
|
22
|
+
def resolved_type_name
|
23
|
+
@resolved_type_name ||= resolve_type_name(@realize_class_name)
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def resolve_type_name(class_name_string)
|
29
|
+
return class_name_string unless class_name_string.is_a?(String)
|
30
|
+
|
31
|
+
# Try simple name first (preferred for HAL output)
|
32
|
+
begin
|
33
|
+
Object.const_get(class_name_string)
|
34
|
+
class_name_string
|
35
|
+
rescue NameError
|
36
|
+
# Try within current module namespace
|
37
|
+
begin
|
38
|
+
current_module = name.split('::')[0..-2].join('::')
|
39
|
+
unless current_module.empty?
|
40
|
+
Object.const_get(current_module).const_get(class_name_string)
|
41
|
+
return class_name_string
|
42
|
+
end
|
43
|
+
rescue NameError
|
44
|
+
# Continue to fallback
|
45
|
+
end
|
46
|
+
|
47
|
+
# Fallback: return the original string (may be fully qualified)
|
48
|
+
class_name_string
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
# Override the type getter to use class-level lazy resolution
|
54
|
+
def type
|
55
|
+
@type || self.class.resolved_type_name
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
data/lib/lutaml/hal/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: lutaml-hal
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.7
|
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-
|
11
|
+
date: 2025-07-04 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: faraday
|
@@ -82,10 +82,13 @@ files:
|
|
82
82
|
- lib/lutaml/hal/errors.rb
|
83
83
|
- lib/lutaml/hal/global_register.rb
|
84
84
|
- lib/lutaml/hal/link.rb
|
85
|
+
- lib/lutaml/hal/link_class_factory.rb
|
85
86
|
- lib/lutaml/hal/link_set.rb
|
87
|
+
- lib/lutaml/hal/link_set_class_factory.rb
|
86
88
|
- lib/lutaml/hal/model_register.rb
|
87
89
|
- lib/lutaml/hal/page.rb
|
88
90
|
- lib/lutaml/hal/resource.rb
|
91
|
+
- lib/lutaml/hal/type_resolver.rb
|
89
92
|
- lib/lutaml/hal/version.rb
|
90
93
|
homepage: https://github.com/lutaml/lutaml-hal
|
91
94
|
licenses:
|