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.
@@ -57,6 +57,30 @@ module Lutaml
57
57
  raise LinkResolutionError, "Resource not found: #{e.message}"
58
58
  end
59
59
 
60
+ # Make a GET request with custom headers
61
+ def get_with_headers(url, headers = {})
62
+ cache_key = "#{url}:#{headers.to_json}"
63
+
64
+ return @cache[cache_key] if @cache_enabled && @cache.key?(cache_key)
65
+
66
+ @last_response = @connection.get(url) do |req|
67
+ headers.each { |key, value| req.headers[key] = value }
68
+ end
69
+
70
+ response = handle_response(@last_response, url)
71
+
72
+ @cache[cache_key] = response if @cache_enabled
73
+ response
74
+ rescue Faraday::ConnectionFailed => e
75
+ raise ConnectionError, "Connection failed: #{e.message}"
76
+ rescue Faraday::TimeoutError => e
77
+ raise TimeoutError, "Request timed out: #{e.message}"
78
+ rescue Faraday::ParsingError => e
79
+ raise ParsingError, "Response parsing error: #{e.message}"
80
+ rescue Faraday::Adapter::Test::Stubs::NotFound => e
81
+ raise LinkResolutionError, "Resource not found: #{e.message}"
82
+ end
83
+
60
84
  private
61
85
 
