lutaml-hal 0.1.7 → 0.1.9

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
@@ -90,7 +134,12 @@ module Lutaml
90
134
 
91
135
  # Handle both array and single values with the same logic
92
136
  values = value.is_a?(Array) ? value : [value]
93
- values.each { |item| mark_model_links_with_register(item) }
137
+ values.each do |item|
138
+ mark_model_links_with_register(item)
139
+
140
+ # If this is a Link, set the parent resource for automatic embedded content detection
141
+ item.parent_resource = inspecting_model if item.is_a?(Lutaml::Hal::Link)
142
+ end
94
143
  end
95
144
 
96
145
  inspecting_model
@@ -98,35 +147,116 @@ module Lutaml
98
147
 
99
148
  private
100
149
 
101
- def interpolate_url(url_template, params)
102
- params.reduce(url_template) do |url, (key, value)|
103
- url.gsub("{#{key}}", value.to_s)
150
+ def process_parameters(parameter_definitions, provided_params)
151
+ result = { path: {}, query: {}, headers: {}, cookies: {} }
152
+
153
+ parameter_definitions.each do |param_def|
154
+ param_name = param_def.name.to_sym
155
+ provided_value = provided_params[param_name]
156
+
157
+ # Check required parameters
158
+ if param_def.required && provided_value.nil?
159
+ raise ArgumentError, "Required parameter '#{param_def.name}' is missing"
160
+ end
161
+
162
+ # Use default value if not provided
163
+ value = provided_value || param_def.default_value
164
+
165
+ # Skip if still nil and not required
166
+ next if value.nil?
167
+
168
+ # Validate parameter value
169
+ unless param_def.validate_value(value)
170
+ raise ArgumentError, "Invalid value for parameter '#{param_def.name}': #{value}"
171
+ end
172
+
173
+ # Store in appropriate category
174
+ case param_def.location
175
+ when :path
176
+ result[:path][param_def.name] = value
177
+ when :query
178
+ result[:query][param_def.name] = value
179
+ when :header
180
+ result[:headers][param_def.name] = value
181
+ when :cookie
182
+ result[:cookies][param_def.name] = value
183
+ end
104
184
  end
185
+
186
+ result
105
187
  end
106
188
 
107
- def build_url_with_query_params(base_url, query_params_template, params)
108
- return base_url unless query_params_template
189
+ def validate_path_parameters(url, parameters)
190
+ # Extract path parameter names from URL template
191
+ url_params = url.scan(/\{([^}]+)\}/).flatten
192
+
193
+ # Find path parameters in parameter definitions
194
+ path_params = parameters.select(&:path_parameter?).map(&:name)
195
+
196
+ # Check that all URL parameters have definitions
197
+ missing_params = url_params - path_params
198
+ unless missing_params.empty?
199
+ raise ArgumentError, "URL contains undefined path parameters: #{missing_params.join(', ')}"
200
+ end
109
201
 
110
- query_params = []
111
- query_params_template.each do |param_name, param_template|
112
- # If the template is like {page}, look for the param in the passed params
113
- if param_template.is_a?(String) && param_template.match?(/\{(.+)\}/)
114
- param_key = param_template.match(/\{(.+)\}/)[1]
115
- query_params << "#{param_name}=#{params[param_key.to_sym]}" if params[param_key.to_sym]
116
- else
117
- # Fixed parameter - always include it
118
- query_params << "#{param_name}=#{param_template}"
202
+ # Check that all path parameter definitions are used in URL
203
+ unused_params = path_params - url_params
204
+ return if unused_params.empty?
205
+
206
+ raise ArgumentError, "Path parameters defined but not used in URL: #{unused_params.join(', ')}"
207
+ end
208
+
209
+ def parameters_match?(params1, params2)
210
+ return true if params1.nil? && params2.nil?
211
+ return false if params1.nil? || params2.nil?
212
+ return false if params1.length != params2.length
213
+
214
+ params1.zip(params2).all? do |p1, p2|
215
+ p1.name == p2.name && p1.location == p2.location
216
+ end
217
+ end
218
+
219
+ def build_url_with_path_params(url_template, path_params)
220
+ path_params.reduce(url_template) do |url, (key, value)|
221
+ url.gsub("{#{key}}", value.to_s)
222
+ end
223
+ end
224
+
225
+ def build_url_with_query_params(base_url, query_params, params = nil)
226
+ # Handle both 2-argument and 3-argument calls for backward compatibility
227
+ if params.nil?
228
+ # 2-argument call: query_params is the final query parameters
229
+ final_query_params = query_params
230
+ else
231
+ # 3-argument call: query_params is template, params contains values
232
+ final_query_params = {}
233
+ query_params.each do |key, template_value|
234
+ if template_value.is_a?(String) && template_value.match?(/\{(\w+)\}/)
235
+ param_name = template_value.match(/\{(\w+)\}/)[1].to_sym
236
+ final_query_params[key] = params[param_name] if params[param_name]
237
+ else
238
+ final_query_params[key] = template_value
239
+ end
119
240
  end
