class OpenUI::Rails::LibraryManifest

Immutable, validated view of an OpenUI component-library manifest (the ‘.spec.json` produced by the pinned OpenUI CLI).

The manifest is the Gem’s source of truth for which component library a rendered response was produced against. A stable fingerprint is persisted with every response so a client can detect drift instead of rendering an old source with an incompatible new schema.

Constants

COLLECTION_KEYS
MAX_BYTES

Guard against accidentally loading an enormous or hostile file.

NAME_KEYS

Candidate JSON keys, in priority order, for the human-readable name and version. The upstream schema is not frozen, so extraction is tolerant.

VERSION_KEYS

Attributes

digest[R]

Full SHA-256 digest of the canonically serialized document.

document[R]
fingerprint[R]

Persisted, scheme-tagged fingerprint.

identifier[R]

Human-readable identifier, e.g. “support@1.2.0+1a2b3c4d5e6f”.

name[R]
source[R]
version[R]

Public Class Methods

build(document, source: nil) click to toggle source

Validate an already-parsed document.

@param document [Hash] @param source [String, nil] optional path or source label @return [LibraryManifest]

# File lib/openui/rails/library_manifest.rb, line 72
def build(document, source: nil)
  unless document.is_a?(Hash)
    raise ManifestError, "library manifest must be a JSON object"
  end

  new(
    document: document,
    name: extract(document, NAME_KEYS),
    version: extract(document, VERSION_KEYS),
    source: source
  )
end
load(path) click to toggle source

Load and validate a manifest from disk.

@param path [String, Pathname]

@return [LibraryManifest]

@raise [ManifestError] when the file is missing, oversized or invalid

# File lib/openui/rails/library_manifest.rb, line 38
def load(path)
  path = Pathname(path)
  raise ManifestError, "library spec not found: #{path}" unless path.file?

  if path.size > MAX_BYTES
    raise ManifestError, "library spec too large (#{path.size} bytes)"
  end

  parse(path.read, source: path.expand_path.to_s)
rescue Errno::ENOENT, Errno::EACCES => e
  raise ManifestError, "cannot read library spec: #{e.message}"
end
new(document:, name: nil, version: nil, source: nil) click to toggle source

Create an immutable manifest and calculate its canonical fingerprint.

@param document [Hash] @param name [String, nil] @param version [String, nil] @param source [String, nil] @return [LibraryManifest]

# File lib/openui/rails/library_manifest.rb, line 105
def initialize(document:, name: nil, version: nil, source: nil)
  @document = JsonValue.copy(document)
  @name = name&.dup&.freeze
  @version = version&.dup&.freeze
  @source = source&.dup&.freeze
  @canonical_json = build_canonical_json.freeze
  @digest = Digest::SHA256.hexdigest(@canonical_json).freeze
  @fingerprint = "sha256:#{@digest}".freeze
  @identifier = build_identifier.freeze
  freeze
end
parse(json, source: nil) click to toggle source

Parse and validate a manifest from a JSON string.

@param json [String] @param source [String, nil] optional path or source label @return [LibraryManifest] @raise [ManifestError] when the string is oversized or invalid

# File lib/openui/rails/library_manifest.rb, line 57
def parse(json, source: nil)
  raise ManifestError, "library spec must be a JSON string" unless json.is_a?(String)
  raise ManifestError, "library spec too large" if json.bytesize > MAX_BYTES

  document = JSON.parse(json)
  build(document, source: source)
rescue JSON::ParserError => e
  raise ManifestError, "invalid library manifest JSON: #{e.message}"
end

Public Instance Methods

==(other) click to toggle source
# File lib/openui/rails/library_manifest.rb, line 154
def ==(other)
  other.is_a?(LibraryManifest) && other.fingerprint == fingerprint
end
action_names(= names_for(:actions)) click to toggle source

Return the names declared in the actions collection.

@return [Array<String>]

# File lib/openui/rails/library_manifest.rb, line 139
  def action_names = names_for(:actions)

  # Constant-time comparison of fingerprints for mismatch checks.
  #
  # @param other_fingerprint [String, nil]
  # @return [Boolean]
  def matches?(other_fingerprint)
    return false if other_fingerprint.nil?

    expected = fingerprint
    return false unless expected.bytesize == other_fingerprint.to_s.bytesize

    secure_compare(expected, other_fingerprint.to_s)
  end

  def ==(other)
    other.is_a?(LibraryManifest) && other.fingerprint == fingerprint
  end
  alias eql? ==

  def hash
    fingerprint.hash
  end

  # Return the manifest's name, version, fingerprint, and source.
  #
  # @return [Hash]
  def to_h
    {
      name: name,
      version: version,
      fingerprint: fingerprint,
      source: source
    }
  end

  private

  def names_for(collection)
    keys = COLLECTION_KEYS.fetch(collection)
    keys.each do |key|
      value = document[key]
      next unless value.is_a?(Array)

      return value.filter_map { |entry| name_of(entry) }
    end
    []
  end

  def name_of(entry)
    return entry if entry.is_a?(String)

    if entry.is_a?(Hash)
      NAME_KEYS.each { |key| return entry[key].to_s if entry[key].is_a?(String) }
    end
    nil
  end

  def canonical_json
    @canonical_json
  end

  def build_canonical_json
    JSON.generate(sort_deep(document))
  rescue JSON::GeneratorError
    # Defensive: an exotic document should still fingerprint deterministically.
    JSON.generate(document)
  end

  def build_identifier
    base = [ name, version ].compact.join("@")
    base = "library" if base.empty?
    "#{base}+#{digest[0, 12]}"
  end

  def sort_deep(value)
    case value
    when Hash
      value.keys.sort_by(&:to_s).each_with_object({}) { |key, memo| memo[key] = sort_deep(value[key]) }
    when Array
      value.map { |entry| sort_deep(entry) }
    else
      value
    end
  end

  def secure_compare(left, right)
    return false unless left.bytesize == right.bytesize

    result = 0
    left.bytes.zip(right.bytes) { |a, b| result |= a ^ b }
    result.zero?
  end
end
build_canonical_json() click to toggle source
# File lib/openui/rails/library_manifest.rb, line 201
def build_canonical_json
  JSON.generate(sort_deep(document))
rescue JSON::GeneratorError
  # Defensive: an exotic document should still fingerprint deterministically.
  JSON.generate(document)
end
build_identifier() click to toggle source
# File lib/openui/rails/library_manifest.rb, line 208
def build_identifier
  base = [ name, version ].compact.join("@")
  base = "library" if base.empty?
  "#{base}+#{digest[0, 12]}"
end
canonical_json() click to toggle source
# File lib/openui/rails/library_manifest.rb, line 197
def canonical_json
  @canonical_json
end
component_names(= names_for(:components)) click to toggle source

Return the names declared in the components collection.

@return [Array<String>]

# File lib/openui/rails/library_manifest.rb, line 129
      def component_names = names_for(:components)

      # Return the names declared in the functions collection.
      #
      # @return [Array<String>]
      def function_names = names_for(:functions)

      # Return the names declared in the actions collection.
      #
      # @return [Array<String>]
      def action_names = names_for(:actions)

      # Constant-time comparison of fingerprints for mismatch checks.
      #
      # @param other_fingerprint [String, nil]
      # @return [Boolean]
      def matches?(other_fingerprint)
        return false if other_fingerprint.nil?

        expected = fingerprint
        return false unless expected.bytesize == other_fingerprint.to_s.bytesize

        secure_compare(expected, other_fingerprint.to_s)
      end

      def ==(other)
        other.is_a?(LibraryManifest) && other.fingerprint == fingerprint
      end
      alias eql? ==

      def hash
        fingerprint.hash
      end

      # Return the manifest's name, version, fingerprint, and source.
      #
      # @return [Hash]
      def to_h
        {
          name: name,
          version: version,
          fingerprint: fingerprint,
          source: source
        }
      end

      private

      def names_for(collection)
        keys = COLLECTION_KEYS.fetch(collection)
        keys.each do |key|
          value = document[key]
          next unless value.is_a?(Array)

          return value.filter_map { |entry| name_of(entry) }
        end
        []
      end

      def name_of(entry)
        return entry if entry.is_a?(String)

        if entry.is_a?(Hash)
          NAME_KEYS.each { |key| return entry[key].to_s if entry[key].is_a?(String) }
        end
        nil
      end

      def canonical_json
        @canonical_json
      end

      def build_canonical_json
        JSON.generate(sort_deep(document))
      rescue JSON::GeneratorError
        # Defensive: an exotic document should still fingerprint deterministically.
        JSON.generate(document)
      end

      def build_identifier
        base = [ name, version ].compact.join("@")
        base = "library" if base.empty?
        "#{base}+#{digest[0, 12]}"
      end

      def sort_deep(value)
        case value
        when Hash
          value.keys.sort_by(&:to_s).each_with_object({}) { |key, memo| memo[key] = sort_deep(value[key]) }
        when Array
          value.map { |entry| sort_deep(entry) }
        else
          value
        end
      end

      def secure_compare(left, right)
        return false unless left.bytesize == right.bytesize

        result = 0
        left.bytes.zip(right.bytes) { |a, b| result |= a ^ b }
        result.zero?
      end
    end
  end
end
function_names(= names_for(:functions)) click to toggle source

Return the names declared in the functions collection.

@return [Array<String>]

# File lib/openui/rails/library_manifest.rb, line 134
    def function_names = names_for(:functions)

    # Return the names declared in the actions collection.
    #
    # @return [Array<String>]
    def action_names = names_for(:actions)

    # Constant-time comparison of fingerprints for mismatch checks.
    #
    # @param other_fingerprint [String, nil]
    # @return [Boolean]
    def matches?(other_fingerprint)
      return false if other_fingerprint.nil?

      expected = fingerprint
      return false unless expected.bytesize == other_fingerprint.to_s.bytesize

      secure_compare(expected, other_fingerprint.to_s)
    end

    def ==(other)
      other.is_a?(LibraryManifest) && other.fingerprint == fingerprint
    end
    alias eql? ==

    def hash
      fingerprint.hash
    end

    # Return the manifest's name, version, fingerprint, and source.
    #
    # @return [Hash]
    def to_h
      {
        name: name,
        version: version,
        fingerprint: fingerprint,
        source: source
      }
    end

    private

    def names_for(collection)
      keys = COLLECTION_KEYS.fetch(collection)
      keys.each do |key|
        value = document[key]
        next unless value.is_a?(Array)

        return value.filter_map { |entry| name_of(entry) }
      end
      []
    end

    def name_of(entry)
      return entry if entry.is_a?(String)

      if entry.is_a?(Hash)
        NAME_KEYS.each { |key| return entry[key].to_s if entry[key].is_a?(String) }
      end
      nil
    end

    def canonical_json
      @canonical_json
    end

    def build_canonical_json
      JSON.generate(sort_deep(document))
    rescue JSON::GeneratorError
      # Defensive: an exotic document should still fingerprint deterministically.
      JSON.generate(document)
    end

    def build_identifier
      base = [ name, version ].compact.join("@")
      base = "library" if base.empty?
      "#{base}+#{digest[0, 12]}"
    end

    def sort_deep(value)
      case value
      when Hash
        value.keys.sort_by(&:to_s).each_with_object({}) { |key, memo| memo[key] = sort_deep(value[key]) }
      when Array
        value.map { |entry| sort_deep(entry) }
      else
        value
      end
    end

    def secure_compare(left, right)
      return false unless left.bytesize == right.bytesize

      result = 0
      left.bytes.zip(right.bytes) { |a, b| result |= a ^ b }
      result.zero?
    end
  end
end
hash() click to toggle source
# File lib/openui/rails/library_manifest.rb, line 159
def hash
  fingerprint.hash
end
matches?(other_fingerprint) click to toggle source

Constant-time comparison of fingerprints for mismatch checks.

@param other_fingerprint [String, nil] @return [Boolean]

# File lib/openui/rails/library_manifest.rb, line 145
def matches?(other_fingerprint)
  return false if other_fingerprint.nil?

  expected = fingerprint
  return false unless expected.bytesize == other_fingerprint.to_s.bytesize

  secure_compare(expected, other_fingerprint.to_s)
end
name_of(entry) click to toggle source
# File lib/openui/rails/library_manifest.rb, line 188
def name_of(entry)
  return entry if entry.is_a?(String)

  if entry.is_a?(Hash)
    NAME_KEYS.each { |key| return entry[key].to_s if entry[key].is_a?(String) }
  end
  nil
end
names_for(collection) click to toggle source
# File lib/openui/rails/library_manifest.rb, line 177
def names_for(collection)
  keys = COLLECTION_KEYS.fetch(collection)
  keys.each do |key|
    value = document[key]
    next unless value.is_a?(Array)

    return value.filter_map { |entry| name_of(entry) }
  end
  []
end
secure_compare(left, right) click to toggle source
# File lib/openui/rails/library_manifest.rb, line 225
def secure_compare(left, right)
  return false unless left.bytesize == right.bytesize

  result = 0
  left.bytes.zip(right.bytes) { |a, b| result |= a ^ b }
  result.zero?
end
sort_deep(value) click to toggle source
# File lib/openui/rails/library_manifest.rb, line 214
def sort_deep(value)
  case value
  when Hash
    value.keys.sort_by(&:to_s).each_with_object({}) { |key, memo| memo[key] = sort_deep(value[key]) }
  when Array
    value.map { |entry| sort_deep(entry) }
  else
    value
  end
end
to_h() click to toggle source

Return the manifest’s name, version, fingerprint, and source.

@return [Hash]

# File lib/openui/rails/library_manifest.rb, line 166
def to_h
  {
    name: name,
    version: version,
    fingerprint: fingerprint,
    source: source
  }
end