lutaml-hal 0.1.6 → 0.1.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,10 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'cgi'
3
4
  require_relative 'errors'
5
+ require_relative 'endpoint_configuration'
4
6
 
5
7
  module Lutaml
6
8
  module Hal
7
- # Register to map URL patterns to model classes
9
+ # Register to map URL patterns to model classes with EndpointParameter support
8
10
  class ModelRegister
9
11
  attr_accessor :models, :client, :register_name
10
12
 
@@ -15,12 +17,22 @@ module Lutaml
15
17
  @models = {}
16
18
  end
17
19
 
18
- # Register a model with its base URL pattern
19
- def add_endpoint(id:, type:, url:, model:, query_params: nil)
20
+ # Register a model with its URL pattern and parameters
21
+ def add_endpoint(id:, type:, url:, model:, parameters: [])
20
22
  @models ||= {}
21
23
 
22
24
  raise "Model with ID #{id} already registered" if @models[id]
23
- if @models.values.any? { |m| m[:url] == url && m[:type] == type && m[:query_params] == query_params }
25
+
26
+ # Validate all parameters
27
+ parameters.each(&:validate!)
28
+
29
+ # Ensure path parameters in URL have corresponding parameter definitions
30
+ validate_path_parameters(url, parameters)
31
+
32
+ # Check for duplicate endpoints
33
+ if @models.values.any? do |m|
34
+ m[:url] == url && m[:type] == type && parameters_match?(m[:parameters], parameters)
35
+ end
24
36
  raise "Duplicate URL pattern #{url} for type #{type}"
25
37
  end
26
38
 
@@ -29,20 +41,52 @@ module Lutaml
29
41
  type: type,
30
42
  url: url,
31
43
  model: model,
32
- query_params: query_params
44
+ parameters: parameters
33
45
  }
34
46
  end
35
47
 
48
+ # Register an endpoint using block configuration syntax
49
+ def register_endpoint(id, model, type: :index)
50
+ config = EndpointConfiguration.new
51
+ yield(config) if block_given?
52
+
53
+ raise ArgumentError, 'Endpoint path must be configured' unless config.endpoint_path
54
+
55
+ add_endpoint(
56
+ id: id,
57
+ type: type,
58
+ url: config.endpoint_path,
59
+ model: model,
60
+ parameters: config.parameters || []
61
+ )
62
+ end
63
+
36
64
  # Resolve and cast data to the appropriate model based on URL
37
65
  def fetch(endpoint_id, **params)
38
66
  endpoint = @models[endpoint_id] || raise("Unknown endpoint: #{endpoint_id}")
39
67
  raise 'Client not configured' unless client
40
68
 
41
- url = interpolate_url(endpoint[:url], params)
42
- response = client.get(build_url_with_query_params(url, endpoint[:query_params], params))
69
+ # Process parameters through EndpointParameter objects
70
+ processed_params = process_parameters(endpoint[:parameters], params)
71
+
72
+ # Build URL with path parameters
73
+ url = build_url_with_path_params(endpoint[:url], processed_params[:path])
74
+
75
+ # Add query parameters
76
+ final_url = build_url_with_query_params(url, processed_params[:query])
77
+
78
+ # Make request with headers
79
+ response = if processed_params[:headers].any?
80
+ client.get_with_headers(final_url, processed_params[:headers])
81
+ else
82
+ client.get(final_url)
83
+ end
43
84
 
44
85
  realized_model = endpoint[:model].from_json(response.to_json)
45
86
 
87
+ # Store embedded data for later resolution
88
+ realized_model.instance_variable_set(:@_embedded, response['_embedded']) if response['_embedded']
89
+
46
90
  mark_model_links_with_register(realized_model)
47
91
  realized_model
48
92
  end
@@ -98,46 +142,144 @@ module Lutaml
98
142
 
99
143
  private
100
144
 