62
86
  def create_connection
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lutaml
4
+ module Hal
5
+ # Configuration object for endpoint registration with EndpointParameter support
6
+ class EndpointConfiguration
7
+ attr_reader :endpoint_path, :parameters
8
+
9
+ def initialize
10
+ @endpoint_path = nil
11
+ @parameters = []
12
+ end
13
+
14
+ # Set the endpoint path
15
+ def path(path_string)
16
+ @endpoint_path = path_string
17
+ end
18
+
19
+ # Add a parameter to this endpoint
20
+ def add_parameter(parameter)
21
+ @parameters << parameter
22
+ end
23
+
24
+ # Add multiple parameters
25
+ def parameters=(param_list)
26
+ @parameters = param_list || []
27
+ end
28
+
29
+ # DSL method to add path parameter
30
+ def path_parameter(name, **options)
31
+ add_parameter(EndpointParameter.path(name, **options))
32
+ end
33
+
34
+ # DSL method to add query parameter
35
+ def query_parameter(name, **options)
36
+ add_parameter(EndpointParameter.query(name, **options))
37
+ end
38
+
39
+ # DSL method to add header parameter
40
+ def header_parameter(name, **options)
41
+ add_parameter(EndpointParameter.header(name, **options))
42
+ end
43
+
44
+ # DSL method to add cookie parameter
45
+ def cookie_parameter(name, **options)
46
+ add_parameter(EndpointParameter.cookie(name, **options))
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,188 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lutaml
4
+ module Hal
5
+ # OpenAPI-inspired parameter definition for HAL endpoints
6
+ class EndpointParameter
7
+ attr_reader :name, :location, :required, :schema, :description, :example
8
+
9
+ def initialize(name:, in:, required: nil, schema: {}, description: nil, example: nil)
10
+ @name = name.to_s
11
+ @location = validate_location(binding.local_variable_get(:in))
12
+ @required = determine_required(required, @location)
13
+ @schema = Schema.new(schema)
14
+ @description = description
15
+ @example = example
16
+
17
+ validate!
18
+ end
19
+
20
+ # Convenience constructors for different parameter types
21
+ def self.path(name, **options)
22
+ new(name: name, in: :path, **options)
23
+ end
24
+
25
+ def self.query(name, **options)
26
+ new(name: name, in: :query, **options)
27
+ end
28
+
29
+ def self.header(name, **options)
30
+ new(name: name, in: :header, **options)
31
+ end
32
+
33
+ def self.cookie(name, **options)
34
+ new(name: name, in: :cookie, **options)
35
+ end
36
+
37
+ # Parameter type checks
38
+ def path_parameter?
39
+ @location == :path
40
+ end
41
+
42
+ def query_parameter?
43
+ @location == :query
44
+ end
45
+
46
+ def header_parameter?
47
+ @location == :header
48
+ end
49
+
50
+ def cookie_parameter?
51
+ @location == :cookie
52
+ end
53
+
54
+ # Validate a parameter value against this parameter's schema
55
+ def validate_value(value)
56
+ @schema.validate(value)
57
+ end
58
+
59
+ # Get the default value for this parameter
60
+ def default_value
61
+ @schema.default
62
+ end
63
+
64
+ def validate!
65
+ # Path parameters must be required
66
+ raise ArgumentError, 'Path parameters must be required' if path_parameter? && !@required
67
+
68
+ @schema.validate_definition!
69
+ end
70
+
71
+ private
72
+
73
+ def validate_location(location)
74
+ valid_locations = %i[path query header cookie]
75
+ unless valid_locations.include?(location)
76
+ raise ArgumentError, "Invalid parameter location: #{location}. Must be one of: #{valid_locations.join(', ')}"
77
+ end
78
+
79
+ location
80
+ end
81
+
82
+ def determine_required(required, location)
83
+ return true if location == :path # Path parameters are always required
84
+
85
+ required.nil? ? false : required
86
+ end
87
+
88
+ # Schema validation and type checking
89
+ class Schema
90
+ attr_reader :type, :format, :enum, :minimum, :maximum, :min_length, :max_length,
91
+ :pattern, :items, :default, :nullable
92
+
93
+ def initialize(definition = {})
94
+ @type = definition[:type] || :string
95
+ @format = definition[:format]
96
+ @enum = definition[:enum]
97
+ @minimum = definition[:minimum]
98
+ @maximum = definition[:maximum]
99
+ @min_length = definition[:min_length]
100
+ @max_length = definition[:max_length]
101
+ @pattern = definition[:pattern]
102
+ @items = definition[:items]
103
+ @default = definition[:default]
104
+ @nullable = definition[:nullable] || false
105
+ end
106
+
107
+ def validate(value)
108
+ return true if value.nil? && @nullable
109
+ return false if value.nil? && !@nullable
110
+
111
+ case @type
112
+ when :string
113
+ validate_string(value)
114
+ when :integer
115
+ validate_integer(value)
116
+ when :number
117
+ validate_number(value)
118
+ when :boolean
119
+ validate_boolean(value)
120
+ when :array
121
+ validate_array(value)
122
+ else
123
+ true
124
+ end
125
+ end
126
+
127
+ def validate_definition!
128
+ valid_types = %i[string integer number boolean array object]
129
+ raise ArgumentError, "Invalid schema type: #{@type}" unless valid_types.include?(@type)
130
+
131
+ return unless @type == :array && @items.nil?
132
+
133
+ raise ArgumentError, "Array type requires 'items' definition"
134
+ end
135
+
136
+ private
137
+
138
+ def validate_string(value)
139
+ return false unless value.is_a?(String)
140
+ return false if @min_length && value.length < @min_length
141
+ return false if @max_length && value.length > @max_length
142
+ return false if @pattern && !value.match?(@pattern)
143
+ return false if @enum && !@enum.include?(value)
144
+
145
+ true
146
+ end
147
+
148
+ def validate_integer(value)
149
+ int_value = value.is_a?(String) ? value.to_i : value
150
+ return false unless int_value.is_a?(Integer)
151
+ return false if @minimum && int_value < @minimum
152
+ return false if @maximum && int_value > @maximum
153
+ return false if @enum && !@enum.include?(int_value)
154
+
155
+ true
156
+ end
157
+
158
+ def validate_number(value)
159
+ num_value = value.is_a?(String) ? value.to_f : value
160
+ return false unless num_value.is_a?(Numeric)
161
+ return false if @minimum && num_value < @minimum
162
+ return false if @maximum && num_value > @maximum
163
+ return false if @enum && !@enum.include?(num_value)
164
+
165
+ true
166
+ end
167
+
168
+ def validate_boolean(value)
169
+ [true, false, 'true', 'false', '1', '0'].include?(value)
170
+ end
171
+
172
+ def validate_array(value)
173
+ return false unless value.is_a?(Array)
174
+ return false if @min_items && value.length < @min_items
175
+ return false if @max_items && value.length > @max_items
176
+
177
+ # Validate each item if items schema is provided
178
+ if @items
179
+ item_schema = Schema.new(@items)
180
+ return false unless value.all? { |item| item_schema.validate(item) }
181
+ end
182
+
183
+ true
184
+ end
185
+ end
186
+ end
187
+ end
188
+ end
@@ -39,6 +39,10 @@ module Lutaml
39
39
 
