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
Full SHA-256 digest of the canonically serialized document.
Persisted, scheme-tagged fingerprint.
Human-readable identifier, e.g. “support@1.2.0+1a2b3c4d5e6f”.
Public Class Methods
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 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
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 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
# File lib/openui/rails/library_manifest.rb, line 154 def ==(other) other.is_a?(LibraryManifest) && other.fingerprint == fingerprint end
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
# 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
# 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
# File lib/openui/rails/library_manifest.rb, line 197 def canonical_json @canonical_json end
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
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
# File lib/openui/rails/library_manifest.rb, line 159 def hash fingerprint.hash end
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
# 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
# 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
# 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
# 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
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