101
- def interpolate_url(url_template, params)
102
- params.reduce(url_template) do |url, (key, value)|
103
- url.gsub("{#{key}}", value.to_s)
145
+ def process_parameters(parameter_definitions, provided_params)
146
+ result = { path: {}, query: {}, headers: {}, cookies: {} }
147
+
148
+ parameter_definitions.each do |param_def|
149
+ param_name = param_def.name.to_sym
150
+ provided_value = provided_params[param_name]
151
+
152
+ # Check required parameters
153
+ if param_def.required && provided_value.nil?
154
+ raise ArgumentError, "Required parameter '#{param_def.name}' is missing"
155
+ end
156
+
157
+ # Use default value if not provided
158
+ value = provided_value || param_def.default_value
159
+
160
+ # Skip if still nil and not required
161
+ next if value.nil?
162
+
163
+ # Validate parameter value
164
+ unless param_def.validate_value(value)
165
+ raise ArgumentError, "Invalid value for parameter '#{param_def.name}': #{value}"
166
+ end
167
+
168
+ # Store in appropriate category
169
+ case param_def.location
170
+ when :path
171
+ result[:path][param_def.name] = value
172
+ when :query
173
+ result[:query][param_def.name] = value
174
+ when :header
175
+ result[:headers][param_def.name] = value
176
+ when :cookie
177
+ result[:cookies][param_def.name] = value
178
+ end
104
179
  end
180
+
181
+ result
105
182
  end
106
183
 
107
- def build_url_with_query_params(base_url, query_params_template, params)
108
- return base_url unless query_params_template
184
+ def validate_path_parameters(url, parameters)
185
+ # Extract path parameter names from URL template
186
+ url_params = url.scan(/\{([^}]+)\}/).flatten
187
+
188
+ # Find path parameters in parameter definitions
189
+ path_params = parameters.select(&:path_parameter?).map(&:name)
190
+
191
+ # Check that all URL parameters have definitions
192
+ missing_params = url_params - path_params
193
+ unless missing_params.empty?
194
+ raise ArgumentError, "URL contains undefined path parameters: #{missing_params.join(', ')}"
195
+ end
196
+
197
+ # Check that all path parameter definitions are used in URL
198
+ unused_params = path_params - url_params
199
+ return if unused_params.empty?
109
200
 
110
- query_params = []
111
- query_params_template.each do |param_name, param_template|
112
- # If the template is like {page}, look for the param in the passed params
113
- if param_template.is_a?(String) && param_template.match?(/\{(.+)\}/)
114
- param_key = param_template.match(/\{(.+)\}/)[1]
115
- query_params << "#{param_name}=#{params[param_key.to_sym]}" if params[param_key.to_sym]
201
+ raise ArgumentError, "Path parameters defined but not used in URL: #{unused_params.join(', ')}"
202
+ end
203
+
204
+ def parameters_match?(params1, params2)
205
+ return true if params1.nil? && params2.nil?
206
+ return false if params1.nil? || params2.nil?
207
+ return false if params1.length != params2.length
208
+
209
+ params1.zip(params2).all? do |p1, p2|
210
+ p1.name == p2.name && p1.location == p2.location
211
+ end
212
+ end
213
+
214
+ def build_url_with_path_params(url_template, path_params)
215
+ path_params.reduce(url_template) do |url, (key, value)|
216
+ url.gsub("{#{key}}", value.to_s)
217
+ end
218
+ end
219
+
220
+ def build_url_with_query_params(base_url, query_params, params = nil)
221
+ # Handle both 2-argument and 3-argument calls for backward compatibility
222
+ if params.nil?
223
+ # 2-argument call: query_params is the final query parameters
224
+ final_query_params = query_params
225
+ else
226
+ # 3-argument call: query_params is template, params contains values
227
+ final_query_params = {}
228
+ query_params.each do |key, template_value|
229
+ if template_value.is_a?(String) && template_value.match?(/\{(\w+)\}/)
230
+ param_name = template_value.match(/\{(\w+)\}/)[1].to_sym
231
+ final_query_params[key] = params[param_name] if params[param_name]
232
+ else
233
+ final_query_params[key] = template_value
234
+ end
116
235
  end
117
236
  end
118
237
 
