module JSI
class SchemaRegistry
class Collision < StandardError
end
class NonAbsoluteURI < StandardError
end
class ResourceNotFound < StandardError
attr_accessor :uri
end
def initialize
@resources = {}
@autoload_uris = {}
@resources_mutex = Mutex.new
end
def register(resource)
unless resource.is_a?(JSI::Base)
raise(ArgumentError, "resource must be a JSI::Base. got: #{resource.pretty_inspect.chomp}")
end
unless resource.is_a?(JSI::Schema) || resource.jsi_ptr.root?
raise(ArgumentError, "undefined behavior: registration of a JSI which is not a schema and is not at the root of a document")
end
if resource.jsi_schema_base_uri && resource.jsi_ptr.root?
register_single(resource.jsi_schema_base_uri, resource)
end
resource.jsi_each_descendent_node do |node|
if node.is_a?(JSI::Schema) && node.schema_absolute_uri
register_single(node.schema_absolute_uri, node)
end
end
nil
end
def autoload_uri(uri, &block)
uri = ensure_uri_absolute(uri)
mutating
unless block
raise(ArgumentError, ["#{SchemaRegistry}#autoload_uri must be invoked with a block", "URI: #{uri}"].join("\n"))
end
if @autoload_uris.key?(uri)
raise(Collision, ["already registered URI for autoload", "URI: #{uri}", "loader: #{@autoload_uris[uri]}"].join("\n"))
end
@autoload_uris[uri] = block
nil
end
def find(uri)
uri = ensure_uri_absolute(uri)
if @autoload_uris.key?(uri)
autoloaded = @autoload_uris[uri].call
register(autoloaded)
@autoload_uris.delete(uri)
end
if !@resources.key?(uri)
if autoloaded
msg = [
"URI #{uri} was registered with autoload_uri but the result did not contain a resource with that URI.",
"the resource resulting from autoload_uri was:",
autoloaded.pretty_inspect.chomp,
]
else
msg = ["URI #{uri} is not registered. registered URIs:", *(@resources.keys | @autoload_uris.keys)]
end
raise(ResourceNotFound.new(msg.join("\n")).tap { |e| e.uri = uri })
end
@resources[uri]
end
def inspect
[
'#<JSI::SchemaRegistry',
*[['autoload', @autoload_uris.keys], ['resources', @resources.keys]].map do |label, uris|
[
" #{label} (#{uris.size})#{uris.empty? ? "" : ":"}",
*uris.map do |uri|
" #{uri}"
end,
]
end.inject([], &:+),
'>',
].join("\n").freeze
end
alias_method :to_s, :inspect
def dup
self.class.new.tap do |reg|
@resources.each do |uri, resource|
reg.register_single(uri, resource)
end
@autoload_uris.each do |uri, autoload|
reg.autoload_uri(uri, &autoload)
end
end
end
def freeze
@resources.freeze
@autoload_uris.freeze
@resources_mutex = nil
super
end
protected
def register_single(uri, resource)
mutating
@resources_mutex.synchronize do
ensure_uri_absolute(uri)
if @resources.key?(uri)
if @resources[uri] != resource
raise(Collision, "URI collision on #{uri}.\nexisting:\n#{@resources[uri].pretty_inspect.chomp}\nnew:\n#{resource.pretty_inspect.chomp}")
end
else
@resources[uri] = resource
end
end
nil
end
private
def ensure_uri_absolute(uri)
uri = Util.uri(uri)
if uri.fragment
raise(NonAbsoluteURI, "#{self.class} only registers absolute URIs. cannot access URI with fragment: #{uri}")
end
if uri.relative?
raise(NonAbsoluteURI, "#{self.class} only registers absolute URIs. cannot access relative URI: #{uri}")
end
uri
end
def mutating
if frozen?
raise(FrozenError, "cannot modify frozen #{self.class}")
end
end
end
end