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.
- checksums.yaml +4 -4
- data/README.adoc +115 -1047
- 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 +56 -1
- data/lib/lutaml/hal/link_class_factory.rb +100 -0
- data/lib/lutaml/hal/link_set_class_factory.rb +92 -0
- data/lib/lutaml/hal/model_register.rb +188 -62
- data/lib/lutaml/hal/page.rb +3 -0
- data/lib/lutaml/hal/resource.rb +61 -55
- data/lib/lutaml/hal/type_resolver.rb +59 -0
- data/lib/lutaml/hal/version.rb +1 -1
- data/lib/lutaml/hal.rb +1 -0
- metadata +7 -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
|
@@ -98,46 +142,144 @@ module Lutaml
|
|
98
142
|
|
99
143
|
private
|
100
144
|
|
101
|
-
def
|
102
|
-
|
103
|
-
|
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
|
108
|
-
|
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
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
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
|
-
|
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
|
-
|
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
|
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
|
-
|
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
|
-
|
138
|
-
return
|
274
|
+
path_match_result = path_matches?(pattern_path, uri.path)
|
275
|
+
return false unless path_match_result
|
139
276
|
|
140
|
-
|
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
|
-
|
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
|
-
|
163
|
-
|
164
|
-
|
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
|
-
|
167
|
-
|
168
|
-
|
303
|
+
# Required parameters must be present
|
304
|
+
if param_def.required
|
305
|
+
return false if actual_value.nil?
|
169
306
|
|
170
|
-
|
171
|
-
|
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
|
-
|
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
|
209
|
-
|
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}")
|
data/lib/lutaml/hal/page.rb
CHANGED
@@ -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
|
data/lib/lutaml/hal/resource.rb
CHANGED
@@ -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(
|
91
|
+
link_klass = link_class || create_link_class(realize_class_name)
|
47
92
|
|
48
|
-
#
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
#
|
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
|
-
|
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
|
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.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-
|
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:
|