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.
- checksums.yaml +4 -4
- data/lib/lutaml/hal/cache/cache_configuration.rb +17 -75
- data/lib/lutaml/hal/cache/cache_entry.rb +2 -23
- data/lib/lutaml/hal/cache/cache_manager.rb +17 -225
- data/lib/lutaml/hal/cache/cache_metadata.rb +3 -46
- data/lib/lutaml/hal/cache/response_adapter.rb +33 -0
- data/lib/lutaml/hal/cache.rb +13 -0
- data/lib/lutaml/hal/client.rb +8 -20
- data/lib/lutaml/hal/global_register.rb +2 -13
- data/lib/lutaml/hal/link.rb +4 -32
- data/lib/lutaml/hal/link_class_factory.rb +0 -7
- data/lib/lutaml/hal/link_set.rb +0 -2
- data/lib/lutaml/hal/link_set_class_factory.rb +0 -4
- data/lib/lutaml/hal/model_register.rb +97 -142
- data/lib/lutaml/hal/page.rb +1 -59
- data/lib/lutaml/hal/rate_limiter.rb +12 -28
- data/lib/lutaml/hal/resource.rb +3 -44
- data/lib/lutaml/hal/single_flight.rb +63 -0
- data/lib/lutaml/hal/version.rb +1 -1
- data/lib/lutaml/hal.rb +29 -13
- metadata +5 -3
- data/lib/lutaml/hal/cache/simple_cache_store.rb +0 -83
data/lib/lutaml/hal/resource.rb
CHANGED
|
@@ -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
|
-
|
|
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
|
|
49
|
+
realize_class.name.split('::').last
|
|
74
50
|
when String
|
|
75
|
-
realize_class
|
|
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
|
data/lib/lutaml/hal/version.rb
CHANGED
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.
|
|
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-
|
|
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/
|
|
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
|