119
- query_params.any? ? "#{base_url}?#{query_params.join('&')}" : base_url
238
+ return base_url if final_query_params.empty?
239
+
240
+ query_string = final_query_params.map { |key, value| "#{key}=#{CGI.escape(value.to_s)}" }.join('&')
241
+ "#{base_url}?#{query_string}"
242
+ end
243
+
244
+ # Interpolate path parameters in a URL template
245
+ def interpolate_url(url_template, params)
246
+ params.reduce(url_template) do |url, (key, value)|
247
+ url.gsub("{#{key}}", value.to_s)
248
+ end
120
249
  end
121
250
 
122
251
  def find_matching_model_class(href)
123
- @models.values.find do |model_data|
252
+ # Find all matching patterns and select the most specific one (longest pattern)
253
+ matching_models = @models.values.select do |model_data|
124
254
  matches_url_with_params?(model_data, href)
125
- end&.[](:model)
255
+ end
256
+
257
+ return nil if matching_models.empty?
258
+
259
+ # Sort by pattern length (descending) to get the most specific match first
260
+ result = matching_models.max_by { |model_data| model_data[:url].length }
261
+
262
+ result[:model]
126
263
  end
127
264
 
128
265
  def matches_url_with_params?(model_data, href)
129
266
  pattern = model_data[:url]
130
- query_params = model_data[:query_params]
267
+ parameters = model_data[:parameters]
131
268
 
132
269
  return false unless pattern && href
133
270
 
134
271
  uri = parse_href_uri(href)
135
272
  pattern_path = extract_pattern_path(pattern)
136
273
 
137
- return false unless path_matches?(pattern_path, uri.path)
138
- return true unless query_params
274
+ path_match_result = path_matches?(pattern_path, uri.path)
275
+ return false unless path_match_result
139
276
 
140
- query_params_match?(query_params, parse_query_params(uri.query))
277
+ # Check query parameters if any are defined
278
+ query_params = parameters.select(&:query_parameter?)
279
+ return true if query_params.empty?
280
+
281
+ parsed_query = parse_query_params(uri.query)
282
+ query_params_match?(query_params, parsed_query)
141
283
  end
142
284
 
143
285
  def parse_href_uri(href)
@@ -150,25 +292,27 @@ module Lutaml
150
292
  end
151
293
 
152
294
  def path_matches?(pattern_path, href_path)
153
- if href_path.start_with?('/') && client&.api_url
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
295
+ pattern_match?(pattern_path, href_path)
159
296
  end
160
297
 
161
298
  def query_params_match?(expected_params, actual_params)
162
- expected_params.all? do |param_name, param_pattern|
163
- actual_value = actual_params[param_name]
164
- next false unless actual_value
299
+ # Query parameters should be optional unless marked as required
300
+ expected_params.all? do |param_def|
301
+ actual_value = actual_params[param_def.name]
165
302
 
166
- template_param?(param_pattern) || actual_value == param_pattern.to_s
167
- end
168
- end
303
+ # Required parameters must be present
304
+ if param_def.required
305
+ return false if actual_value.nil?
169
306
 
170
- def template_param?(param_pattern)
171
- param_pattern.is_a?(String) && param_pattern.match?(/\{.+\}/)
307
+ return param_def.validate_value(actual_value)
308
+ end
309
+
310
+ # Optional parameters are always considered matching if not present
311
+ return true if actual_value.nil?
312
+
313
+ # If present, they must be valid
314
+ param_def.validate_value(actual_value)
315
+ end
172
316
  end
173
317
 
174
318
  def parse_query_params(query_string)
@@ -176,37 +320,19 @@ module Lutaml
176
320
 
177
321
  query_string.split('&').each_with_object({}) do |param, hash|
178
322
  key, value = param.split('=', 2)
179
- hash[key] = value if key
323
+ hash[key] = CGI.unescape(value) if key && value
180
324
  end
181
325
  end
182
326
 