120
241
  end
121
242
 
122
- query_params.any? ? "#{base_url}?#{query_params.join('&')}" : base_url
243
+ return base_url if final_query_params.empty?
244
+
245
+ query_string = final_query_params.map { |key, value| "#{key}=#{CGI.escape(value.to_s)}" }.join('&')
246
+ "#{base_url}?#{query_string}"
247
+ end
248
+
249
+ # Interpolate path parameters in a URL template
250
+ def interpolate_url(url_template, params)
251
+ params.reduce(url_template) do |url, (key, value)|
252
+ url.gsub("{#{key}}", value.to_s)
253
+ end
123
254
  end
124
255
 
125
256
  def find_matching_model_class(href)
126
257
  # Find all matching patterns and select the most specific one (longest pattern)
127
258
  matching_models = @models.values.select do |model_data|
128
- matches = matches_url_with_params?(model_data, href)
129
- matches
259
+ matches_url_with_params?(model_data, href)
130
260
  end
131
261
 
132
262
  return nil if matching_models.empty?
@@ -139,7 +269,7 @@ module Lutaml
139
269
 
140
270
  def matches_url_with_params?(model_data, href)
141
271
  pattern = model_data[:url]
142
- query_params = model_data[:query_params]
272
+ parameters = model_data[:parameters]
143
273
 
144
274
  return false unless pattern && href
145
275
 
@@ -149,7 +279,9 @@ module Lutaml
149
279
  path_match_result = path_matches?(pattern_path, uri.path)
150
280
  return false unless path_match_result
151
281
 
152
- return true unless query_params
282
+ # Check query parameters if any are defined
283
+ query_params = parameters.select(&:query_parameter?)
284
+ return true if query_params.empty?
153
285
 
154
286
  parsed_query = parse_query_params(uri.query)
155
287
  query_params_match?(query_params, parsed_query)
@@ -169,24 +301,23 @@ module Lutaml
169
301
  end
170
302
 
171
303
  def query_params_match?(expected_params, actual_params)
172
- # Query parameters should be optional - if they're template parameters (like {page}),
173
- # they don't need to be present in the actual URL
174
- expected_params.all? do |param_name, param_pattern|
175
- actual_value = actual_params[param_name]
176
-
177
- # If it's a template parameter (like {page}), it's optional
178
- if template_param?(param_pattern)
179
- # Template parameters are always considered matching (they're optional)
180
- true
181
- else
182
- # Non-template parameters must match exactly if present
183
- actual_value == param_pattern.to_s
304
+ # Query parameters should be optional unless marked as required
305
+ expected_params.all? do |param_def|
306
+ actual_value = actual_params[param_def.name]
307
+
308
+ # Required parameters must be present
309
+ if param_def.required
310
+ return false if actual_value.nil?
311
+
312
+ return param_def.validate_value(actual_value)
184
313
  end
185
- end
186
- end
187
314
 
188
- def template_param?(param_pattern)
189
- param_pattern.is_a?(String) && param_pattern.match?(/\{.+\}/)
315
+ # Optional parameters are always considered matching if not present
316
+ return true if actual_value.nil?
317
+
318
+ # If present, they must be valid
319
+ param_def.validate_value(actual_value)
320
+ end
190
321
  end
191
322
 
192
323
  def parse_query_params(query_string)
@@ -194,30 +325,11 @@ module Lutaml
194
325
 
195
326
  query_string.split('&').each_with_object({}) do |param, hash|
196
327
  key, value = param.split('=', 2)
197
- hash[key] = value if key
198
- end
199
- end
200
-
201
- def matches_url?(pattern, href)
202
- return false unless pattern && href
203
-
204
- if href.start_with?('/') && client&.api_url
205
- # Try both with and without the API endpoint prefix
206
- path_pattern = extract_path(pattern)
207
- return pattern_match?(path_pattern, href) ||
208
- pattern_match?(pattern, "#{client.api_url}#{href}")
328
+ hash[key] = CGI.unescape(value) if key && value
209
329
  end
210
-
211
- pattern_match?(pattern, href)
212
- end
213
-
214
- def extract_path(pattern)
215
- return pattern unless client&.api_url && pattern.start_with?(client.api_url)
216
-
217
- pattern.sub(client.api_url, '')
218
330
  end
219
331
 
220
- # Match URL pattern (supports * wildcards and {param} templates)
332
+ # Match URL pattern (supports {param} templates)
221
333
  def pattern_match?(pattern, url)
222
334
  return false unless pattern && url
223
335
 
@@ -13,6 +13,28 @@ module Lutaml
13
13
  # will be used to resolve links unless overriden in resource#realize()
14
14
  attr_accessor Hal::REGISTER_ID_ATTR_NAME.to_sym
15
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
+
16
38
  class << self
17
39
  attr_accessor :link_definitions
18
40
 
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Lutaml
4
4
  module Hal
5
- VERSION = '0.1.7'
5
+ VERSION = '0.1.9'
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.7
4
+ version: 0.1.9
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-04 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,6 +79,8 @@ 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