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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e5a918f01a4197929abfdbbdd1e061ecff5d37592ed0c880024ec11f6abe2943
4
- data.tar.gz: 032f6ba78ced7adefc4b6a9ce9c2c76acc25eba6b9d3d96398f3f09d330d312b
3
+ metadata.gz: b1006ba9c57ff945fa51c368b1ca12971d83fc47f668513a962da76811b6cc91
4
+ data.tar.gz: 4bc4bc6697181d9512604736ba42a6d4ce16d67d235ffd5ef79b481e09ca8243
5
5
  SHA512:
6
- metadata.gz: '0802c9c05fbb34fc5a8d53a023cdd1cfda2291305b597387a5cc7afc665c9aa7eac542f03840cac494389c49442a030e79e7fdcf2169d992747e7a707fc4d9fa'
7
- data.tar.gz: 7ecabcdc778455d9d4fee7b9708c7754891ab64e3da5edb6b88927cc687806b3c5863dde63b89aa785cdcf8174c431b58fa862e0a6443e456faad13fcced0faf
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::Serialization`, which provides data
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
- Syntax:
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
- The `Lutaml::Hal::Page` class is used to handle pagination in HAL APIs. The
794
- `Page` class itself is implemented as a `Resource`, so you can use the same
795
- methods to access the page's attributes and links.
912
+ # Without a GlobalRegister
913
+ product_2_related_1 = product_2.links.related.first.realize(register)
796
914
 
797
- The `Page` class by default supports the following attributes:
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
- class MyPage < Lutaml::Hal::Page
809
- # These are typical links given for page objects
810
- hal_link :self, key: 'self', realize_class: 'MyPage'
811
- hal_link :prev, key: 'prev', realize_class: 'MyPage'
812
- hal_link :next, key: 'next', realize_class: 'MyPage'
813
- hal_link :first, key: 'first', realize_class: 'MyPage'
814
- hal_link :last, key: 'last', realize_class: 'MyPage'
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
- Where,
921
+ === Handling HAL pages / pagination
826
922
 
827
- `MyPage`:: The class of the page that will be fetched from the API. The class
828
- must inherit from `Lutaml::Hal::Page`.
829
- `register`:: The instance of `ModelRegister`.
830
- `id`:: The ID of the pagination endpoint to be registered in the `ModelRegister`.
831
- `url`:: The URL of the pagination endpoint.
832
- `model`:: The class of the page that will be fetched from the API.
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 MyPage < Lutaml::Hal::Page
843
- hal_link :self, key: 'self', realize_class: 'MyPage'
844
- hal_link :prev, key: 'prev', realize_class: 'MyPage'
845
- hal_link :next, key: 'next', realize_class: 'MyPage'
846
- hal_link :first, key: 'first', realize_class: 'MyPage'
847
- hal_link :last, key: 'last', realize_class: 'MyPage'
937
+ class ResourceIndex < Lutaml::Hal::Page
938
+ # No attribute definition necessary
848
939
  end
849
940
 
850
941
  register.add_endpoint(
851
- id: :my_pages,
942
+ id: :resource_index,
852
943
  type: :index,
853
- url: '/my_pages',
854
- model: MyPage
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(:my_pages)
863
- # => client.get('/my_pages')
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": { "href": "/my_pages" },
871
- # "next": { "href": "/my_pages/2" },
872
- # "last": { "href": "/my_pages/9" }
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
- # => #<MyPage page: 1, pages: 10, limit: 10, total: 100,
877
- # links: #<MyPageLinks self: <MyPageLink href: "/my_pages">,
878
- # next: <MyPageLink href: "/my_pages/2">,
879
- # last: <MyPageLink href: "/my_pages/9">>>
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
- # => client.get('/my_pages/2')
882
- # => #<MyPage page: 2, pages: 10, limit: 10, total: 100,
883
- # links: #<MyPageLinks self: <MyPageLink href: "/my_pages/2">,
884
- # prev: <MyPageLink href: "/my_pages/1">,
885
- # next: <MyPageLink href: "/my_pages/3">,
886
- # first: <MyPageLink href: "/my_pages/1">,
887
- # last: <MyPageLink href: "/my_pages/9">>>
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
 
@@ -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.sub(%r{^#{@api_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
- debug_log(response, url) if @debug
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 debug_log(response, url)
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}"
@@ -8,5 +8,6 @@ module Lutaml
8
8
  class BadRequestError < Error; end
9
9
  class ServerError < Error; end
10
10
  class LinkResolutionError < Error; end
11
+ class ParsingError < Error; end
11
12
  end
12
13
  end
@@ -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
@@ -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
- def realize(register)
21
- register.resolve_and_cast(href)
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 more content into the resource
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
- def debug_log(message)
110
- puts "DEBUG: #{message}" if ENV['DEBUG_API']
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
@@ -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
@@ -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 || get_links_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 attr_key, to: attribute_name
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 get_links_class
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 = "#{parent_klass_name}::#{child_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 = "#{parent_klass_name}::#{child_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::Model::Serializable)
95
- Object.const_get(parent_klass_name).tap do |parent_klass|
96
- parent_klass.const_set(child_klass_name, klass)
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 = "#{name.split('::').last}Link"
116
- klass_name = "#{parent_klass_name}::#{child_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
- Object.const_get(parent_klass_name).tap do |parent_klass|
126
- parent_klass.const_set(child_klass_name, klass)
127
- 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)
128
137
 
129
138
  klass
130
139
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Lutaml
4
4
  module Hal
5
- VERSION = '0.1.2'
5
+ VERSION = '0.1.4'
6
6
  end
7
7
  end
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.2
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