lutaml-hal 0.1.6 → 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/README.adoc +141 -43
- 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 +34 -15
- data/lib/lutaml/hal/page.rb +3 -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/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
|
-
|
354
|
-
|
383
|
+
[source,ruby]
|
384
|
+
----
|
385
|
+
hal_link :category, key: 'category', realize_class: 'Category', link_class: 'CategoryLink'
|
386
|
+
----
|
355
387
|
|
356
|
-
|
357
|
-
after the resource's class name (with an appended `LinkSet` string), which in turn
|
358
|
-
contains the defined links to other resources. The link set class is inherited
|
359
|
-
from `Lutaml::Model::Serializable`.
|
388
|
+
**Class references**: Use actual class objects when classes are statically available:
|
360
389
|
|
361
|
-
[
|
362
|
-
|
363
|
-
|
364
|
-
|
365
|
-
|
390
|
+
[source,ruby]
|
391
|
+
----
|
392
|
+
hal_link :category, key: 'category', realize_class: Category, link_class: CategoryLink
|
393
|
+
----
|
394
|
+
--
|
366
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:
|
367
404
|
|
368
|
-
|
369
|
-
|
370
|
-
|
371
|
-
|
405
|
+
[source,ruby]
|
406
|
+
----
|
407
|
+
hal_link :category, key: 'category', realize_class: 'Category', link_set_class: 'ProductLinkSet'
|
408
|
+
----
|
372
409
|
|
373
|
-
|
374
|
-
====
|
375
|
-
A HAL resource of class `Product` with a link set that contains the `self`
|
376
|
-
(points to a `Product`) and `category` (points to a `Category`) links will
|
377
|
-
have:
|
410
|
+
**Class references**: Use actual class objects when classes are statically available:
|
378
411
|
|
379
|
-
|
380
|
-
|
381
|
-
|
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.
|
@@ -744,8 +847,6 @@ must inherit from `Lutaml::Hal::Page`.
|
|
744
847
|
`model`:: The class of the page that will be fetched from the API.
|
745
848
|
|
746
849
|
|
747
|
-
|
748
|
-
|
749
850
|
== Usage: Runtime
|
750
851
|
|
751
852
|
=== General
|
@@ -810,7 +911,7 @@ product_1 = register.fetch(:product_resource, id: 1)
|
|
810
911
|
|
811
912
|
product_1
|
812
913
|
# => #<Product id: 1, name: "Product 1", price: 10.0, links:
|
813
|
-
# #<
|
914
|
+
# #<ProductLinkSet self: <ProductLink href: "/products/1">,
|
814
915
|
# category: <ProductLink href: "/categories/1", title: "Category 1">,
|
815
916
|
# related: [
|
816
917
|
# <ProductLink href: "/products/3", title: "Product 3">,
|
@@ -819,8 +920,6 @@ product_1
|
|
819
920
|
----
|
820
921
|
====
|
821
922
|
|
822
|
-
|
823
|
-
|
824
923
|
=== Fetching a resource index
|
825
924
|
|
826
925
|
In HAL, collections are provided via the `_links` or the `_embedded` sections of
|
@@ -872,10 +971,10 @@ product_index = register.fetch(:product_index)
|
|
872
971
|
|
873
972
|
product_index
|
874
973
|
# => #<ProductPage page: 1, pages: 10, limit: 10, total: 45,
|
875
|
-
# links: #<
|
974
|
+
# links: #<ProductLinkSet self: <ProductLink href: "/products/1">,
|
876
975
|
# next: <ProductLink href: "/products/2">,
|
877
976
|
# last: <ProductLink href: "/products/5">,
|
878
|
-
# products: <
|
977
|
+
# products: <ProductLinkSet
|
879
978
|
# <ProductLink href: "/products/1", title: "Product 1">,
|
880
979
|
# <ProductLink href: "/products/2", title: "Product 2">
|
881
980
|
# ]>>
|
@@ -971,7 +1070,7 @@ product_2 = product_index.links.products.last.realize
|
|
971
1070
|
|
972
1071
|
product_2
|
973
1072
|
# => #<Product id: 2, name: "Product 2", price: 20.0, links:
|
974
|
-
# #<
|
1073
|
+
# #<ProductLinkSet self: <ProductLink href: "/products/2">,
|
975
1074
|
# category: <ProductLink href: "/categories/2", title: "Category 2">,
|
976
1075
|
# related: [
|
977
1076
|
# <ProductLink href: "/products/4", title: "Product 4">,
|
@@ -986,7 +1085,6 @@ product_2_related_1 = product_2.links.related.first.realize
|
|
986
1085
|
----
|
987
1086
|
====
|
988
1087
|
|
989
|
-
|
990
1088
|
=== Handling HAL pages / pagination
|
991
1089
|
|
992
1090
|
==== General
|
@@ -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
|
@@ -113,6 +113,9 @@ module Lutaml
|
|
113
113
|
if param_template.is_a?(String) && param_template.match?(/\{(.+)\}/)
|
114
114
|
param_key = param_template.match(/\{(.+)\}/)[1]
|
115
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}"
|
116
119
|
end
|
117
120
|
end
|
118
121
|
|
@@ -120,9 +123,18 @@ module Lutaml
|
|
120
123
|
end
|
121
124
|
|
122
125
|
def find_matching_model_class(href)
|
123
|
-
|
124
|
-
|
125
|
-
|
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]
|
126
138
|
end
|
127
139
|
|
128
140
|
def matches_url_with_params?(model_data, href)
|
@@ -134,10 +146,13 @@ module Lutaml
|
|
134
146
|
uri = parse_href_uri(href)
|
135
147
|
pattern_path = extract_pattern_path(pattern)
|
136
148
|
|
137
|
-
|
149
|
+
path_match_result = path_matches?(pattern_path, uri.path)
|
150
|
+
return false unless path_match_result
|
151
|
+
|
138
152
|
return true unless query_params
|
139
153
|
|
140
|
-
|
154
|
+
parsed_query = parse_query_params(uri.query)
|
155
|
+
query_params_match?(query_params, parsed_query)
|
141
156
|
end
|
142
157
|
|
143
158
|
def parse_href_uri(href)
|
@@ -150,20 +165,23 @@ module Lutaml
|
|
150
165
|
end
|
151
166
|
|
152
167
|
def path_matches?(pattern_path, href_path)
|
153
|
-
|
154
|
-
path_pattern = extract_path(pattern_path)
|
155
|
-
pattern_match?(path_pattern, href_path) || pattern_match?(pattern_path, href_path)
|
156
|
-
else
|
157
|
-
pattern_match?(pattern_path, href_path)
|
158
|
-
end
|
168
|
+
pattern_match?(pattern_path, href_path)
|
159
169
|
end
|
160
170
|
|
161
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
|
162
174
|
expected_params.all? do |param_name, param_pattern|
|
163
175
|
actual_value = actual_params[param_name]
|
164
|
-
next false unless actual_value
|
165
176
|
|
166
|
-
|
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
|
167
185
|
end
|
168
186
|
end
|
169
187
|
|
@@ -205,8 +223,9 @@ module Lutaml
|
|
205
223
|
|
206
224
|
# Convert {param} to wildcards for matching
|
207
225
|
pattern_with_wildcards = pattern.gsub(/\{[^}]+\}/, '*')
|
208
|
-
# Convert * wildcards to regex pattern - use
|
209
|
-
|
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
|
228
|
+
regex = Regexp.new("^#{pattern_with_wildcards.gsub('*', '[^/]+')}$")
|
210
229
|
|
211
230
|
Hal.debug_log("pattern_match?: regex: #{regex.inspect}")
|
212
231
|
Hal.debug_log("pattern_match?: href to match #{url}")
|
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
|
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-07-
|
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:
|