class Lutaml::Hal::ModelRegister
Register to map URL patterns to model classes
def add_endpoint(id:, type:, url:, model:)
def add_endpoint(id:, type:, url:, model:) @models ||= {} raise "Model with ID #{id} already registered" if @models[id] if @models.values.any? { |m| m[:url] == url && m[:type] == type } raise "Duplicate URL pattern #{url} for type #{type}" end @models[id] = { id: id, type: type, url: url, model: model } end
def extract_path(pattern)
def extract_path(pattern) return pattern unless client&.api_url && pattern.start_with?(client.api_url) pattern.sub(client.api_url, '') end
def fetch(endpoint_id, **params)
def fetch(endpoint_id, **params) endpoint = @models[endpoint_id] || raise("Unknown endpoint: #{endpoint_id}") raise 'Client not configured' unless client url = interpolate_url(endpoint[:url], params) response = client.get(url) realized_model = endpoint[:model].from_json(response.to_json) mark_model_links_with_register(realized_model) realized_model end
def find_matching_model_class(href)
def find_matching_model_class(href) @models.values.find do |model_data| matches_url?(model_data[:url], href) end&.[](:model) end
def initialize(name:, client: nil)
def initialize(name:, client: nil) @register_name = name # If `client` is not set, it can be set later @client = client @models = {} end
def interpolate_url(url_template, params)
def interpolate_url(url_template, params) params.reduce(url_template) do |url, (key, value)| url.gsub("{#{key}}", value.to_s) end end
def mark_model_links_with_register(inspecting_model)
This is used to ensure that all links in the model are registered
Recursively mark all models in the link with the register name
def mark_model_links_with_register(inspecting_model) return unless inspecting_model.is_a?(Lutaml::Model::Serializable) inspecting_model.instance_variable_set("@#{Hal::REGISTER_ID_ATTR_NAME}", @register_name) # Recursively process model attributes to mark links with this register inspecting_model.class.attributes.each_pair do |key, config| attr_type = config.type next unless attr_type < Lutaml::Hal::Resource || attr_type < Lutaml::Hal::Link || attr_type < Lutaml::Hal::LinkSet value = inspecting_model.send(key) next if value.nil? # Handle both array and single values with the same logic values = value.is_a?(Array) ? value : [value] values.each { |item| mark_model_links_with_register(item) } end inspecting_model end
def matches_url?(pattern, href)
def matches_url?(pattern, href) return false unless pattern && href if href.start_with?('/') && client&.api_url # Try both with and without the API endpoint prefix path_pattern = extract_path(pattern) return pattern_match?(path_pattern, href) || pattern_match?(pattern, "#{client.api_url}#{href}") end pattern_match?(pattern, href) end
def pattern_match?(pattern, url)
def pattern_match?(pattern, url) return false unless pattern && url # Convert {param} to wildcards for matching pattern_with_wildcards = pattern.gsub(/\{[^}]+\}/, '*') # Convert * wildcards to regex pattern regex = Regexp.new("^#{pattern_with_wildcards.gsub('*', '[^/]+')}$") Hal.debug_log("pattern_match?: regex: #{regex.inspect}") Hal.debug_log("pattern_match?: href to match #{url}") Hal.debug_log("pattern_match?: pattern to match #{pattern_with_wildcards}") matches = regex.match?(url) Hal.debug_log("pattern_match?: matches = #{matches}") matches end
def resolve_and_cast(link, href)
def resolve_and_cast(link, href) raise 'Client not configured' unless client Hal.debug_log("resolve_and_cast: link #{link}, href #{href}") response = client.get_by_url(href) # TODO: Merge full Link content into the resource? response_with_link_details = response.to_h.merge({ 'href' => href }) href_path = href.sub(client.api_url, '') model_class = find_matching_model_class(href_path) raise LinkResolutionError, "Unregistered URL pattern: #{href}" unless model_class Hal.debug_log("resolve_and_cast: resolved to model_class #{model_class}") Hal.debug_log("resolve_and_cast: response: #{response.inspect}") Hal.debug_log("resolve_and_cast: amended: #{response_with_link_details}") model = model_class.from_json(response_with_link_details.to_json) mark_model_links_with_register(model) model end