183
- def matches_url?(pattern, href)
184
- return false unless pattern && href
185
-
186
- if href.start_with?('/') && client&.api_url
187
- # Try both with and without the API endpoint prefix
188
- path_pattern = extract_path(pattern)
189
- return pattern_match?(path_pattern, href) ||
190
- pattern_match?(pattern, "#{client.api_url}#{href}")
191
- end
192
-
193
- pattern_match?(pattern, href)
194
- end
195
-
196
- def extract_path(pattern)
197
- return pattern unless client&.api_url && pattern.start_with?(client.api_url)
198
-
199
- pattern.sub(client.api_url, '')
200
- end
201
-
202
- # Match URL pattern (supports * wildcards and {param} templates)
327
+ # Match URL pattern (supports {param} templates)
203
328
  def pattern_match?(pattern, url)
204
329
  return false unless pattern && url
205
330
 
206
331
  # Convert {param} to wildcards for matching
207
332
  pattern_with_wildcards = pattern.gsub(/\{[^}]+\}/, '*')
208
- # Convert * wildcards to regex pattern - use .+ instead of [^/]+ to match query parameters
209
- regex = Regexp.new("^#{pattern_with_wildcards.gsub('*', '.+')}$")
333
+ # Convert * wildcards to regex pattern - use [^/]+ to match path segments, not across slashes
334
+ # This ensures that {param} only matches a single path segment
335
+ regex = Regexp.new("^#{pattern_with_wildcards.gsub('*', '[^/]+')}$")
210
336
 
211
337
  Hal.debug_log("pattern_match?: regex: #{regex.inspect}")
212
338
  Hal.debug_log("pattern_match?: href to match #{url}")
@@ -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
@@ -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
@@ -11,6 +13,28 @@ module Lutaml
11
13
  # will be used to resolve links unless overriden in resource#realize()
12
14
  attr_accessor Hal::REGISTER_ID_ATTR_NAME.to_sym
13
15
 
16
+ # Access embedded data if available
17
+ def embedded_data
18
+ @_embedded
19
+ end
20
+
21
+ # Check if embedded data exists for a given key
22
+ def has_embedded?(key)
23
+ embedded_data&.key?(key.to_s)
24
+ end
25
+
26
+ # Get embedded content for a specific key
27
+ def get_embedded(key)
28
+ embedded_data&.[](key.to_s)
29
+ end
30
+
31
+ # Create a resource instance from embedded JSON data
32
+ def self.from_embedded(json_data, register_name = nil)
33
+ instance = from_json(json_data.to_json)
34
+ instance.instance_variable_set("@#{Hal::REGISTER_ID_ATTR_NAME}", register_name) if register_name
35
+ instance
36
+ end
37
+
14
38
  class << self
15
39
  attr_accessor :link_definitions
16
40
 
@@ -18,7 +42,6 @@ module Lutaml
18
42
  def inherited(subclass)
19
43
  super
20
44
  subclass.class_eval do
21
- create_link_set_class
22
45
  init_links_definition
23
46
  end
24
47
  end
@@ -36,21 +59,47 @@ module Lutaml
36
59
  link_set_class: nil,
37
60
  collection: false,
38
61
  type: :link)
62
+ # Validate required parameters
63
+ raise ArgumentError, 'realize_class parameter is required' if realize_class.nil?
64
+
39
65
  # Use the provided "key" as the attribute name
40
66
  attribute_name = attr_key.to_sym
41
67
 
42
68
  Hal.debug_log "Defining HAL link for `#{attr_key}` with realize class `#{realize_class}`"
43
69
 
70
+ # Normalize realize_class to a string for consistent handling
71
+ # Support both Class objects (when autoload is available) and strings (for delayed interpretation)
72
+ realize_class_name = case realize_class
73
+ when Class
74
+ realize_class.name.split('::').last # Use simple name from actual class
75
+ when String
76
+ realize_class # Use string as-is for lazy resolution
77
+ else
78
+ raise ArgumentError,
79
+ "realize_class must be a Class or String, got #{realize_class.class}"
80
+ end
81
+
82
+ # Create a dynamic LinkSet class if `link_set_class:` is not provided.
83
+ # This must happen BEFORE creating the Link class to ensure proper order
84
+ link_set_klass = link_set_class || create_link_set_class
85
+
86
+ # Ensure it was actually created
87
+ raise 'Failed to create LinkSet class' if link_set_klass.nil?
88
+
44
89
  # Create a dynamic Link subclass name based on "realize_class", the
