lutaml-hal 0.1.2 → 0.1.4
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 +184 -74
- data/lib/lutaml/hal/client.rb +12 -5
- data/lib/lutaml/hal/errors.rb +1 -0
- data/lib/lutaml/hal/global_register.rb +44 -0
- data/lib/lutaml/hal/link.rb +31 -3
- data/lib/lutaml/hal/link_set.rb +13 -0
- data/lib/lutaml/hal/model_register.rb +51 -14
- data/lib/lutaml/hal/page.rb +13 -0
- data/lib/lutaml/hal/resource.rb +24 -15
- data/lib/lutaml/hal/version.rb +1 -1
- data/lib/lutaml/hal.rb +7 -0
- metadata +3 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b1006ba9c57ff945fa51c368b1ca12971d83fc47f668513a962da76811b6cc91
|
4
|
+
data.tar.gz: 4bc4bc6697181d9512604736ba42a6d4ce16d67d235ffd5ef79b481e09ca8243
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: befafec41b21cd9244ae937bdb5ce5e975a99bd708cd960e68b4db122554b73d73ef506bc82bda0961d196f10785957c10246d14607345d2e8239420571b1a58
|
7
|
+
data.tar.gz: e719a907ff8e198f797691829b4fab1280e866f8a403ca80945a55f0178d48a0737c274dc12f3052c635d96f53d7ca3663081f12359293638aa7d6b2d45f9845
|
data/README.adoc
CHANGED
@@ -71,6 +71,10 @@ A registry for managing HAL resource models and their endpoints. It allows you
|
|
71
71
|
to register models, define their relationships, and fetch resources from the
|
72
72
|
API.
|
73
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
|
+
|
74
78
|
`Lutaml::Hal::Resource`::
|
75
79
|
A base class for defining HAL resource models. It includes methods for
|
76
80
|
defining attributes, links, and key-value mappings for resources.
|
@@ -96,6 +100,8 @@ At the data definition phase:
|
|
96
100
|
. Define the API endpoint using the `Client` class.
|
97
101
|
. Create a `ModelRegister` to manage the resource models and their
|
98
102
|
respective endpoints.
|
103
|
+
. (optional) Create a `GlobalRegister` to manage one or more `ModelRegister`
|
104
|
+
instances. It is necessary for automatic Link resolution.
|
99
105
|
. Define the resource models using the `Resource` class.
|
100
106
|
. Register the models with the `ModelRegister` and define their
|
101
107
|
relationships using the `add_endpoint` method.
|
@@ -145,10 +151,38 @@ require 'lutaml-hal'
|
|
145
151
|
|
146
152
|
# Create a new client with API endpoint
|
147
153
|
client = Lutaml::Hal::Client.new(api_url: 'https://api.example.com')
|
148
|
-
register = Lutaml::Hal::ModelRegister.new(client: client)
|
154
|
+
register = Lutaml::Hal::ModelRegister.new(name: :my_model_register, client: client)
|
149
155
|
# Or set client later, `register.client = client`
|
150
156
|
----
|
151
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.
|
167
|
+
|
168
|
+
[source,ruby]
|
169
|
+
----
|
170
|
+
require 'lutaml-hal'
|
171
|
+
|
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
|
+
|
152
186
|
|
153
187
|
=== Defining HAL resource models
|
154
188
|
|
@@ -158,7 +192,7 @@ A HAL resource is defined by creating a subclass of the `Resource` class and
|
|
158
192
|
defining its attributes, links, and key-value mappings.
|
159
193
|
|
160
194
|
The `Resource` class is the base class for defining HAL resource models.
|
161
|
-
It inherits from `Lutaml::Model::
|
195
|
+
It inherits from `Lutaml::Model::Serializable`, which provides data
|
162
196
|
modelling and serialization capabilities.
|
163
197
|
|
164
198
|
The declaration of attributes, links, and key-value mappings for a HAL resource
|
@@ -580,6 +614,69 @@ register.add_endpoint(
|
|
580
614
|
====
|
581
615
|
|
582
616
|
|
617
|
+
[[defining_hal_page_models]]
|
618
|
+
=== Defining HAL page models
|
619
|
+
|
620
|
+
HAL index APIs often support pagination, which allows clients to retrieve a
|
621
|
+
limited number of resources at a time.
|
622
|
+
|
623
|
+
The `Lutaml::Hal::Page` class is used to handle pagination in HAL APIs. It is a
|
624
|
+
subclass of `Resource`, and provides additional attributes and methods for
|
625
|
+
handling pagination information
|
626
|
+
|
627
|
+
The `Page` class by default supports the following attributes:
|
628
|
+
|
629
|
+
`page`:: The current page number.
|
630
|
+
`pages`:: The total number of pages.
|
631
|
+
`limit`:: The number of resources per page.
|
632
|
+
`total`:: The total number of resources.
|
633
|
+
|
634
|
+
The way to use the `Page` class is through inheritance from it, where the
|
635
|
+
class will automatically create the necessary links for typical page objects.
|
636
|
+
|
637
|
+
The typical links of a page object are:
|
638
|
+
|
639
|
+
`self`:: A link to the current page.
|
640
|
+
`prev`:: A link to the previous page.
|
641
|
+
`next`:: A link to the next page.
|
642
|
+
`first`:: A link to the first page.
|
643
|
+
`last`:: A link to the last page.
|
644
|
+
|
645
|
+
The "realize class" of these links are the same as the inherited page
|
646
|
+
object, ensuring consistency in the pagination model.
|
647
|
+
|
648
|
+
Syntax:
|
649
|
+
|
650
|
+
[source,ruby]
|
651
|
+
----
|
652
|
+
class ProductIndex < Lutaml::Hal::Page
|
653
|
+
# No attributes necessary
|
654
|
+
end
|
655
|
+
|
656
|
+
register.add_endpoint(
|
657
|
+
id: :product_index,
|
658
|
+
type: :index,
|
659
|
+
url: '/products',
|
660
|
+
model: ProductIndex
|
661
|
+
)
|
662
|
+
|
663
|
+
page_1 = register.fetch(:product_index) # Updated to use the correct endpoint id
|
664
|
+
page_2_link = page_1.links.next
|
665
|
+
# => <#ProductIndexLink href: "/products/2", title: "Next Page">
|
666
|
+
----
|
667
|
+
|
668
|
+
Where,
|
669
|
+
|
670
|
+
`ProductIndex`:: The class of the page that will be fetched from the API. The class
|
671
|
+
must inherit from `Lutaml::Hal::Page`.
|
672
|
+
`register`:: The instance of `ModelRegister`.
|
673
|
+
`id`:: The ID of the pagination endpoint to be registered in the `ModelRegister`.
|
674
|
+
`url`:: The URL of the pagination endpoint.
|
675
|
+
`model`:: The class of the page that will be fetched from the API.
|
676
|
+
|
677
|
+
|
678
|
+
|
679
|
+
|
583
680
|
== Usage: Runtime
|
584
681
|
|
585
682
|
=== General
|
@@ -668,9 +765,6 @@ them using the `fetch` method.
|
|
668
765
|
The `fetch` method will automatically handle the URL resolution and fetch the
|
669
766
|
resource index from the API.
|
670
767
|
|
671
|
-
// The `Page` class is used to handle pagination and resource
|
672
|
-
// resolution for collections.
|
673
|
-
|
674
768
|
Syntax:
|
675
769
|
|
676
770
|
[source,ruby]
|
@@ -720,16 +814,34 @@ product_index
|
|
720
814
|
====
|
721
815
|
|
722
816
|
|
817
|
+
[[fetching_resource_via_link_realization]]
|
723
818
|
=== Fetching a resource via link realization
|
724
819
|
|
725
820
|
Given a resource index that contains links to resources, the individual resource
|
726
821
|
links can be "realized" as actual model instances through the
|
727
|
-
`Link#realize(register)` method which dynamically retrieves the resource.
|
822
|
+
`Link#realize(register:)` method which dynamically retrieves the resource.
|
728
823
|
|
729
824
|
Given a `Link` object, the `realize` method fetches the resource from the API
|
730
825
|
using the provided `register`.
|
731
826
|
|
732
|
-
|
827
|
+
There are two ways a resource gets realized from a `Link` object:
|
828
|
+
|
829
|
+
* If a `Lutaml::Hal::GlobalRegister` is used, and the `Link` object originated
|
830
|
+
from a fetch using a `ModelRegister` then the `realize` method has sufficient
|
831
|
+
information to automatically fetch the resource from the API using the same
|
832
|
+
`register`.
|
833
|
+
+
|
834
|
+
NOTE: This relies on the `Hal::REGISTER_ID_ATTR_NAME` attribute to be set
|
835
|
+
in the `ModelRegister` class. This attribute is used to identify the
|
836
|
+
resource endpoint ID in the URL.
|
837
|
+
|
838
|
+
* If a `GlobalRegister` is not used, even if the Link object originated
|
839
|
+
from a fetch using a `ModelRegister`, the `realize` method does not have sufficient
|
840
|
+
information to fetch the resource from the API using the same
|
841
|
+
`register`. In this case an explicit `register` must be provided to the
|
842
|
+
`realize(register: ...)` method.
|
843
|
+
|
844
|
+
Syntax for standalone usage:
|
733
845
|
|
734
846
|
[source,ruby]
|
735
847
|
----
|
@@ -753,12 +865,26 @@ NOTE: It is possible to use the `realize` method on a link object using another
|
|
753
865
|
`ModelRegister` instance. This is useful when you want to resolve a link
|
754
866
|
using a different API endpoint or a different set of resource models.
|
755
867
|
|
868
|
+
Syntax when using a `GlobalRegister`:
|
869
|
+
|
870
|
+
[source,ruby]
|
871
|
+
----
|
872
|
+
resource_index = model_register.fetch(:resource_index)
|
873
|
+
resource_index.links.products.first.realize
|
874
|
+
# => client.get('/resources/1')
|
875
|
+
----
|
876
|
+
|
756
877
|
.Dynamically realizing a resource from the collection using links
|
757
878
|
[example]
|
758
879
|
====
|
759
880
|
[source,ruby]
|
760
881
|
----
|
882
|
+
# Without a GlobalRegister
|
761
883
|
product_2 = product_index.links.products.last.realize(register)
|
884
|
+
|
885
|
+
# With a GlobalRegister
|
886
|
+
product_2 = product_index.links.products.last.realize
|
887
|
+
|
762
888
|
# => client.get('/products/2')
|
763
889
|
# => {
|
764
890
|
# "id": 2,
|
@@ -782,54 +908,23 @@ product_2
|
|
782
908
|
# <ProductLink href: "/products/4", title: "Product 4">,
|
783
909
|
# <ProductLink href: "/products/6", title: "Product 6">
|
784
910
|
# ]}>
|
785
|
-
----
|
786
|
-
====
|
787
|
-
|
788
|
-
=== Pagination
|
789
|
-
|
790
|
-
HAL index APIs often support pagination, which allows clients to retrieve a
|
791
|
-
limited number of resources at a time.
|
792
911
|
|
793
|
-
|
794
|
-
|
795
|
-
methods to access the page's attributes and links.
|
912
|
+
# Without a GlobalRegister
|
913
|
+
product_2_related_1 = product_2.links.related.first.realize(register)
|
796
914
|
|
797
|
-
|
798
|
-
|
799
|
-
`page`:: The current page number.
|
800
|
-
`pages`:: The total number of pages.
|
801
|
-
`limit`:: The number of resources per page.
|
802
|
-
`total`:: The total number of resources.
|
803
|
-
|
804
|
-
Syntax:
|
805
|
-
|
806
|
-
[source,ruby]
|
915
|
+
# With a GlobalRegister
|
916
|
+
product_2_related_1 = product_2.links.related.first.realize
|
807
917
|
----
|
808
|
-
|
809
|
-
# These are typical links given for page objects
|
810
|
-
hal_link :self, key: 'self', realize_class: 'MyPage'
|
811
|
-
hal_link :prev, key: 'prev', realize_class: 'MyPage'
|
812
|
-
hal_link :next, key: 'next', realize_class: 'MyPage'
|
813
|
-
hal_link :first, key: 'first', realize_class: 'MyPage'
|
814
|
-
hal_link :last, key: 'last', realize_class: 'MyPage'
|
815
|
-
end
|
918
|
+
====
|
816
919
|
|
817
|
-
register.add_endpoint(
|
818
|
-
id: :my_pages,
|
819
|
-
type: :index,
|
820
|
-
url: '/my_pages',
|
821
|
-
model: MyPage
|
822
|
-
)
|
823
|
-
----
|
824
920
|
|
825
|
-
|
921
|
+
=== Handling HAL pages / pagination
|
826
922
|
|
827
|
-
`
|
828
|
-
|
829
|
-
|
830
|
-
|
831
|
-
|
832
|
-
`model`:: The class of the page that will be fetched from the API.
|
923
|
+
The `Lutaml::Hal::Page` class is used to handle pagination in HAL APIs.
|
924
|
+
|
925
|
+
As described in <<defining_hal_page_models>>, subclassing the `Page` class
|
926
|
+
provides pagination capabilities, including the management of links to navigate
|
927
|
+
through pages of resources.
|
833
928
|
|
834
929
|
|
835
930
|
.Usage example of the Page class
|
@@ -839,19 +934,15 @@ Declaration:
|
|
839
934
|
|
840
935
|
[source,ruby]
|
841
936
|
----
|
842
|
-
class
|
843
|
-
|
844
|
-
hal_link :prev, key: 'prev', realize_class: 'MyPage'
|
845
|
-
hal_link :next, key: 'next', realize_class: 'MyPage'
|
846
|
-
hal_link :first, key: 'first', realize_class: 'MyPage'
|
847
|
-
hal_link :last, key: 'last', realize_class: 'MyPage'
|
937
|
+
class ResourceIndex < Lutaml::Hal::Page
|
938
|
+
# No attribute definition necessary
|
848
939
|
end
|
849
940
|
|
850
941
|
register.add_endpoint(
|
851
|
-
id: :
|
942
|
+
id: :resource_index,
|
852
943
|
type: :index,
|
853
|
-
url: '/
|
854
|
-
model:
|
944
|
+
url: '/resources',
|
945
|
+
model: ResourceIndex
|
855
946
|
)
|
856
947
|
----
|
857
948
|
|
@@ -859,32 +950,51 @@ Usage:
|
|
859
950
|
|
860
951
|
[source,ruby]
|
861
952
|
----
|
862
|
-
page_1 = register.fetch(:
|
863
|
-
# => client.get('/
|
953
|
+
page_1 = register.fetch(:resource_index)
|
954
|
+
# => client.get('/resources')
|
864
955
|
# => {
|
865
956
|
# "page": 1,
|
866
957
|
# "pages": 10,
|
867
958
|
# "limit": 10,
|
868
959
|
# "total": 100,
|
869
960
|
# "_links": {
|
870
|
-
# "self": {
|
871
|
-
#
|
872
|
-
#
|
961
|
+
# "self": {
|
962
|
+
# "href": "https://api.example.com/resources?page=1&items=10"
|
963
|
+
# },
|
964
|
+
# "first": {
|
965
|
+
# "href": "https://api.example.com/resources?page=1&items=10"
|
966
|
+
# },
|
967
|
+
# "last": {
|
968
|
+
# "href": "https://api.example.com/resources?page=10&items=10"
|
969
|
+
# },
|
970
|
+
# "next": {
|
971
|
+
# "href": "https://api.example.com/resources?page=2&items=10"
|
972
|
+
# }
|
873
973
|
# }
|
874
974
|
# }
|
975
|
+
|
875
976
|
page_1
|
876
|
-
# => #<
|
877
|
-
# links: #<
|
878
|
-
#
|
879
|
-
#
|
977
|
+
# => #<ResourceIndex page: 1, pages: 10, limit: 10, total: 100,
|
978
|
+
# links: #<ResourceIndexLinks
|
979
|
+
# self: #<ResourceIndexLink href: "/resources?page=1&items=10">,
|
980
|
+
# next: #<ResourceIndexLink href: "/resources?page=2&items=10">,
|
981
|
+
# last: #<ResourceIndexLink href: "/resources?page=10&items=10">>>
|
982
|
+
|
983
|
+
# Without a GlobalRegister
|
880
984
|
page_2 = page.links.next.realize(register)
|
881
|
-
|
882
|
-
#
|
883
|
-
|
884
|
-
|
885
|
-
#
|
886
|
-
#
|
887
|
-
#
|
985
|
+
|
986
|
+
# With a GlobalRegister
|
987
|
+
page_2 = page.links.next.realize
|
988
|
+
|
989
|
+
# => client.get('/resources?page=2&items=10')
|
990
|
+
# => #<ResourceIndex page: 2, pages: 10, limit: 10, total: 100,
|
991
|
+
# links: #<ResourceIndexLinks
|
992
|
+
# self: #<ResourceIndexLink href: "/resources?page=2&items=10">,
|
993
|
+
# prev: #<ResourceIndexLink href: "/resources?page=1&items=10">,
|
994
|
+
# next: #<ResourceIndexLink href: "/resources?page=3&items=10">,
|
995
|
+
# first: #<ResourceIndexLink href: "/resources?page=1&items=10">,
|
996
|
+
# last: #<ResourceIndexLink href: "/resources?page=10&items=10">>>,
|
997
|
+
# prev: #<ResourceIndexLink href: "/resources?page=1&items=10">>>
|
888
998
|
----
|
889
999
|
====
|
890
1000
|
|
data/lib/lutaml/hal/client.rb
CHANGED
@@ -19,12 +19,19 @@ module Lutaml
|
|
19
19
|
@debug = options[:debug] || !ENV['DEBUG_API'].nil?
|
20
20
|
@cache = options[:cache] || {}
|
21
21
|
@cache_enabled = options[:cache_enabled] || false
|
22
|
+
|
23
|
+
@api_url = strip_api_url(@api_url)
|
24
|
+
end
|
25
|
+
|
26
|
+
# Strip any trailing slash from the API URL
|
27
|
+
def strip_api_url(url)
|
28
|
+
url.sub(%r{/\Z}, '')
|
22
29
|
end
|
23
30
|
|
24
31
|
# Get a resource by its full URL
|
25
32
|
def get_by_url(url, params = {})
|
26
33
|
# Strip API endpoint if it's included
|
27
|
-
path = url
|
34
|
+
path = strip_api_url(url)
|
28
35
|
get(path, params)
|
29
36
|
end
|
30
37
|
|
@@ -62,7 +69,7 @@ module Lutaml
|
|
62
69
|
end
|
63
70
|
|
64
71
|
def handle_response(response, url)
|
65
|
-
|
72
|
+
debug_api_log(response, url) if @debug
|
66
73
|
|
67
74
|
case response.status
|
68
75
|
when 200..299
|
@@ -80,11 +87,11 @@ module Lutaml
|
|
80
87
|
end
|
81
88
|
end
|
82
89
|
|
83
|
-
def
|
90
|
+
def debug_api_log(response, url)
|
84
91
|
if defined?(Rainbow)
|
85
|
-
puts Rainbow("\n===== DEBUG: HAL API REQUEST =====").blue
|
92
|
+
puts Rainbow("\n===== Lutaml::Hal DEBUG: HAL API REQUEST =====").blue
|
86
93
|
else
|
87
|
-
puts "\n===== DEBUG: HAL API REQUEST ====="
|
94
|
+
puts "\n===== Lutaml::Hal DEBUG: HAL API REQUEST ====="
|
88
95
|
end
|
89
96
|
|
90
97
|
puts "URL: #{url}"
|
data/lib/lutaml/hal/errors.rb
CHANGED
@@ -0,0 +1,44 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'singleton'
|
4
|
+
|
5
|
+
module Lutaml
|
6
|
+
module Hal
|
7
|
+
# Global register for model registers
|
8
|
+
# This class is a singleton that manages the registration and retrieval of model registers.
|
9
|
+
# It ensures that each model register is unique and provides a way to access them globally.
|
10
|
+
#
|
11
|
+
# @example
|
12
|
+
# global_register = GlobalRegister.instance
|
13
|
+
# global_register.register(:example, ExampleModelRegister.new)
|
14
|
+
# example_register = global_register.get(:example)
|
15
|
+
class GlobalRegister
|
16
|
+
include Singleton
|
17
|
+
|
18
|
+
def initialize
|
19
|
+
@model_registers = {}
|
20
|
+
end
|
21
|
+
|
22
|
+
def register(name, model_register)
|
23
|
+
if @model_registers[name] && @model_registers[name] != model_register
|
24
|
+
raise "Model register with name #{name} replacing another one" \
|
25
|
+
" (#{@model_registers[name].inspect} vs #{model_register.inspect})"
|
26
|
+
end
|
27
|
+
|
28
|
+
@model_registers[name] = model_register
|
29
|
+
end
|
30
|
+
|
31
|
+
def get(name)
|
32
|
+
raise "Model register with name #{name} not found" unless @model_registers[name]
|
33
|
+
|
34
|
+
@model_registers[name]
|
35
|
+
end
|
36
|
+
|
37
|
+
def delete(name)
|
38
|
+
return unless @model_registers[name]
|
39
|
+
|
40
|
+
@model_registers.delete(name)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
data/lib/lutaml/hal/link.rb
CHANGED
@@ -7,6 +7,10 @@ module Lutaml
|
|
7
7
|
module Hal
|
8
8
|
# HAL Link representation with realization capability
|
9
9
|
class Link < Lutaml::Model::Serializable
|
10
|
+
# This is the model register that has fetched the origin of this link, and
|
11
|
+
# will be used to resolve unless overriden in resource#realize()
|
12
|
+
attr_accessor Hal::REGISTER_ID_ATTR_NAME.to_sym
|
13
|
+
|
10
14
|
attribute :href, :string
|
11
15
|
attribute :title, :string
|
12
16
|
attribute :name, :string
|
@@ -16,9 +20,33 @@ module Lutaml
|
|
16
20
|
attribute :profile, :string
|
17
21
|
attribute :lang, :string
|
18
22
|
|
19
|
-
# Fetch the actual resource this link points to
|
20
|
-
|
21
|
-
|
23
|
+
# Fetch the actual resource this link points to.
|
24
|
+
# This method will use the global register according to the source of the Link object.
|
25
|
+
# If the Link does not have a register, a register needs to be provided explicitly
|
26
|
+
# via the `register:` parameter.
|
27
|
+
def realize(register: nil)
|
28
|
+
register = find_register(register)
|
29
|
+
raise "No register provided for link resolution (class: #{self.class}, href: #{href})" if register.nil?
|
30
|
+
|
31
|
+
Hal.debug_log "Resolving link href: #{href} using register"
|
32
|
+
register.resolve_and_cast(self, href)
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
def find_register(explicit_register)
|
38
|
+
return explicit_register if explicit_register
|
39
|
+
|
40
|
+
register_id = instance_variable_get("@#{Hal::REGISTER_ID_ATTR_NAME}")
|
41
|
+
return nil if register_id.nil?
|
42
|
+
|
43
|
+
register = Lutaml::Hal::GlobalRegister.instance.get(register_id)
|
44
|
+
if register.nil?
|
45
|
+
raise 'GlobalRegister in use but unable to find the register. '\
|
46
|
+
'Please provide a register to the `#realize` method to resolve the link'
|
47
|
+
end
|
48
|
+
|
49
|
+
register
|
22
50
|
end
|
23
51
|
end
|
24
52
|
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'lutaml/model'
|
4
|
+
require_relative 'model_register'
|
5
|
+
|
6
|
+
module Lutaml
|
7
|
+
module Hal
|
8
|
+
# HAL Link representation with realization capability
|
9
|
+
class LinkSet < Lutaml::Model::Serializable
|
10
|
+
attr_accessor Hal::REGISTER_ID_ATTR_NAME.to_sym
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -6,9 +6,10 @@ module Lutaml
|
|
6
6
|
module Hal
|
7
7
|
# Register to map URL patterns to model classes
|
8
8
|
class ModelRegister
|
9
|
-
attr_accessor :models, :client
|
9
|
+
attr_accessor :models, :client, :register_name
|
10
10
|
|
11
|
-
def initialize(client: nil)
|
11
|
+
def initialize(name:, client: nil)
|
12
|
+
@register_name = name
|
12
13
|
# If `client` is not set, it can be set later
|
13
14
|
@client = client
|
14
15
|
@models = {}
|
@@ -39,27 +40,59 @@ module Lutaml
|
|
39
40
|
url = interpolate_url(endpoint[:url], params)
|
40
41
|
response = client.get(url)
|
41
42
|
|
42
|
-
endpoint[:model].from_json(response.to_json)
|
43
|
+
realized_model = endpoint[:model].from_json(response.to_json)
|
44
|
+
|
45
|
+
mark_model_links_with_register(realized_model)
|
46
|
+
realized_model
|
43
47
|
end
|
44
48
|
|
45
|
-
def resolve_and_cast(href)
|
49
|
+
def resolve_and_cast(link, href)
|
46
50
|
raise 'Client not configured' unless client
|
47
51
|
|
48
|
-
debug_log("href #{href}")
|
52
|
+
Hal.debug_log("resolve_and_cast: link #{link}, href #{href}")
|
49
53
|
response = client.get_by_url(href)
|
50
54
|
|
51
|
-
# TODO: Merge
|
55
|
+
# TODO: Merge full Link content into the resource?
|
52
56
|
response_with_link_details = response.to_h.merge({ 'href' => href })
|
53
57
|
|
54
58
|
href_path = href.sub(client.api_url, '')
|
59
|
+
|
55
60
|
model_class = find_matching_model_class(href_path)
|
56
61
|
raise LinkResolutionError, "Unregistered URL pattern: #{href}" unless model_class
|
57
62
|
|
58
|
-
debug_log("model_class #{model_class}")
|
59
|
-
debug_log("response: #{response.inspect}")
|
60
|
-
debug_log("amended: #{response_with_link_details}")
|
63
|
+
Hal.debug_log("resolve_and_cast: resolved to model_class #{model_class}")
|
64
|
+
Hal.debug_log("resolve_and_cast: response: #{response.inspect}")
|
65
|
+
Hal.debug_log("resolve_and_cast: amended: #{response_with_link_details}")
|
61
66
|
|
62
|
-
model_class.from_json(response_with_link_details.to_json)
|
67
|
+
model = model_class.from_json(response_with_link_details.to_json)
|
68
|
+
mark_model_links_with_register(model)
|
69
|
+
model
|
70
|
+
end
|
71
|
+
|
72
|
+
# Recursively mark all models in the link with the register name
|
73
|
+
# This is used to ensure that all links in the model are registered
|
74
|
+
# with the same register name for consistent resolution
|
75
|
+
def mark_model_links_with_register(inspecting_model)
|
76
|
+
return unless inspecting_model.is_a?(Lutaml::Model::Serializable)
|
77
|
+
|
78
|
+
inspecting_model.instance_variable_set("@#{Hal::REGISTER_ID_ATTR_NAME}", @register_name)
|
79
|
+
|
80
|
+
# Recursively process model attributes to mark links with this register
|
81
|
+
inspecting_model.class.attributes.each_pair do |key, config|
|
82
|
+
attr_type = config.type
|
83
|
+
next unless attr_type < Lutaml::Hal::Resource ||
|
84
|
+
attr_type < Lutaml::Hal::Link ||
|
85
|
+
attr_type < Lutaml::Hal::LinkSet
|
86
|
+
|
87
|
+
value = inspecting_model.send(key)
|
88
|
+
next if value.nil?
|
89
|
+
|
90
|
+
# Handle both array and single values with the same logic
|
91
|
+
values = value.is_a?(Array) ? value : [value]
|
92
|
+
values.each { |item| mark_model_links_with_register(item) }
|
93
|
+
end
|
94
|
+
|
95
|
+
inspecting_model
|
63
96
|
end
|
64
97
|
|
65
98
|
private
|
@@ -103,11 +136,15 @@ module Lutaml
|
|
103
136
|
pattern_with_wildcards = pattern.gsub(/\{[^}]+\}/, '*')
|
104
137
|
# Convert * wildcards to regex pattern
|
105
138
|
regex = Regexp.new("^#{pattern_with_wildcards.gsub('*', '[^/]+')}$")
|
106
|
-
regex.match?(url)
|
107
|
-
end
|
108
139
|
|
109
|
-
|
110
|
-
|
140
|
+
Hal.debug_log("pattern_match?: regex: #{regex.inspect}")
|
141
|
+
Hal.debug_log("pattern_match?: href to match #{url}")
|
142
|
+
Hal.debug_log("pattern_match?: pattern to match #{pattern_with_wildcards}")
|
143
|
+
|
144
|
+
matches = regex.match?(url)
|
145
|
+
Hal.debug_log("pattern_match?: matches = #{matches}")
|
146
|
+
|
147
|
+
matches
|
111
148
|
end
|
112
149
|
end
|
113
150
|
end
|
data/lib/lutaml/hal/page.rb
CHANGED
@@ -19,6 +19,19 @@ module Lutaml
|
|
19
19
|
map 'pages', to: :pages
|
20
20
|
map 'total', to: :total
|
21
21
|
end
|
22
|
+
|
23
|
+
def self.inherited(subclass)
|
24
|
+
super
|
25
|
+
|
26
|
+
page_links_symbols = %i[self next prev first last up]
|
27
|
+
subclass_name = subclass.name
|
28
|
+
subclass.class_eval do
|
29
|
+
# Define common page links
|
30
|
+
page_links_symbols.each do |link_symbol|
|
31
|
+
hal_link link_symbol, key: link_symbol.to_s, realize_class: subclass_name
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
22
35
|
end
|
23
36
|
end
|
24
37
|
end
|
data/lib/lutaml/hal/resource.rb
CHANGED
@@ -7,6 +7,10 @@ module Lutaml
|
|
7
7
|
module Hal
|
8
8
|
# Resource class for all HAL resources
|
9
9
|
class Resource < Lutaml::Model::Serializable
|
10
|
+
# This is the model register that has fetched this resource, and
|
11
|
+
# will be used to resolve links unless overriden in resource#realize()
|
12
|
+
attr_accessor Hal::REGISTER_ID_ATTR_NAME.to_sym
|
13
|
+
|
10
14
|
class << self
|
11
15
|
attr_accessor :link_definitions
|
12
16
|
|
@@ -35,20 +39,22 @@ module Lutaml
|
|
35
39
|
# Use the provided "key" as the attribute name
|
36
40
|
attribute_name = attr_key.to_sym
|
37
41
|
|
42
|
+
Hal.debug_log "Defining HAL link for `#{attr_key}` with realize class `#{realize_class}`"
|
43
|
+
|
38
44
|
# Create a dynamic Link subclass name based on "realize_class", the
|
39
45
|
# class to realize for a Link object, if `link_class:` is not provided.
|
40
46
|
link_klass = link_class || create_link_class(realize_class)
|
41
47
|
|
42
48
|
# Create a dynamic LinkSet class if `link_set_class:` is not provided.
|
43
49
|
unless link_set_class
|
44
|
-
link_set_klass = link_set_class ||
|
50
|
+
link_set_klass = link_set_class || get_link_set_class
|
45
51
|
link_set_klass.class_eval do
|
46
52
|
# Declare the corresponding lutaml-model attribute
|
47
53
|
attribute attribute_name, link_klass, collection: collection
|
48
54
|
|
49
55
|
# Define the mapping for the attribute
|
50
56
|
key_value do
|
51
|
-
map
|
57
|
+
map key, to: attribute_name
|
52
58
|
end
|
53
59
|
end
|
54
60
|
end
|
@@ -67,10 +73,10 @@ module Lutaml
|
|
67
73
|
end
|
68
74
|
|
69
75
|
# This method obtains the Links class that holds the Link classes
|
70
|
-
def
|
76
|
+
def get_link_set_class
|
71
77
|
parent_klass_name = name.split('::')[0..-2].join('::')
|
72
78
|
child_klass_name = "#{name.split('::').last}LinkSet"
|
73
|
-
klass_name =
|
79
|
+
klass_name = [parent_klass_name, child_klass_name].join('::')
|
74
80
|
|
75
81
|
raise unless Object.const_defined?(klass_name)
|
76
82
|
|
@@ -84,17 +90,18 @@ module Lutaml
|
|
84
90
|
def create_link_set_class
|
85
91
|
parent_klass_name = name.split('::')[0..-2].join('::')
|
86
92
|
child_klass_name = "#{name.split('::').last}LinkSet"
|
87
|
-
klass_name =
|
93
|
+
klass_name = [parent_klass_name, child_klass_name].join('::')
|
94
|
+
|
95
|
+
Hal.debug_log "Creating link set class #{klass_name}"
|
88
96
|
|
89
97
|
# Check if the LinkSet class is already defined, return if so
|
90
98
|
return Object.const_get(klass_name) if Object.const_defined?(klass_name)
|
91
99
|
|
92
100
|
# Define the LinkSet class dynamically as a normal Lutaml::Model class
|
93
|
-
# since it is not a Resource
|
94
|
-
klass = Class.new(Lutaml::
|
95
|
-
Object.const_get(parent_klass_name)
|
96
|
-
|
97
|
-
end
|
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)
|
98
105
|
|
99
106
|
# Define the LinkSet class with mapping inside the current class
|
100
107
|
class_eval do
|
@@ -112,8 +119,10 @@ module Lutaml
|
|
112
119
|
# This is a Link class that helps us realize the targeted class
|
113
120
|
def create_link_class(realize_class_name)
|
114
121
|
parent_klass_name = name.split('::')[0..-2].join('::')
|
115
|
-
child_klass_name = "#{
|
116
|
-
klass_name =
|
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}"
|
117
126
|
|
118
127
|
return Object.const_get(klass_name) if Object.const_defined?(klass_name)
|
119
128
|
|
@@ -122,9 +131,9 @@ module Lutaml
|
|
122
131
|
# Define the link class with the specified key and class
|
123
132
|
attribute :type, :string, default: realize_class_name
|
124
133
|
end
|
125
|
-
|
126
|
-
|
127
|
-
|
134
|
+
|
135
|
+
parent_klass = !parent_klass_name.empty? ? Object.const_get(parent_klass_name) : Object
|
136
|
+
parent_klass.const_set(child_klass_name, klass)
|
128
137
|
|
129
138
|
klass
|
130
139
|
end
|
data/lib/lutaml/hal/version.rb
CHANGED
data/lib/lutaml/hal.rb
CHANGED
@@ -5,13 +5,20 @@ require 'lutaml/model'
|
|
5
5
|
module Lutaml
|
6
6
|
# HAL implementation for Lutaml
|
7
7
|
module Hal
|
8
|
+
REGISTER_ID_ATTR_NAME = '_global_register_id'
|
9
|
+
|
10
|
+
def self.debug_log(message)
|
11
|
+
puts "[Lutaml::Hal] DEBUG: #{message}" if ENV['DEBUG_API']
|
12
|
+
end
|
8
13
|
end
|
9
14
|
end
|
10
15
|
|
11
16
|
require_relative 'hal/version'
|
12
17
|
require_relative 'hal/errors'
|
13
18
|
require_relative 'hal/link'
|
19
|
+
require_relative 'hal/link_set'
|
14
20
|
require_relative 'hal/resource'
|
15
21
|
require_relative 'hal/page'
|
22
|
+
require_relative 'hal/global_register'
|
16
23
|
require_relative 'hal/model_register'
|
17
24
|
require_relative 'hal/client'
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
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.4
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Ribose Inc.
|
@@ -80,7 +80,9 @@ files:
|
|
80
80
|
- lib/lutaml/hal.rb
|
81
81
|
- lib/lutaml/hal/client.rb
|
82
82
|
- lib/lutaml/hal/errors.rb
|
83
|
+
- lib/lutaml/hal/global_register.rb
|
83
84
|
- lib/lutaml/hal/link.rb
|
85
|
+
- lib/lutaml/hal/link_set.rb
|
84
86
|
- lib/lutaml/hal/model_register.rb
|
85
87
|
- lib/lutaml/hal/page.rb
|
86
88
|
- lib/lutaml/hal/resource.rb
|