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.
@@ -1,33 +1,20 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'lutaml/model'
4
- require_relative 'link'
5
- require_relative 'link_class_factory'
6
- require_relative 'link_set_class_factory'
7
4
 
8
5
  module Lutaml
9
6
  module Hal
10
- # Resource class for all HAL resources
11
7
  class Resource < Lutaml::Model::Serializable
12
- # This is the model register that has fetched this resource, and
13
- # will be used to resolve links unless overriden in resource#realize()
14
- attr_accessor :_global_register_id
8
+ attr_accessor :_global_register_id, :embedded_data
15
9
 
16
- # Embedded resources from the HAL `_embedded` section, retained so that
17
- # links can be realized from already-fetched content.
18
- attr_accessor :embedded_data
19
-
20
- # Check if embedded data exists for a given key
21
10
  def has_embedded?(key)
22
11
  embedded_data&.key?(key.to_s)
23
12
  end
24
13
 
25
- # Get embedded content for a specific key
26
14
  def get_embedded(key)
27
15
  embedded_data&.[](key.to_s)
28
16
  end
29
17
 
30
- # Create a resource instance from embedded JSON data
31
18
  def self.from_embedded(json_data, register_name = nil)
32
19
  instance = from_json(json_data.to_json)
33
20
  instance._global_register_id = register_name if register_name
@@ -37,7 +24,6 @@ module Lutaml
37
24
  class << self
38
25
  attr_accessor :link_definitions
39
26
 
40
- # Callback for when a subclass is created
41
27
  def inherited(subclass)
42
28
  super
43
29
  subclass.class_eval do
@@ -45,12 +31,6 @@ module Lutaml
45
31
  end
46
32
  end
47
33
 
48
- # The developer defines a link to another resource
49
- # The "key" is the name of the attribute in the JSON
50
- # The "realize_class" is the class to be realized
51
- # The "collection" is a boolean indicating if the link
52
- # is a collection of resources or a single resource
53
- # The "type" is the type of the link (default is :link, can be :resource)
54
34
  def hal_link(attr_key,
55
35
  key:,
56
36
  realize_class:,
@@ -58,56 +38,42 @@ module Lutaml
58
38
  link_set_class: nil,
59
39
  collection: false,
60
40
  type: :link)
61
- # Validate required parameters
62
41
  raise ArgumentError, 'realize_class parameter is required' if realize_class.nil?
63
42
 
64
- # Use the provided "key" as the attribute name
65
43
  attribute_name = attr_key.to_sym
66
44
 
67
45
  Hal.debug_log "Defining HAL link for `#{attr_key}` with realize class `#{realize_class}`"
68
46
 
69
- # Normalize realize_class to a string for consistent handling
70
- # Support both Class objects (when autoload is available) and strings (for delayed interpretation)
71
47
  realize_class_name = case realize_class
72
48
  when Class
73
- realize_class.name.split('::').last # Use simple name from actual class
49
+ realize_class.name.split('::').last
74
50
  when String
75
- realize_class # Use string as-is for lazy resolution
51
+ realize_class
76
52
  else
77
53
  raise ArgumentError,
78
54
  "realize_class must be a Class or String, got #{realize_class.class}"
79
55
  end
80
56
 
81
- # Create a dynamic LinkSet class if `link_set_class:` is not provided.
82
- # This must happen BEFORE creating the Link class to ensure proper order
83
57
  link_set_klass = link_set_class || create_link_set_class
84
58
 
85
- # Ensure it was actually created
86
59
  raise 'Failed to create LinkSet class' if link_set_klass.nil?
87
60
 
88
- # Create a dynamic Link subclass name based on "realize_class", the
89
- # class to realize for a Link object, if `link_class:` is not provided.
90
61
  link_klass = link_class || create_link_class(realize_class_name)
91
62
 
92
- # Now add the link to the LinkSet class
93
63
  unless link_set_class
94
64
  link_set_klass.class_eval do
95
- # Declare the corresponding lutaml-model attribute
96
- # Pass collection parameter correctly to the attribute definition
97
65
  if collection
98
66
  attribute attribute_name, link_klass, collection: true
99
67
  else
100
68
  attribute attribute_name, link_klass
101
69
  end
102
70
 
103
- # Define the mapping for the attribute
104
71
  key_value do
105
72
  map key, to: attribute_name
106
73
  end
107
74
  end
108
75
  end
109
76
 