45
90
  # class to realize for a Link object, if `link_class:` is not provided.
46
- link_klass = link_class || create_link_class(realize_class)
91
+ link_klass = link_class || create_link_class(realize_class_name)
47
92
 
48
- # Create a dynamic LinkSet class if `link_set_class:` is not provided.
93
+ # Now add the link to the LinkSet class
49
94
  unless link_set_class
50
- link_set_klass = link_set_class || get_link_set_class
51
95
  link_set_klass.class_eval do
52
96
  # Declare the corresponding lutaml-model attribute
53
- attribute attribute_name, link_klass, collection: collection
97
+ # Pass collection parameter correctly to the attribute definition
98
+ if collection
99
+ attribute attribute_name, link_klass, collection: true
100
+ else
101
+ attribute attribute_name, link_klass
102
+ end
54
103
 
55
104
  # Define the mapping for the attribute
56
105
  key_value do
@@ -73,69 +122,26 @@ module Lutaml
73
122
  end
74
123
 
75
124
  # This method obtains the Links class that holds the Link classes
125
+ # Delegates to LinkSetClassFactory for simplified implementation
76
126
  def get_link_set_class
77
- parent_klass_name = name.split('::')[0..-2].join('::')
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)
127
+ create_link_set_class
84
128
  end
85
129
 
86
- private
87
-
88
130
  # The "links" class holds the `_links` object which contains
89
131
  # the resource-linked Link classes
132
+ # Delegates to LinkSetClassFactory for simplified implementation
90
133
  def create_link_set_class
91
- parent_klass_name = name.split('::')[0..-2].join('::')
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
134
+ LinkSetClassFactory.create_for(self)
113
135
  end
114
136
 
115
137
  def init_links_definition
116
138
  @link_definitions = {}
117
139
  end
118
140
 
119
- # This is a Link class that helps us realize the targeted class
141
+ # Creates a Link class that helps us realize the targeted class
142
+ # Delegates to LinkClassFactory for simplified implementation
120
143
  def create_link_class(realize_class_name)
121
- parent_klass_name = name.split('::')[0..-2].join('::')
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
144
+ LinkClassFactory.create_for(self, realize_class_name)
139
145
  end
140
146
  end
141
147
  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
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Lutaml
4
4
  module Hal
5
- VERSION = '0.1.6'
5
+ VERSION = '0.1.8'
6
6
  end
7
7
  end
data/lib/lutaml/hal.rb CHANGED
@@ -15,6 +15,7 @@ end
15
15
 
16
16
  require_relative 'hal/version'
17
17
  require_relative 'hal/errors'
18
+ require_relative 'hal/endpoint_parameter'
18
19
  require_relative 'hal/link'
19
20
  require_relative 'hal/link_set'
20
21
  require_relative 'hal/resource'
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.6
4
+ version: 0.1.8
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-03 00:00:00.000000000 Z
11
+ date: 2025-07-08 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: faraday
@@ -79,13 +79,18 @@ files:
79
79
  - lib/lutaml-hal.rb
80
80
  - lib/lutaml/hal.rb
81
81
  - lib/lutaml/hal/client.rb
82
+ - lib/lutaml/hal/endpoint_configuration.rb
83
+ - lib/lutaml/hal/endpoint_parameter.rb
82
84
  - lib/lutaml/hal/errors.rb
83
85
  - lib/lutaml/hal/global_register.rb
84
86
  - lib/lutaml/hal/link.rb
87
+ - lib/lutaml/hal/link_class_factory.rb
85
88
  - lib/lutaml/hal/link_set.rb
89
+ - lib/lutaml/hal/link_set_class_factory.rb
86
90
  - lib/lutaml/hal/model_register.rb
87
91
  - lib/lutaml/hal/page.rb
88
92
  - lib/lutaml/hal/resource.rb
93
+ - lib/lutaml/hal/type_resolver.rb
89
94
  - lib/lutaml/hal/version.rb
90
95
  homepage: https://github.com/lutaml/lutaml-hal
91
96
  licenses: