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.
- checksums.yaml +4 -4
- data/README.adoc +115 -1145
- data/lib/lutaml/hal/client.rb +24 -0
- data/lib/lutaml/hal/endpoint_configuration.rb +50 -0
- data/lib/lutaml/hal/endpoint_parameter.rb +188 -0
- data/lib/lutaml/hal/global_register.rb +4 -0
- data/lib/lutaml/hal/link.rb +62 -1
- data/lib/lutaml/hal/model_register.rb +176 -64
- data/lib/lutaml/hal/resource.rb +22 -0
- data/lib/lutaml/hal/version.rb +1 -1
- data/lib/lutaml/hal.rb +1 -0
- metadata +4 -2
@@ -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
|
19
|
-
def add_endpoint(id:, type:, url:, model:,
|
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
|
-
|
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
|
-
|
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
|
-
|
42
|
-
|
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
|
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
|
102
|
-
|
103
|
-
|
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
|
108
|
-
|
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
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
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
|
-
|
189
|
-
|
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
|
332
|
+
# Match URL pattern (supports {param} templates)
|
221
333
|
def pattern_match?(pattern, url)
|
222
334
|
return false unless pattern && url
|
223
335
|
|
data/lib/lutaml/hal/resource.rb
CHANGED
@@ -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
|
|
data/lib/lutaml/hal/version.rb
CHANGED
data/lib/lutaml/hal.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: lutaml-hal
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.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-
|
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
|