lutaml-hal 0.1.0

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 7ea54d1764e2083f8ba5e232022fa2bae95db1775db65e1be46101c3dc2db3a8
4
+ data.tar.gz: 3d519340919d3f8d84b5c6a38bc73796fbf662cc40c4a01cc030b3cfad9b00ea
5
+ SHA512:
6
+ metadata.gz: 31543a34726a0d12557f07aae9d9f70fba8ea936abb0b3f8792d26741ba4f353fcd7b9fd08e2cbff44e91819e18efcb5b67a3c50714752221c58f6a39c7761f1
7
+ data.tar.gz: 2422ddeeb2903e7adce861fce307594c11f5d147b185121e082ad49fe19156016a940ee9199d6591ab203942c4713e48c0ab3d375bf03d944df5faa914a1b4b1
data/Gemfile ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ source 'https://rubygems.org'
4
+
5
+ # Specify your gem's dependencies in lutaml-hal.gemspec
6
+ gemspec
7
+
8
+ gem 'rake'
9
+ gem 'rspec', '~> 3.12'
10
+ gem 'rubocop'
11
+
12
+ gem 'lutaml-model', git: 'https://github.com/lutaml/lutaml-model.git'
data/LICENSE.md ADDED
@@ -0,0 +1,33 @@
1
+ Licenses & Copyright
2
+ ====================
3
+
4
+ This license file adheres to the formatting guidelines of
5
+ [readable-licenses](https://github.com/nevir/readable-licenses).
6
+
7
+
8
+ Ribose's BSD 2-Clause License
9
+ -----------------------------
10
+
11
+ Copyright (c) 2024, [Ribose Inc](https://www.ribose.com).
12
+ All rights reserved.
13
+
14
+ Redistribution and use in source and binary forms, with or without modification,
15
+ are permitted provided that the following conditions are met:
16
+
17
+ 1. Redistributions of source code must retain the above copyright notice,
18
+ this list of conditions and the following disclaimer.
19
+
20
+ 2. Redistributions in binary form must reproduce the above copyright notice,
21
+ this list of conditions and the following disclaimer in the documentation
22
+ and/or other materials provided with the distribution.
23
+
24
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
25
+ ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
26
+ WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
27
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
28
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
29
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
30
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
31
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
32
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
33
+ THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
data/README.adoc ADDED
@@ -0,0 +1,244 @@
1
+ = LutaML Model for HAL
2
+
3
+ https://github.com/lutaml/lutaml-hal[image:https://img.shields.io/github/stars/lutaml/lutaml-hal.svg?style=social[GitHub Stars]]
4
+ https://github.com/lutaml/lutaml-hal[image:https://img.shields.io/github/forks/lutaml/lutaml-hal.svg?style=social[GitHub Forks]]
5
+ image:https://img.shields.io/github/license/lutaml/lutaml-hal.svg[License]
6
+ image:https://img.shields.io/github/actions/workflow/status/lutaml/lutaml-hal/test.yml?branch=main[Build Status]
7
+ image:https://img.shields.io/gem/v/lutaml-hal.svg[RubyGems Version]
8
+
9
+
10
+ == Purpose
11
+
12
+ The `lutaml-hal` gem provides a framework for interacting with HAL-compliant
13
+ APIs using the power of LutaML Models.
14
+
15
+ Hypertext Application Language (HAL)
16
+ (https://www.ietf.org/archive/id/draft-kelly-json-hal-11.html[HAL Internet-Draft])
17
+ is a simple format for representing
18
+ resources and their relationships in a hypermedia-driven API.
19
+
20
+ It allows clients to navigate and interact with resources using links, making it
21
+ easier to build flexible and extensible applications.
22
+
23
+ This library provides a set of classes and methods for modeling HAL resources,
24
+ links, and collections, as well as a client for making HTTP requests to HAL
25
+ APIs.
26
+
27
+ == Features
28
+
29
+ * Classes for modeling HAL resources and links
30
+ * A client for making HTTP requests to HAL APIs
31
+ * Tools for pagination and resource resolution
32
+ * Integration with the `lutaml-model` serialization framework
33
+ * Error handling and response validation for API interactions
34
+
35
+
36
+ == Installation
37
+
38
+ Add this line to your application's Gemfile:
39
+
40
+ [source,ruby]
41
+ ----
42
+ gem 'lutaml-hal'
43
+ ----
44
+
45
+ And then execute:
46
+
47
+ [source,sh]
48
+ ----
49
+ $ bundle install
50
+ ----
51
+
52
+ Or install it yourself as:
53
+
54
+ [source,sh]
55
+ ----
56
+ $ gem install lutaml-hal
57
+ ----
58
+
59
+ == Structure
60
+
61
+ The classes in this library are organized into the following modules:
62
+
63
+ `Lutaml::Hal::Client`::
64
+ A client for making HTTP requests to HAL APIs. It includes methods for setting
65
+ the API endpoint, making GET requests, and handling responses.
66
+ +
67
+ NOTE: Only GET requests are supported at the moment.
68
+
69
+ `Lutaml::Hal::ModelRegister`::
70
+ A registry for managing HAL resource models and their endpoints. It allows you
71
+ to register models, define their relationships, and fetch resources from the
72
+ API.
73
+
74
+ `Lutaml::Hal::Resource`::
75
+ A base class for defining HAL resource models. It includes methods for
76
+ defining attributes, links, and key-value mappings for resources.
77
+
78
+ `Lutaml::Hal::Link`::
79
+ A class for defining HAL links. It includes methods for specifying the
80
+ relationship between resources and their links, as well as methods for
81
+ resolving links to their target resources.
82
+
83
+ `Lutaml::Hal::Page`::
84
+ A class for handling pagination in HAL APIs. It includes methods for
85
+ defining pagination attributes, such as `page`, `pages`, `limit`, and
86
+ `total`, as well as methods for accessing linked resources within a page.
87
+
88
+
89
+ == Usage
90
+
91
+ === General
92
+
93
+ In order to interact with a HAL API, the following steps are required:
94
+
95
+ . Create a `Client` that points to the API endpoint.
96
+ . Create a `ModelRegister` to manage the resource models and their
97
+ respective endpoints.
98
+ . Define the resource models using the `Resource` class.
99
+ . Register the models with the `ModelRegister`.
100
+ . Fetch resources from the API using the `ModelRegister`.
101
+ .. Once the resources are fetched, you can access their attributes and links
102
+ and navigate through the resource graph.
103
+ . Pagination, such as on "index" type pages, can be handled by subclassing the `Page` class.
104
+ The `Page` class itself is also implemented as a `Resource`, so you can
105
+ use the same methods to access the page's attributes and links.
106
+
107
+
108
+ === Creating a HAL model register
109
+
110
+ [source,ruby]
111
+ ----
112
+ require 'lutaml-hal'
113
+
114
+ # Create a new client with API endpoint
115
+ client = Lutaml::Hal::Client.new(api_url: 'https://api.example.com')
116
+ register = Lutaml::Hal::ModelRegister.new(client: client)
117
+ # Or set client later, `register.client = client`
118
+
119
+ register.add_endpoint(
120
+ id: :product_index,
121
+ type: :index,
122
+ url: '/products',
123
+ model: Product
124
+ )
125
+ register.add_endpoint(
126
+ id: :product_resource,
127
+ type: :resource,
128
+ url: '/products/{id}',
129
+ model: Product
130
+ )
131
+
132
+ register.fetch(:product_index)
133
+ # => client.get('/products')
134
+
135
+ # => {
136
+ # "page": 1,
137
+ # "pages": 10,
138
+ # "limit": 10,
139
+ # "total": 45,
140
+ # "_links": {
141
+ # "self": { "href": "/products/1" },
142
+ # "next": { "href": "/products/2" },
143
+ # "last": { "href": "/products/5" },
144
+ # "products": [
145
+ # { "id": 1, "name": "Product 1", "price": 10.0 },
146
+ # { "id": 2, "name": "Product 2", "price": 15.0 }
147
+ # ]
148
+ # }
149
+
150
+ product_1 = register.fetch(:product_resource, id: 1)
151
+ # => client.get('/products/1')
152
+
153
+ # => {
154
+ # "id": 1,
155
+ # "name": "Product 1",
156
+ # "price": 10.0,
157
+ # "_links": {
158
+ # "self": { "href": "/products/1" },
159
+ # "category": { "href": "/categories/1", "title": "Category 1" },
160
+ # "related": [
161
+ # { "href": "/products/3", "title": "Product 3" },
162
+ # { "href": "/products/5", "title": "Product 5" }
163
+ # ]
164
+ # }
165
+ # }
166
+
167
+ product_1
168
+ # => #<Product id: 1, name: "Product 1", price: 10.0, links:
169
+ # #<ProductLinks self: <ProductLink href: "/products/1">,
170
+ # category: <ProductLink href: "/categories/1", title: "Category 1">,
171
+ # related: [
172
+ # <ProductLink href: "/products/3", title: "Product 3">,
173
+ # <ProductLink href: "/products/5", title: "Product 5">
174
+ # ]}>
175
+ ----
176
+
177
+ === Defining resource models
178
+
179
+ [source,ruby]
180
+ ----
181
+ module MyApi
182
+ class Product < Lutaml::Hal::Resource
183
+ attribute :id, :string
184
+ attribute :name, :string
185
+ attribute :price, :float
186
+
187
+ hal_link :self, key: 'self', realize_class: 'Product'
188
+ hal_link :category, key: 'category', realize_class: 'Category'
189
+
190
+ key_value do
191
+ map 'id', to: :id
192
+ map 'name', to: :name
193
+ map 'price', to: :price
194
+ end
195
+ end
196
+
197
+ # Register the model with the registry
198
+ Lutaml::Hal::ModelRegister.register(Product, '/products/{id}')
199
+ end
200
+ ----
201
+
202
+ === Registering endpoints
203
+
204
+ === Fetching Resources
205
+
206
+ [source,ruby]
207
+ ----
208
+ # Assume that the client is already created and registered at
209
+ # the ModelRegister
210
+ # Get a resource
211
+ product = client.get('products/123')
212
+ product_resource = MyApi::Product.from_json(product.to_json)
213
+
214
+ # Follow a link
215
+ category = product_resource.category.realize(register)
216
+ ----
217
+
218
+ === Working with Collections
219
+
220
+ [source,ruby]
221
+ ----
222
+ class ProductPage < Lutaml::Hal::Page
223
+ # Define the relationship between page and items
224
+ end
225
+
226
+ response = client.get('/products')
227
+ products = ProductPage.from_json(response.to_json)
228
+
229
+ # Access pagination info
230
+ puts "Page #{products.page} of #{products.pages}, total: #{products.total}"
231
+
232
+ # Access linked items
233
+ products.links.products.each do |product|
234
+ puts "#{product.name}: $#{product.price}"
235
+ end
236
+ ----
237
+
238
+
239
+ == License and Copyright
240
+
241
+ This project is licensed under the BSD 2-clause License.
242
+ See the link:LICENSE.md[] file for details.
243
+
244
+ Copyright Ribose.
@@ -0,0 +1,119 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'faraday'
4
+ require 'faraday/follow_redirects'
5
+ require 'json'
6
+ require 'rainbow'
7
+ require_relative 'errors'
8
+
9
+ module Lutaml
10
+ module Hal
11
+ # HAL Client for making HTTP requests to HAL APIs
12
+ class Client
13
+ attr_reader :last_response, :api_url, :connection
14
+
15
+ def initialize(options = {})
16
+ @api_url = options[:api_url] || raise(ArgumentError, 'api_url is required')
17
+ @connection = options[:connection] || create_connection
18
+ @params_default = options[:params_default] || {}
19
+ @debug = options[:debug] || !ENV['DEBUG_API'].nil?
20
+ @cache = options[:cache] || {}
21
+ @cache_enabled = options[:cache_enabled] || false
22
+ end
23
+
24
+ # Get a resource by its full URL
25
+ def get_by_url(url, params = {})
26
+ # Strip API endpoint if it's included
27
+ path = url.sub(%r{^#{@api_url}/}, '')
28
+ get(path, params)
29
+ end
30
+
31
+ # Make a GET request to the API
32
+ def get(url, params = {})
33
+ cache_key = "#{url}:#{params.to_json}"
34
+
35
+ return @cache[cache_key] if @cache_enabled && @cache.key?(cache_key)
36
+
37
+ @last_response = @connection.get(url, params)
38
+
39
+ response = handle_response(@last_response, url)
40
+
41
+ @cache[cache_key] = response if @cache_enabled
42
+ response
43
+ rescue Faraday::ConnectionFailed => e
44
+ raise ConnectionError, "Connection failed: #{e.message}"
45
+ rescue Faraday::TimeoutError => e
46
+ raise TimeoutError, "Request timed out: #{e.message}"
47
+ rescue Faraday::ParsingError => e
48
+ raise ParsingError, "Response parsing error: #{e.message}"
49
+ rescue Faraday::Adapter::Test::Stubs::NotFound => e
50
+ raise LinkResolutionError, "Resource not found: #{e.message}"
51
+ end
52
+
53
+ private
54
+
55
+ def create_connection
56
+ Faraday.new(url: @api_url) do |conn|
57
+ conn.use Faraday::FollowRedirects::Middleware
58
+ conn.request :json
59
+ conn.response :json, content_type: /\bjson$/
60
+ conn.adapter Faraday.default_adapter
61
+ end
62
+ end
63
+
64
+ def handle_response(response, url)
65
+ debug_log(response, url) if @debug
66
+
67
+ case response.status
68
+ when 200..299
69
+ response.body
70
+ when 400
71
+ raise BadRequestError, response_message(response)
72
+ when 401
73
+ raise UnauthorizedError, response_message(response)
74
+ when 404
75
+ raise NotFoundError, response_message(response)
76
+ when 500..599
77
+ raise ServerError, response_message(response)
78
+ else
79
+ raise Error, response_message(response)
80
+ end
81
+ end
82
+
83
+ def debug_log(response, url)
84
+ if defined?(Rainbow)
85
+ puts Rainbow("\n===== DEBUG: HAL API REQUEST =====").blue
86
+ else
87
+ puts "\n===== DEBUG: HAL API REQUEST ====="
88
+ end
89
+
90
+ puts "URL: #{url}"
91
+ puts "Status: #{response.status}"
92
+
93
+ # Format headers as JSON
94
+ puts "\nHeaders:"
95
+ headers_hash = response.headers.to_h
96
+ puts JSON.pretty_generate(headers_hash)
97
+
98
+ puts "\nResponse body:"
99
+ if response.body.is_a?(Hash) || response.body.is_a?(Array)
100
+ puts JSON.pretty_generate(response.body)
101
+ else
102
+ puts response.body.inspect
103
+ end
104
+
105
+ if defined?(Rainbow)
106
+ puts Rainbow("===== END DEBUG OUTPUT =====\n").blue
107
+ else
108
+ puts "===== END DEBUG OUTPUT =====\n"
109
+ end
110
+ end
111
+
112
+ def response_message(response)
113
+ message = "Status: #{response.status}"
114
+ message += ", Error: #{response.body['error']}" if response.body.is_a?(Hash) && response.body['error']
115
+ message
116
+ end
117
+ end
118
+ end
119
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lutaml
4
+ module Hal
5
+ class Error < StandardError; end
6
+ class NotFoundError < Error; end
7
+ class UnauthorizedError < Error; end
8
+ class BadRequestError < Error; end
9
+ class ServerError < Error; end
10
+ class LinkResolutionError < Error; end
11
+ end
12
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'lutaml/model'
4
+ require_relative 'model_register'
5
+
6
+ module Lutaml
7
+ module Hal
8
+ # HAL Link representation with realization capability
9
+ class Link < Lutaml::Model::Serializable
10
+ attribute :href, :string
11
+ attribute :title, :string
12
+ attribute :name, :string
13
+ attribute :templated, :boolean
14
+ attribute :type, :string
15
+ attribute :deprecation, :string
16
+ attribute :profile, :string
17
+ attribute :lang, :string
18
+
19
+ # Fetch the actual resource this link points to
20
+ def realize(register)
21
+ register.resolve_and_cast(href)
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,114 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'errors'
4
+
5
+ module Lutaml
6
+ module Hal
7
+ # Register to map URL patterns to model classes
8
+ class ModelRegister
9
+ attr_accessor :models, :client
10
+
11
+ def initialize(client: nil)
12
+ # If `client` is not set, it can be set later
13
+ @client = client
14
+ @models = {}
15
+ end
16
+
17
+ # Register a model with its base URL pattern
18
+ def add_endpoint(id:, type:, url:, model:)
19
+ @models ||= {}
20
+
21
+ raise "Model with ID #{id} already registered" if @models[id]
22
+ if @models.values.any? { |m| m[:url] == url && m[:type] == type }
23
+ raise "Duplicate URL pattern #{url} for type #{type}"
24
+ end
25
+
26
+ @models[id] = {
27
+ id: id,
28
+ type: type,
29
+ url: url,
30
+ model: model
31
+ }
32
+ end
33
+
34
+ # Resolve and cast data to the appropriate model based on URL
35
+ def fetch(endpoint_id, **params)
36
+ endpoint = @models[endpoint_id] || raise("Unknown endpoint: #{endpoint_id}")
37
+ raise 'Client not configured' unless client
38
+
39
+ url = interpolate_url(endpoint[:url], params)
40
+ response = client.get(url)
41
+
42
+ endpoint[:model].from_json(response.to_json)
43
+ end
44
+
45
+ def resolve_and_cast(href)
46
+ raise 'Client not configured' unless client
47
+
48
+ debug_log("href #{href}")
49
+ response = client.get_by_url(href)
50
+
51
+ # TODO: Merge more content into the resource
52
+ response_with_link_details = response.to_h.merge({ 'href' => href })
53
+
54
+ href_path = href.sub(client.api_url, '')
55
+ model_class = find_matching_model_class(href_path)
56
+ raise LinkResolutionError, "Unregistered URL pattern: #{href}" unless model_class
57
+
58
+ debug_log("model_class #{model_class}")
59
+ debug_log("response: #{response.inspect}")
60
+ debug_log("amended: #{response_with_link_details}")
61
+
62
+ model_class.from_json(response_with_link_details.to_json)
63
+ end
64
+
65
+ private
66
+
67
+ def interpolate_url(url_template, params)
68
+ params.reduce(url_template) do |url, (key, value)|
69
+ url.gsub("{#{key}}", value.to_s)
70
+ end
71
+ end
72
+
73
+ def find_matching_model_class(href)
74
+ @models.values.find do |model_data|
75
+ matches_url?(model_data[:url], href)
76
+ end&.[](:model)
77
+ end
78
+
79
+ def matches_url?(pattern, href)
80
+ return false unless pattern && href
81
+
82
+ if href.start_with?('/') && client&.api_url
83
+ # Try both with and without the API endpoint prefix
84
+ path_pattern = extract_path(pattern)
85
+ return pattern_match?(path_pattern, href) ||
86
+ pattern_match?(pattern, "#{client.api_url}#{href}")
87
+ end
88
+
89
+ pattern_match?(pattern, href)
90
+ end
91
+
92
+ def extract_path(pattern)
93
+ return pattern unless client&.api_url && pattern.start_with?(client.api_url)
94
+
95
+ pattern.sub(client.api_url, '')
96
+ end
97
+
98
+ # Match URL pattern (supports * wildcards and {param} templates)
99
+ def pattern_match?(pattern, url)
100
+ return false unless pattern && url
101
+
102
+ # Convert {param} to wildcards for matching
103
+ pattern_with_wildcards = pattern.gsub(/\{[^}]+\}/, '*')
104
+ # Convert * wildcards to regex pattern
105
+ regex = Regexp.new("^#{pattern_with_wildcards.gsub('*', '[^/]+')}$")
106
+ regex.match?(url)
107
+ end
108
+
109
+ def debug_log(message)
110
+ puts "DEBUG: #{message}" if ENV['DEBUG_API']
111
+ end
112
+ end
113
+ end
114
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'resource'
4
+
5
+ module Lutaml
6
+ module Hal
7
+ # Models the pagination of a collection of resources
8
+ # This class is used to represent the pagination information
9
+ # for a collection of resources in the HAL format.
10
+ class Page < Resource
11
+ attribute :page, :integer
12
+ attribute :limit, :integer
13
+ attribute :pages, :integer
14
+ attribute :total, :integer
15
+
16
+ key_value do
17
+ map 'page', to: :page
18
+ map 'limit', to: :limit
19
+ map 'pages', to: :pages
20
+ map 'total', to: :total
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,124 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'lutaml/model'
4
+ require_relative 'link'
5
+
6
+ module Lutaml
7
+ module Hal
8
+ # Resource class for all HAL resources
9
+ class Resource < Lutaml::Model::Serializable
10
+ class << self
11
+ attr_accessor :link_definitions
12
+
13
+ # Callback for when a subclass is created
14
+ def inherited(subclass)
15
+ super
16
+ subclass.class_eval do
17
+ create_links_class
18
+ init_links_definition
19
+ end
20
+ end
21
+
22
+ # The developer defines a link to another resource
23
+ # The "key" is the name of the attribute in the JSON
24
+ # The "realize_class" is the class to be realized
25
+ # The "collection" is a boolean indicating if the link
26
+ # is a collection of resources or a single resource
27
+ # The "type" is the type of the link (default is :link, can be :resource)
28
+ def hal_link(attr_key, key:, realize_class:, collection: false, type: :link)
29
+ # Use the provided "key" as the attribute name
30
+ attribute_name = attr_key.to_sym
31
+
32
+ # Create a dynamic Link subclass name based on "realize_class", the
33
+ # class to realize for a Link object
34
+ link_klass = create_link_class(realize_class)
35
+ links_klass = get_links_class
36
+ links_klass.class_eval do
37
+ # Declare the corresponding lutaml-model attribute
38
+ attribute attribute_name, link_klass, collection: collection
39
+
40
+ # Define the mapping for the attribute
41
+ key_value do
42
+ map attr_key, to: attribute_name
43
+ end
44
+ end
45
+
46
+ # Create a new link definition for future reference
47
+ link_def = {
48
+ attribute_name: attribute_name,
49
+ key: attr_key,
50
+ klass: link_klass,
51
+ collection: collection,
52
+ type: type
53
+ }
54
+
55
+ @link_definitions ||= {}
56
+ @link_definitions[key] = link_def
57
+ end
58
+
59
+ # This method obtains the Links class that holds the Link classes
60
+ def get_links_class
61
+ parent_klass_name = name.split('::')[0..-2].join('::')
62
+ child_klass_name = "#{name.split('::').last}Links"
63
+ klass_name = "#{parent_klass_name}::#{child_klass_name}"
64
+
65
+ raise unless Object.const_defined?(klass_name)
66
+
67
+ Object.const_get(klass_name)
68
+ end
69
+
70
+ private
71
+
72
+ # The "links" class holds the `_links` object which contains
73
+ # the resource-linked Link classes
74
+ def create_links_class
75
+ parent_klass_name = name.split('::')[0..-2].join('::')
76
+ child_klass_name = "#{name.split('::').last}Links"
77
+ klass_name = "#{parent_klass_name}::#{child_klass_name}"
78
+
79
+ # Check if the Links class is already defined, return if so
80
+ return Object.const_get(klass_name) if Object.const_defined?(klass_name)
81
+
82
+ # Define the links class dynamically as a normal Lutaml::Model class
83
+ # since it is not a Resource
84
+ klass = Class.new(Lutaml::Model::Serializable)
85
+ Object.const_get(parent_klass_name).tap do |parent_klass|
86
+ parent_klass.const_set(child_klass_name, klass)
87
+ end
88
+
89
+ # Define the Links class with mapping inside the current class
90
+ class_eval do
91
+ attribute :links, klass
92
+ key_value do
93
+ map '_links', to: :links
94
+ end
95
+ end
96
+ end
97
+
98
+ def init_links_definition
99
+ @link_definitions = {}
100
+ end
101
+
102
+ # This is a Link class that helps us realize the targeted class
103
+ def create_link_class(realize_class_name)
104
+ parent_klass_name = name.split('::')[0..-2].join('::')
105
+ child_klass_name = "#{name.split('::').last}Link"
106
+ klass_name = "#{parent_klass_name}::#{child_klass_name}"
107
+
108
+ return Object.const_get(klass_name) if Object.const_defined?(klass_name)
109
+
110
+ # Define the link class dynamically
111
+ klass = Class.new(Link) do
112
+ # Define the link class with the specified key and class
113
+ attribute :type, :string, default: realize_class_name
114
+ end
115
+ Object.const_get(parent_klass_name).tap do |parent_klass|
116
+ parent_klass.const_set(child_klass_name, klass)
117
+ end
118
+
119
+ klass
120
+ end
121
+ end
122
+ end
123
+ end
124
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lutaml
4
+ module Hal
5
+ VERSION = '0.1.0'
6
+ end
7
+ end
data/lib/lutaml/hal.rb ADDED
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'lutaml/model'
4
+
5
+ module Lutaml
6
+ # HAL implementation for Lutaml
7
+ module Hal
8
+ end
9
+ end
10
+
11
+ require_relative 'hal/version'
12
+ require_relative 'hal/errors'
13
+ require_relative 'hal/link'
14
+ require_relative 'hal/resource'
15
+ require_relative 'hal/page'
16
+ require_relative 'hal/model_register'
17
+ require_relative 'hal/client'
data/lib/lutaml-hal.rb ADDED
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'lutaml/hal'
metadata ADDED
@@ -0,0 +1,114 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: lutaml-hal
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Ribose Inc.
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2025-03-10 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: faraday
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '2.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '2.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: faraday-follow_redirects
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '0.3'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '0.3'
41
+ - !ruby/object:Gem::Dependency
42
+ name: lutaml-model
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rainbow
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '3.0'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '3.0'
69
+ description: Hypertext Application Language (HAL) implementation for Lutaml model
70
+ email:
71
+ - open.source@ribose.com
72
+ executables: []
73
+ extensions: []
74
+ extra_rdoc_files: []
75
+ files:
76
+ - Gemfile
77
+ - LICENSE.md
78
+ - README.adoc
79
+ - lib/lutaml-hal.rb
80
+ - lib/lutaml/hal.rb
81
+ - lib/lutaml/hal/client.rb
82
+ - lib/lutaml/hal/errors.rb
83
+ - lib/lutaml/hal/link.rb
84
+ - lib/lutaml/hal/model_register.rb
85
+ - lib/lutaml/hal/page.rb
86
+ - lib/lutaml/hal/resource.rb
87
+ - lib/lutaml/hal/version.rb
88
+ homepage: https://github.com/lutaml/lutaml-hal
89
+ licenses:
90
+ - MIT
91
+ metadata:
92
+ homepage_uri: https://github.com/lutaml/lutaml-hal
93
+ source_code_uri: https://github.com/lutaml/lutaml-hal
94
+ changelog_uri: https://github.com/lutaml/lutaml-hal/blob/master/CHANGELOG.md
95
+ post_install_message:
96
+ rdoc_options: []
97
+ require_paths:
98
+ - lib
99
+ required_ruby_version: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: 2.6.0
104
+ required_rubygems_version: !ruby/object:Gem::Requirement
105
+ requirements:
106
+ - - ">="
107
+ - !ruby/object:Gem::Version
108
+ version: '0'
109
+ requirements: []
110
+ rubygems_version: 3.5.22
111
+ signing_key:
112
+ specification_version: 4
113
+ summary: HAL implementation for LutaML
114
+ test_files: []