110
- # Create a new link definition for future reference
111
77
  link_def = {
112
78
  attribute_name: attribute_name,
113
79
  key: attr_key,
@@ -120,15 +86,10 @@ module Lutaml
120
86
  @link_definitions[key] = link_def
121
87
  end
122
88
 
123
- # This method obtains the Links class that holds the Link classes
124
- # Delegates to LinkSetClassFactory for simplified implementation
125
89
  def get_link_set_class
126
90
  create_link_set_class
127
91
  end
128
92
 
129
- # The "links" class holds the `_links` object which contains
130
- # the resource-linked Link classes
131
- # Delegates to LinkSetClassFactory for simplified implementation
132
93
  def create_link_set_class
133
94
  LinkSetClassFactory.create_for(self)
134
95
  end
@@ -137,8 +98,6 @@ module Lutaml
137
98
  @link_definitions = {}
138
99
  end
139
100
 
140
- # Creates a Link class that helps us realize the targeted class
141
- # Delegates to LinkClassFactory for simplified implementation
142
101
  def create_link_class(realize_class_name)
143
102
  LinkClassFactory.create_for(self, realize_class_name)
144
103
  end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lutaml
4
+ module Hal
5
+ # Coalesces concurrent calls that share a key so the work runs once and the
6
+ # result (or error) is shared by all callers. Calls for *different* keys run
7
+ # in parallel — only same-key callers wait on the in-flight leader.
8
+ #
9
+ # Pure stdlib (Mutex + ConditionVariable); no external dependency. In-flight
10
+ # entries are removed once resolved, so memory is bounded by concurrency,
11
+ # not by the number of distinct keys ever seen.
12
+ class SingleFlight
13
+ Call = Struct.new(:mutex, :cond, :done, :value, :error)
14
+
15
+ def initialize
16
+ @registry_mutex = Mutex.new
17
+ @calls = {}
18
+ end
19
+
20
+ # Run the block at most once per key under concurrency, returning its
21
+ # result. The first caller for a key (the leader) runs the block; others
22
+ # wait and receive the same value (or re-raise the same error).
23
+ def run(key)
24
+ leader = false
25
+ call = @registry_mutex.synchronize do
26
+ @calls[key] ||= begin
27
+ leader = true
28
+ Call.new(Mutex.new, ConditionVariable.new, false)
29
+ end
30
+ end
31
+
32
+ return await(call) unless leader
33
+
34
+ begin
35
+ call.value = yield
36
+ rescue StandardError => e
37
+ call.error = e
38
+ ensure
39
+ @registry_mutex.synchronize { @calls.delete(key) }
40
+ call.mutex.synchronize do
41
+ call.done = true
42
+ call.cond.broadcast
43
+ end
44
+ end
45
+
46
+ raise call.error if call.error
47
+
48
+ call.value
49
+ end
50
+
51
+ private
52
+
53
+ def await(call)
54
+ call.mutex.synchronize do
55
+ call.cond.wait(call.mutex) until call.done
56
+ end
57
+ raise call.error if call.error
58
+
59
+ call.value
60
+ end
61
+ end
62
+ end
63
+ end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Lutaml
4
4
  module Hal
5
- VERSION = '0.2.1'
5
+ VERSION = '0.2.3'
6
6
  end
7
7
  end
data/lib/lutaml/hal.rb CHANGED
@@ -4,22 +4,38 @@ require 'lutaml/model'
4
4
  require 'lutaml/store'
5
5
 
6
6
  module Lutaml
7
- # HAL implementation for Lutaml
8
7
  module Hal
8
+ REGISTER_ID_ATTR_NAME = '_global_register_id'
9
+
9
10
  def self.debug_log(message)
10
11
  puts "[Lutaml::Hal] DEBUG: #{message}" if ENV['DEBUG_API']
11
12
  end
13
+
14
+ autoload :VERSION, 'lutaml/hal/version'
15
+ autoload :Error, 'lutaml/hal/errors'
16
+ autoload :NotFoundError, 'lutaml/hal/errors'
17
+ autoload :UnauthorizedError, 'lutaml/hal/errors'
18
+ autoload :BadRequestError, 'lutaml/hal/errors'
19
+ autoload :ServerError, 'lutaml/hal/errors'
20
+ autoload :LinkResolutionError, 'lutaml/hal/errors'
21
+ autoload :ParsingError, 'lutaml/hal/errors'
22
+ autoload :ConnectionError, 'lutaml/hal/errors'
23
+ autoload :TimeoutError, 'lutaml/hal/errors'
24
+ autoload :TooManyRequestsError, 'lutaml/hal/errors'
25
+ autoload :Cache, 'lutaml/hal/cache'
26
+ autoload :Client, 'lutaml/hal/client'
27
+ autoload :EndpointConfiguration, 'lutaml/hal/endpoint_configuration'
28
+ autoload :EndpointParameter, 'lutaml/hal/endpoint_parameter'
29
+ autoload :GlobalRegister, 'lutaml/hal/global_register'
30
+ autoload :Link, 'lutaml/hal/link'
31
+ autoload :LinkClassFactory, 'lutaml/hal/link_class_factory'
32
+ autoload :LinkSet, 'lutaml/hal/link_set'
33
+ autoload :LinkSetClassFactory, 'lutaml/hal/link_set_class_factory'
34
+ autoload :ModelRegister, 'lutaml/hal/model_register'
35
+ autoload :Page, 'lutaml/hal/page'
36
+ autoload :RateLimiter, 'lutaml/hal/rate_limiter'
37
+ autoload :Resource, 'lutaml/hal/resource'
38
+ autoload :SingleFlight, 'lutaml/hal/single_flight'
39
+ autoload :TypeResolver, 'lutaml/hal/type_resolver'
12
40
  end
13
41
  end
14
-
15
- require_relative 'hal/version'
16
- require_relative 'hal/errors'
17
- require_relative 'hal/rate_limiter'
18
- require_relative 'hal/endpoint_parameter'
19
- require_relative 'hal/link'
20
- require_relative 'hal/link_set'
21
- require_relative 'hal/resource'
22
- require_relative 'hal/page'
23
- require_relative 'hal/global_register'
24
- require_relative 'hal/model_register'
25
- require_relative 'hal/client'
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.2.1
4
+ version: 0.2.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-06-03 00:00:00.000000000 Z
11
+ date: 2026-06-08 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: faraday
@@ -92,11 +92,12 @@ files:
92
92
  - README.adoc
93
93
  - lib/lutaml-hal.rb
94
94
  - lib/lutaml/hal.rb
95
+ - lib/lutaml/hal/cache.rb
95
96
  - lib/lutaml/hal/cache/cache_configuration.rb
96
97
  - lib/lutaml/hal/cache/cache_entry.rb
97
98
  - lib/lutaml/hal/cache/cache_manager.rb
98
99
  - lib/lutaml/hal/cache/cache_metadata.rb
99
- - lib/lutaml/hal/cache/simple_cache_store.rb
100
+ - lib/lutaml/hal/cache/response_adapter.rb
100
101
  - lib/lutaml/hal/client.rb
101
102
  - lib/lutaml/hal/endpoint_configuration.rb
102
103
  - lib/lutaml/hal/endpoint_parameter.rb
@@ -110,6 +111,7 @@ files:
110
111
  - lib/lutaml/hal/page.rb
111
112
  - lib/lutaml/hal/rate_limiter.rb
112
113
  - lib/lutaml/hal/resource.rb
114
+ - lib/lutaml/hal/single_flight.rb
113
115
  - lib/lutaml/hal/type_resolver.rb
114
116
  - lib/lutaml/hal/version.rb
115
117
  homepage: https://github.com/lutaml/lutaml-hal
@@ -1,83 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Lutaml
4
- module Hal
5
- module Cache
6
- # Simple in-memory cache store for testing and fallback scenarios.
7
- #
8
- # Thread-safe: a single mutex guards the cache and its LRU bookkeeping so
9
- # it can back the register cache when consumers realize links from many
10
- # threads (e.g. a parallel fetcher).
11
- class SimpleCacheStore
12
- attr_reader :max_size
13
-
14
- def initialize(max_size = 100)
15
- @max_size = max_size
16
- @cache = {}
17
- @access_order = []
18
- @mutex = Mutex.new
19
- end
20
-
21
- def get(key)
22
- @mutex.synchronize do
23
- return nil unless @cache.key?(key)
24
-
25
- # Update access order for LRU
26
- @access_order.delete(key)
27
- @access_order.push(key)
28
-
29
- @cache[key]
30
- end
31
- end
32
-
33
- def set(key, value)
34
- @mutex.synchronize do
35
- # Remove existing entry if present
36
- if @cache.key?(key)
37
- @access_order.delete(key)
38
- elsif @cache.size >= @max_size
39
- # Evict least recently used item
40
- lru_key = @access_order.shift
41
- @cache.delete(lru_key)
42
- end
43
-
44
- @cache[key] = value
45
- @access_order.push(key)
46
- end
47
- end
48
-
49
- def delete(key)
50
- @mutex.synchronize do
51
- @access_order.delete(key)
52
- @cache.delete(key)
53
- end
54
- end
55
-
56
- def clear
57
- @mutex.synchronize do
58
- @cache.clear
59
- @access_order.clear
60
- end
61
- end
62
-
63
- def size
64
- @mutex.synchronize { @cache.size }
65
- end
66
-
67
- def stats
68
- @mutex.synchronize do
69
- {
70
- size: @cache.size,
71
- max_size: @max_size,
72
- keys: @cache.keys
73
- }
74
- end
75
- end
76
-
77
- def cache_info
78
- stats
79
- end
80
- end
81
- end
82
- end
83
- end