lutaml-hal 0.2.1 → 0.2.3

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.
@@ -5,7 +5,6 @@ require 'lutaml/model'
5
5
  module Lutaml
6
6
  module Hal
7
7
  module Cache
8
- # Represents HTTP response metadata from the request that created the HAL resource
9
8
  class CacheMetadata < Lutaml::Model::Serializable
10
9
  attribute :etag, :string
11
10
  attribute :last_modified, :string
@@ -16,23 +15,21 @@ module Lutaml
16
15
  attribute :date, :string
17
16
  attribute :vary, :string
18
17
 
19
- # Extract metadata from HTTP response headers
20
18
  def self.from_response(response)
21
- headers = extract_headers(response)
19
+ headers = ResponseAdapter.headers(response)
22
20
 
23
21
  new(
24
22
  etag: headers['etag'],
25
23
  last_modified: headers['last-modified'],
26
24
  cache_control: headers['cache-control'],
27
25
  expires: headers['expires'],
28
- status_code: extract_status_code(response),
26
+ status_code: ResponseAdapter.status_code(response),
29
27
  content_type: headers['content-type'],
30
28
  date: headers['date'],
31
29
  vary: headers['vary']
32
30
  )
33
31
  end
34
32
 
35
- # Generate conditional request headers for cache validation
36
33
  def conditional_headers
37
34
  headers = {}
38
35
  headers['If-None-Match'] = etag if etag
@@ -40,7 +37,6 @@ module Lutaml
40
37
  headers
41
38
  end
42
39
 
43
- # Check if the metadata indicates the response is cacheable
44
40
  def cacheable?
45
41
  return false if cache_control&.include?('no-cache')
46
42
  return false if cache_control&.include?('no-store')
@@ -49,7 +45,6 @@ module Lutaml
49
45
  true
50
46
  end
51
47
 
52
- # Extract TTL from cache-control header
53
48
  def max_age
54
49
  return nil unless cache_control
55
50
 
@@ -57,46 +52,8 @@ module Lutaml
57
52
  match ? match[1].to_i : nil
58
53
  end
59
54
 
60
- # Check if the response can be revalidated with conditional requests
61
55
  def revalidatable?
62
- !etag.nil? && !etag.empty? || !last_modified.nil? && !last_modified.empty?
63
- end
64
-
65
- def self.extract_headers(response)
66
- case response
67
- when Hash
68
- # Response is already a hash, extract headers directly
69
- response.select { |k, _| k.is_a?(String) && k.match?(/^[a-z-]+$/) }
70
- when ->(r) { r.respond_to?(:headers) }
71
- # Response has headers method
72
- response.headers.to_h
73
- when ->(r) { r.respond_to?(:[]) }
74
- # Response is hash-like, try to extract common headers
75
- {
76
- 'etag' => response['etag'],
77
- 'last-modified' => response['last-modified'],
78
- 'cache-control' => response['cache-control'],
79
- 'expires' => response['expires'],
80
- 'content-type' => response['content-type'],
81
- 'date' => response['date'],
82
- 'vary' => response['vary']
83
- }.compact
84
- else
85
- {}
86
- end
87
- end
88
-
89
- def self.extract_status_code(response)
90
- case response
91
- when Hash
92
- response['status'] || response[:status] || 200
93
- when ->(r) { r.respond_to?(:status) }
94
- response.status
95
- when ->(r) { r.respond_to?(:code) }
96
- response.code.to_i
97
- else
98
- 200
99
- end
56
+ !!(etag && !etag.empty?) || !!(last_modified && !last_modified.empty?)
100
57
  end
101
58
  end
102
59
  end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lutaml
4
+ module Hal
5
+ module Cache
6
+ module ResponseAdapter
7
+ def self.headers(response)
8
+ case response
9
+ when Hash
10
+ response.select { |k, _| k.is_a?(String) && k.match?(/^[a-z-]+$/) }
11
+ when nil
12
+ {}
13
+ else
14
+ response.headers.to_h
15
+ end
16
+ end
17
+
18
+ def self.status_code(response)
19
+ case response
20
+ when Hash
21
+ response['status'] || response[:status] || 200
22
+ when nil
23
+ 200
24
+ else
25
+ response.status
26
+ end
27
+ rescue NoMethodError
28
+ 200
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lutaml
4
+ module Hal
5
+ module Cache
6
+ autoload :CacheConfiguration, 'lutaml/hal/cache/cache_configuration'
7
+ autoload :CacheEntry, 'lutaml/hal/cache/cache_entry'
8
+ autoload :CacheManager, 'lutaml/hal/cache/cache_manager'
9
+ autoload :CacheMetadata, 'lutaml/hal/cache/cache_metadata'
10
+ autoload :ResponseAdapter, 'lutaml/hal/cache/response_adapter'
11
+ end
12
+ end
13
+ end
@@ -4,12 +4,9 @@ require 'faraday'
4
4
  require 'faraday/follow_redirects'