40
40
  @model_registers.delete(name)
41
41
  end
42
+
43
+ def unregister(name)
44
+ delete(name)
45
+ end
42
46
  end
43
47
  end
44
48
  end
@@ -11,6 +11,9 @@ module Lutaml
11
11
  # will be used to resolve unless overriden in resource#realize()
12
12
  attr_accessor Hal::REGISTER_ID_ATTR_NAME.to_sym
13
13
 
14
+ # Store reference to parent resource for automatic embedded content detection
15
+ attr_accessor :parent_resource
16
+
14
17
  attribute :href, :string
15
18
  attribute :title, :string
16
19
  attribute :name, :string
@@ -24,7 +27,15 @@ module Lutaml
24
27
  # This method will use the global register according to the source of the Link object.
25
28
  # If the Link does not have a register, a register needs to be provided explicitly
26
29
  # via the `register:` parameter.
27
- def realize(register: nil)
30
+ def realize(register: nil, parent_resource: nil)
31
+ # Use provided parent_resource or fall back to stored parent_resource
32
+ effective_parent = parent_resource || @parent_resource
33
+
34
+ # First check if embedded content is available
35
+ if effective_parent && (embedded_content = check_embedded_content(effective_parent, register))
36
+ return embedded_content
37
+ end
38
+
28
39
  register = find_register(register)
29
40
  raise "No register provided for link resolution (class: #{self.class}, href: #{href})" if register.nil?
30
41
 
@@ -34,6 +45,56 @@ module Lutaml
34
45
 
35
46
  private
36
47
 
48
+ def check_embedded_content(parent_resource, register = nil)
49
+ return nil unless parent_resource.respond_to?(:embedded_data)
50
+
51
+ # Try to find embedded content that matches this link
52
+ # Look for embedded content by matching the link's key or href pattern
53
+ embedded_data = parent_resource.embedded_data
54
+ return nil unless embedded_data
55
+
56
+ # Try to find matching embedded content
57
+ # This is a simplified approach - in practice, you might need more sophisticated matching
58
+ embedded_data.each_value do |content|
59
+ # If content is an array, check if any item matches this link
60
+ if content.is_a?(Array)
61
+ matching_item = content.find { |item| matches_embedded_item?(item) }
62
+ return create_embedded_resource(matching_item, parent_resource, register) if matching_item
63
+ elsif content.is_a?(Hash) && matches_embedded_item?(content)
64
+ return create_embedded_resource(content, parent_resource, register)
65
+ end
66
+ end
67
+
68
+ nil
69
+ end
70
+
71
+ def matches_embedded_item?(item)
72
+ return false unless item.is_a?(Hash)
73
+
74
+ # Check if the embedded item's self link matches this link's href
75
+ item_self_link = item.dig('_links', 'self', 'href')
76
+ return true if item_self_link == href
77
+
78
+ # Additional matching logic could be added here
79
+ false
80
+ end
81
+
82
+ def create_embedded_resource(embedded_item, _parent_resource, register = nil)
83
+ # Get the register to determine the appropriate model class
84
+ register = find_register(register)
85
+ return nil unless register
86
+
87
+ # Try to find the model class for this href
88
+ href_path = href.sub(register.client.api_url, '') if register.client
89
+ model_class = register.send(:find_matching_model_class, href_path)
90
+ return nil unless model_class
91
+
92
+ # Create the resource from embedded data
93
+ resource = model_class.from_embedded(embedded_item, instance_variable_get("@#{Hal::REGISTER_ID_ATTR_NAME}"))
94
+ register.send(:mark_model_links_with_register, resource)
95
+ resource
96
+ end
97
+
37
98
  def find_register(explicit_register)
38
99
  return explicit_register if explicit_register
39
100