5
5
  require 'json'
6
6
  require 'rainbow'
7
- require_relative 'errors'
8
- require_relative 'rate_limiter'
9
7
 
10
8
  module Lutaml
11
9
  module Hal
12
- # HAL Client for making HTTP requests to HAL APIs
13
10
  class Client
14
11
  attr_reader :last_response, :api_url, :connection, :rate_limiter
15
12
 
@@ -19,46 +16,38 @@ module Lutaml
19
16
  @params_default = options[:params_default] || {}
20
17
  @debug = options[:debug] || !ENV['DEBUG_API'].nil?
21
18
  @rate_limiter = options[:rate_limiter] || RateLimiter.new(options[:rate_limiting] || {})
22
-
23
19
  @api_url = strip_api_url(@api_url)
24
20
  end
25
21
 
26
- # Strip any trailing slash from the API URL
27
22
  def strip_api_url(url)
28
23
  url.sub(%r{/\Z}, '')
29
24
  end
30
25
 
31
- # Get a resource by its full URL
32
26
  def get_by_url(url, params = {})
33
- # Strip API endpoint if it's included
34
27
  path = strip_api_url(url)
35
28
  get(path, params)
36
29
  end
37
30
 
38
- # Get a resource by its full URL with custom headers (e.g. conditional
39
- # request headers for cache revalidation)
40
31
  def get_by_url_with_headers(url, headers = {})
41
32
  path = strip_api_url(url)
42
33
  get_with_headers(path, headers)
43
34
  end
44
35
 
45
- # Make a GET request to the API
46
36
  def get(url, params = {})
47
37
  @rate_limiter.with_rate_limiting do
48
38
  @last_response = @connection.get(url, params)
49
39
  handle_response(@last_response, url)
50
40
  end
51
41
  rescue Faraday::ConnectionFailed => e
52
- raise Lutaml::Hal::ConnectionError, "Connection failed: #{e.message}"
42
+ raise ConnectionError, "Connection failed: #{e.message}"
53
43
  rescue Faraday::TimeoutError => e
54
- raise Lutaml::Hal::TimeoutError, "Request timed out: #{e.message}"
44
+ raise TimeoutError, "Request timed out: #{e.message}"
55
45
  rescue Faraday::ParsingError => e
56
- raise Lutaml::Hal::ParsingError, "Response parsing error: #{e.message}"
46
+ raise ParsingError, "Response parsing error: #{e.message}"
57
47
  rescue Faraday::Adapter::Test::Stubs::NotFound => e
58
- raise Lutaml::Hal::LinkResolutionError, "Resource not found: #{e.message}"
48
+ raise LinkResolutionError, "Resource not found: #{e.message}"
59
49
  end
60
50
 
61
- # Make a GET request with custom headers
62
51
  def get_with_headers(url, headers = {})
63
52
  @rate_limiter.with_rate_limiting do
64
53
  @last_response = @connection.get(url) do |req|
@@ -67,13 +56,13 @@ module Lutaml
67
56
  handle_response(@last_response, url)
68
57
  end
69
58
  rescue Faraday::ConnectionFailed => e
70
- raise Lutaml::Hal::ConnectionError, "Connection failed: #{e.message}"
59
+ raise ConnectionError, "Connection failed: #{e.message}"
71
60
  rescue Faraday::TimeoutError => e
72
- raise Lutaml::Hal::TimeoutError, "Request timed out: #{e.message}"
61
+ raise TimeoutError, "Request timed out: #{e.message}"
73
62
  rescue Faraday::ParsingError => e
74
- raise Lutaml::Hal::ParsingError, "Response parsing error: #{e.message}"
63
+ raise ParsingError, "Response parsing error: #{e.message}"
75
64
  rescue Faraday::Adapter::Test::Stubs::NotFound => e
76
- raise Lutaml::Hal::LinkResolutionError, "Resource not found: #{e.message}"
65
+ raise LinkResolutionError, "Resource not found: #{e.message}"
77
66
  end
78
67
 
79
68
  private
@@ -124,7 +113,6 @@ module Lutaml
124
113
  puts "URL: #{url}"
125
114
  puts "Status: #{response.status}"
126
115
 
127
- # Format headers as JSON
128
116
  puts "\nHeaders:"
129
117
  headers_hash = response.headers.to_h
130
118
  puts JSON.pretty_generate(headers_hash)
@@ -4,14 +4,6 @@ require 'singleton'
4
4
 
5
5
  module Lutaml
6
6
  module Hal
7
- # Global register for model registers
8
- # This class is a singleton that manages the registration and retrieval of model registers.
9
- # It ensures that each model register is unique and provides a way to access them globally.
10
- #
11
- # @example
12
- # global_register = GlobalRegister.instance
13
- # global_register.register(:example, ExampleModelRegister.new)
14
- # example_register = global_register.get(:example)
15
7
  class GlobalRegister
16
8
  include Singleton
17
9
 
@@ -44,17 +36,14 @@ module Lutaml
44
36
  delete(name)
45
37
  end
46
38
 
47
- # Cache management methods for all registered model registers
48
39
  def clear_all_caches
49
- @model_registers.each_value do |register|
50
- register.clear_cache if register.respond_to?(:clear_cache)
51
- end
40
+ @model_registers.each_value(&:clear_cache)
52
41
  end
53
42
 
54
43
  def cache_stats
55
44
  stats = {}
56
45
  @model_registers.each do |name, register|
57
- stats[name] = register.cache_info if register.respond_to?(:cache_info)
46
+ stats[name] = register.cache_info
58
47
  end
59
48
  stats
60
49
  end
@@ -1,18 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'lutaml/model'
4
- require_relative 'model_register'
5
4
 
6
5
  module Lutaml
7
6
  module Hal
8
- # HAL Link representation with realization capability
9
7
  class Link < Lutaml::Model::Serializable
10
- # This is the model register that has fetched the origin of this link, and
11
- # will be used to resolve unless overriden in resource#realize()
12
- attr_accessor :_global_register_id
13
-
14
- # Store reference to parent resource for automatic embedded content detection
15
- attr_accessor :parent_resource
8
+ attr_accessor :_global_register_id, :parent_resource
16
9
 
17
10
  attribute :href, :string
18
11
  attribute :title, :string
@@ -23,25 +16,17 @@ module Lutaml
23
16
  attribute :profile, :string
24
17
  attribute :lang, :string
25
18
 
26
- # Fetch the actual resource this link points to.
27
- # This method will use the global register according to the source of the Link object.
28
- # If the Link does not have a register, a register needs to be provided explicitly
29
- # via the `register:` parameter.
30
19
  def realize(register: nil, parent_resource: nil, force_refresh: false)
31
- # Use provided parent_resource or fall back to stored parent_resource
32
20
  effective_parent = parent_resource || @parent_resource
33
21
 
34
22
  register = find_register(register)
35
23
  raise "No register provided for link resolution (class: #{self.class}, href: #{href})" if register.nil?
36
24
 
37
- # Priority 1: Check embedded content first (unless force_refresh)
38
25
  if !force_refresh && effective_parent && (embedded_content = check_embedded_content(effective_parent, register))
39
- # Cache embedded content too, so later lookups by href are served locally
40
26
  register.cache_manager&.set(href, nil, embedded_content)
41
27
  return embedded_content
42
28
  end
43
29
 
44
- # Force refresh bypasses any cached entry for this href
45
30
  register.cache_manager&.invalidate(href) if force_refresh
46
31
 
47
32
  Hal.debug_log "Resolving link href: #{href} using register"
@@ -51,17 +36,12 @@ module Lutaml
51
36
  private
52
37
 
53
38
  def check_embedded_content(parent_resource, register = nil)
54
- return nil unless parent_resource.respond_to?(:embedded_data)
39
+ return nil unless parent_resource.is_a?(Resource) && parent_resource.embedded_data
55
40
 
56
- # Try to find embedded content that matches this link
57
- # Look for embedded content by matching the link's key or href pattern
58
41
  embedded_data = parent_resource.embedded_data
59
42
  return nil unless embedded_data
60
43
 
61
- # Try to find matching embedded content
62
- # This is a simplified approach - in practice, you might need more sophisticated matching
63
44
  embedded_data.each_value do |content|
64
- # If content is an array, check if any item matches this link
65
45
  if content.is_a?(Array)
66
46
  matching_item = content.find { |item| matches_embedded_item?(item) }
67
47
  return create_embedded_resource(matching_item, parent_resource, register) if matching_item
@@ -76,25 +56,17 @@ module Lutaml
76
56
  def matches_embedded_item?(item)
77
57
  return false unless item.is_a?(Hash)
78
58
 
79
- # Check if the embedded item's self link matches this link's href
80
- item_self_link = item.dig('_links', 'self', 'href')
81
- return true if item_self_link == href
82
-
83
- # Additional matching logic could be added here
84
- false
59
+ item.dig('_links', 'self', 'href') == href
85
60
  end
86
61
 
87
62
  def create_embedded_resource(embedded_item, _parent_resource, register = nil)
88
- # Get the register to determine the appropriate model class
89
63
  register = find_register(register)
90
64
  return nil unless register
91
65
 
92
- # Try to find the model class for this href
93
66
  href_path = href.sub(register.client.api_url, '') if register.client
94
67
  model_class = register.find_matching_model_class(href_path)
95
68
  return nil unless model_class
96
69
 
97
- # Create the resource from embedded data
98
70
  resource = model_class.from_embedded(embedded_item, _global_register_id)
99
71
  register.mark_model_links_with_register(resource)
100
72
  resource
@@ -106,7 +78,7 @@ module Lutaml
106
78
  register_id = _global_register_id
107
79
  return nil if register_id.nil?
108
80
 
109
- register = Lutaml::Hal::GlobalRegister.instance.get(register_id)
81
+ register = GlobalRegister.instance.get(register_id)
110
82
  if register.nil?
111
83
  raise 'GlobalRegister in use but unable to find the register. '\
112
84
  'Please provide a register to the `#realize` method to resolve the link'
@@ -1,11 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'link'
4
- require_relative 'type_resolver'
5
-
6
3
  module Lutaml
7
4
  module Hal
8
- # Factory class responsible for creating dynamic Link classes
9
5
  class LinkClassFactory
10
6
  def self.create_for(resource_class, realize_class_name)
11
7
  new(resource_class, realize_class_name).create
@@ -74,8 +70,6 @@ module Lutaml
74
70
  def register_constant(klass, class_names)
75
71
  parent_klass = resolve_parent_class(class_names[:parent_namespace])
76
72
 
77
- # Avoid registering constants on Object in test scenarios
78
- # This prevents conflicts with test mocks that expect specific const_set calls
79
73
  return if parent_klass == Object && in_test_environment?
80
74
 
81
75
  parent_klass.const_set(class_names[:child_name], klass)
@@ -92,7 +86,6 @@ module Lutaml
92
86
  end
93
87
 
94
88
  def in_test_environment?
95
- # Check if we're in a test environment by looking for RSpec or test-related constants
96
89
  defined?(RSpec) || defined?(Test::Unit) || ENV['RAILS_ENV'] == 'test' || ENV['RACK_ENV'] == 'test'
97
90
  end
98
91
  end
@@ -1,11 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'lutaml/model'
4
- require_relative 'model_register'
5
4
 
6
5
  module Lutaml
7
6
  module Hal
8
- # HAL Link representation with realization capability
9
7
  class LinkSet < Lutaml::Model::Serializable
10
8
  attr_accessor :_global_register_id
11
9
  end
@@ -1,10 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'link_set'
4
-
5
3
  module Lutaml
6
4
  module Hal
7
- # Factory class responsible for creating dynamic LinkSet classes
8
5
  class LinkSetClassFactory
9
6
  def self.create_for(resource_class)
10
7
  new(resource_class).create
@@ -79,7 +76,6 @@ module Lutaml
79
76
  end
80
77
 
81
78
  def setup_resource_mapping(link_set_class)
82
- # Define the LinkSet class with mapping inside the resource class
83
79
  @resource_class.class_eval do
84
80
  attribute :links, link_set_class
85
81
  key_value do