lib/json/ld/api.rb in json-ld-0.1.0 vs lib/json/ld/api.rb in json-ld-0.1.2
- old
+ new
@@ -1,6 +1,11 @@
require 'open-uri'
+require 'json/ld/expand'
+require 'json/ld/compact'
+require 'json/ld/frame'
+require 'json/ld/to_rdf'
+require 'json/ld/from_rdf'
module JSON::LD
##
# A JSON-LD processor implementing the JsonLdProcessor interface.
#
@@ -9,312 +14,303 @@
# entirety of the following API must be implemented.
#
# @see http://json-ld.org/spec/latest/json-ld-api/#the-application-programming-interface
# @author [Gregg Kellogg](http://greggkellogg.net/)
class API
+ include Expand
+ include Compact
+ include Triples
+ include FromTriples
+ include Frame
+
attr_accessor :value
attr_accessor :context
##
# Initialize the API, reading in any document and setting global options
#
- # @param [#read, Hash, Array] input
- # @param [IO, Hash, Array] context
+ # @param [String, #read, Hash, Array] input
+ # @param [String, #read,, Hash, Array] context
# An external context to use additionally to the context embedded in input when expanding the input.
# @param [Hash] options
# @yield [api]
# @yieldparam [API]
- def initialize(input, context, options = {})
+ def initialize(input, context, options = {}, &block)
@options = options
- @value = input.respond_to?(:read) ? JSON.parse(input.read) : input
+ @value = case input
+ when Array, Hash then input.dup
+ when IO, StringIO then JSON.parse(input.read)
+ when String
+ content = nil
+ RDF::Util::File.open_file(input) {|f| content = JSON.parse(f.read)}
+ content
+ end
@context = EvaluationContext.new(options)
@context = @context.parse(context) if context
- yield(self) if block_given?
+
+ if block_given?
+ case block.arity
+ when 0, -1 then instance_eval(&block)
+ else block.call(self)
+ end
+ end
end
##
# Expands the given input according to the steps in the Expansion Algorithm. The input must be copied, expanded and returned
# if there are no errors. If the expansion fails, an appropriate exception must be thrown.
#
- # @param [#read, Hash, Array] input
+ # The resulting `Array` is returned via the provided callback.
+ #
+ # Note that for Ruby, if the callback is not provided and a block is given, it will be yielded
+ #
+ # @param [String, #read, Hash, Array] input
# The JSON-LD object to copy and perform the expansion upon.
- # @param [IO, Hash, Array] context
+ # @param [String, #read, Hash, Array] context
# An external context to use additionally to the context embedded in input when expanding the input.
+ # @param [Proc] callback (&block)
+ # Alternative to using block, with same parameters.
# @param [Hash{Symbol => Object}] options
+ # @option options [Boolean] :base
+ # Base IRI to use when processing relative IRIs.
# @raise [InvalidContext]
- # @return [Hash, Array]
+ # @yield jsonld
+ # @yieldparam [Array<Hash>] jsonld
# The expanded JSON-LD document
+ # @return [Array<Hash>]
+ # The expanded JSON-LD document
# @see http://json-ld.org/spec/latest/json-ld-api/#expansion-algorithm
- def self.expand(input, context = nil, options = {})
+ def self.expand(input, context = nil, callback = nil, options = {})
result = nil
API.new(input, context, options) do |api|
result = api.expand(api.value, nil, api.context)
end
+
+ # If, after the algorithm outlined above is run, the resulting element is an
+ # JSON object with just a @graph property, element is set to the value of @graph's value.
+ result = result['@graph'] if result.is_a?(Hash) && result.keys == %w(@graph)
+
+ # Finally, if element is a JSON object, it is wrapped into an array.
+ result = [result] unless result.is_a?(Array)
+ callback.call(result) if callback
+ yield result if block_given?
result
end
-
- ##
- # Expand an Array or Object given an active context and performing local context expansion.
- #
- # @param [Array, Hash] input
- # @param [RDF::URI] predicate
- # @param [EvaluationContext] context
- # @return [Array, Hash]
- def expand(input, predicate, context)
- debug("expand") {"input: #{input.class}, predicate: #{predicate.inspect}, context: #{context.inspect}"}
- case input
- when Array
- # 1) If value is an array, process each item in value recursively using this algorithm,
- # passing copies of the active context and active property.
- depth {input.map {|v| expand(v, predicate, context)}}
- when Hash
- # Merge context
- context = context.parse(input['@context']) if input['@context']
-
- result = Hash.new
- input.each do |key, value|
- debug("expand") {"#{key}: #{value.inspect}"}
- expanded_key = context.mapping(key) || key
- case expanded_key
- when '@context'
- # Ignore in output
- when '@id', '@type'
- # If the key is @id or @type and the value is a string, expand the value according to IRI Expansion.
- result[expanded_key] = case value
- when String then context.expand_iri(value, :position => :subject, :depth => @depth).to_s
- else depth { expand(value, predicate, context) }
- end
- debug("expand") {" => #{result[expanded_key].inspect}"}
- when '@literal', '@language'
- raise ProcessingError::Lossy, "Value of #{expanded_key} must be a string, was #{value.inspect}" unless value.is_a?(String)
- result[expanded_key] = value
- debug("expand") {" => #{result[expanded_key].inspect}"}
- else
- # 2.2.3) Otherwise, if the key is not a keyword, expand the key according to IRI Expansion rules and set as active property.
- unless key[0,1] == '@'
- predicate = context.expand_iri(key, :position => :predicate, :depth => @depth)
- expanded_key = predicate.to_s
- end
- # 2.2.4) If the value is an array, and active property is subject to @list expansion,
- # replace the value with a new key-value key where the key is @list and value set to the current value.
- value = {"@list" => value} if value.is_a?(Array) && context.list(predicate)
-
- value = case value
- # 2.2.5) If the value is an array, process each item in the array recursively using this algorithm,
- # passing copies of the active context and active property
- # 2.2.6) If the value is an object, process the object recursively using this algorithm,
- # passing copies of the active context and active property.
- when Array, Hash then depth {expand(value, predicate, context)}
- else
- # 2.2.7) Otherwise, expand the value according to the Value Expansion rules, passing active property.
- context.expand_value(predicate, value, :position => :object, :depth => @depth)
- end
- result[expanded_key] = value
- debug("expand") {" => #{value.inspect}"}
- end
- end
- result
- else
- # 2.3) Otherwise, expand the value according to the Value Expansion rules, passing active property.
- context.expand_value(predicate, input, :position => :object, :depth => @depth)
- end
- end
-
##
# Compacts the given input according to the steps in the Compaction Algorithm. The input must be copied, compacted and
# returned if there are no errors. If the compaction fails, an appropirate exception must be thrown.
#
# If no context is provided, the input document is compacted using the top-level context of the document
#
- # @param [IO, Hash, Array] input
+ # The resulting `Hash` is returned via the provided callback.
+ #
+ # Note that for Ruby, if the callback is not provided and a block is given, it will be yielded
+ #
+ # @param [String, #read, Hash, Array] input
# The JSON-LD object to copy and perform the compaction upon.
- # @param [IO, Hash, Array] context
+ # @param [String, #read, Hash, Array] context
# The base context to use when compacting the input.
+ # @param [Proc] callback (&block)
+ # Alternative to using block, with same parameters.
# @param [Hash{Symbol => Object}] options
- # @raise [InvalidContext, ProcessingError]
+ # Other options passed to {#expand}
+ # @option options [Boolean] :optimize (false)
+ # Perform further optimmization of the compacted output.
+ # (Presently, this is a noop).
+ # @param [Hash{Symbol => Object}] options
+ # @yield jsonld
+ # @yieldparam [Hash] jsonld
+ # The compacted JSON-LD document
# @return [Hash]
# The compacted JSON-LD document
+ # @raise [InvalidContext, ProcessingError]
# @see http://json-ld.org/spec/latest/json-ld-api/#compaction-algorithm
- def self.compact(input, context = nil, options = {})
+ def self.compact(input, context, callback = nil, options = {})
expanded = result = nil
- API.new(input, nil, options) do |api|
- expanded = api.expand(api.value, nil, api.context)
+ # 1) Perform the Expansion Algorithm on the JSON-LD input.
+ # This removes any existing context to allow the given context to be cleanly applied.
+ API.new(input, nil, options) do
+ expanded = expand(value, nil, self.context)
+ debug(".compact") {"expanded input: #{value.to_json(JSON_STATE)}"}
+
+ # x) If no context provided, use context from input document
+ context ||= value.fetch('@context', nil)
end
- API.new(expanded, context, options) do |api|
- # 1) Perform the Expansion Algorithm on the JSON-LD input.
- # This removes any existing context to allow the given context to be cleanly applied.
-
- result = api.compact(api.value, nil)
+ API.new(expanded, context, options) do
+ result = compact(value, nil)
# xxx) Add the given context to the output
result = case result
- when Hash then api.context.serialize.merge(result)
- when Array then api.context.serialize.merge("@id" => result)
+ when Hash then self.context.serialize.merge(result)
+ when Array
+ kwgraph = self.context.compact_iri('@graph', :quiet => true)
+ self.context.serialize.merge(kwgraph => result)
+ when String
+ kwid = self.context.compact_iri('@id', :quiet => true)
+ self.context.serialize.merge(kwid => result)
end
end
+ callback.call(result) if callback
+ yield result if block_given?
result
end
##
- # Compact an expanded Array or Hash given an active property and a context.
- #
- # @param [Array, Hash] input
- # @param [RDF::URI] predicate (nil)
- # @param [EvaluationContext] context
- # @return [Array, Hash]
- def compact(input, predicate = nil)
- debug("compact") {"input: #{input.class}, predicate: #{predicate.inspect}"}
- case input
- when Array
- # 1) If value is an array, process each item in value recursively using this algorithm,
- # passing copies of the active context and active property.
- debug("compact") {"Array[#{input.length}]"}
- depth {input.map {|v| compact(v, predicate)}}
- when Hash
- result = Hash.new
- input.each do |key, value|
- debug("compact") {"#{key}: #{value.inspect}"}
- compacted_key = context.alias(key)
- debug("compact") {" => compacted key: #{compacted_key.inspect}"} unless compacted_key == key
-
- case key
- when '@id', '@type'
- # If the key is @id or @type
- result[compacted_key] = case value
- when String, RDF::Value
- # If the value is a string, compact the value according to IRI Compaction.
- context.compact_iri(value, :position => :subject, :depth => @depth).to_s
- when Hash
- # Otherwise, if value is an object containing only the @id key, the compacted value
- # if the result of performing IRI Compaction on that value.
- if value.keys == ["@id"]
- context.compact_iri(value["@id"], :position => :subject, :depth => @depth).to_s
- else
- depth { compact(value, predicate) }
- end
- else
- # Otherwise, the compacted value is the result of performing this algorithm on the value
- # with the current active property.
- depth { compact(value, predicate) }
- end
- debug("compact") {" => compacted value: #{result[compacted_key].inspect}"}
- else
- # Otherwise, if the key is not a keyword, set as active property and compact according to IRI Compaction.
- unless key[0,1] == '@'
- predicate = RDF::URI(key)
- compacted_key = context.compact_iri(key, :position => :predicate, :depth => @depth)
- debug("compact") {" => compacted key: #{compacted_key.inspect}"}
- end
-
- # If the value is an object
- compacted_value = if value.is_a?(Hash)
- if value.keys == ['@id'] || value['@literal']
- # If the value contains only an @id key or the value contains a @literal key, the compacted value
- # is the result of performing Value Compaction on the value.
- debug("compact") {"keys: #{value.keys.inspect}"}
- context.compact_value(predicate, value, :depth => @depth)
- elsif value.keys == ['@list'] && context.list(predicate)
- # Otherwise, if the value contains only a @list key, and the active property is subject to list coercion,
- # the compacted value is the result of performing this algorithm on that value.
- debug("compact") {"list"}
- depth {compact(value['@list'], predicate)}
- else
- # Otherwise, the compacted value is the result of performing this algorithm on the value
- debug("compact") {"object"}
- depth {compact(value, predicate)}
- end
- elsif value.is_a?(Array)
- # Otherwise, if the value is an array, the compacted value is the result of performing this algorithm on the value.
- debug("compact") {"array"}
- depth {compact(value, predicate)}
- else
- # Otherwise, the value is already compacted.
- debug("compact") {"value"}
- value
- end
- debug("compact") {" => compacted value: #{compacted_value.inspect}"}
- result[compacted_key || key] = compacted_value
- end
- end
- result
- else
- # For other types, the compacted value is the input value
- debug("compact") {input.class.to_s}
- input
- end
- end
-
- ##
# Frames the given input using the frame according to the steps in the Framing Algorithm. The input is used to build the
# framed output and is returned if there are no errors. If there are no matches for the frame, null must be returned.
# Exceptions must be thrown if there are errors.
#
- # @param [IO, Hash, Array] input
+ # The resulting `Array` is returned via the provided callback.
+ #
+ # Note that for Ruby, if the callback is not provided and a block is given, it will be yielded
+ #
+ # @param [String, #read, Hash, Array] input
# The JSON-LD object to copy and perform the framing on.
- # @param [IO, Hash, Array] frame
+ # @param [String, #read, Hash, Array] frame
# The frame to use when re-arranging the data.
+ # @param [Proc] callback (&block)
+ # Alternative to using block, with same parameters.
# @param [Hash{Symbol => Object}] options
- # @raise [InvalidFrame]
- # @return [Hash]
+ # Other options passed to {#expand}
+ # @option options [Boolean] :embed (true)
+ # a flag specifying that objects should be directly embedded in the output,
+ # instead of being referred to by their IRI.
+ # @option options [Boolean] :explicit (false)
+ # a flag specifying that for properties to be included in the output,
+ # they must be explicitly declared in the framing context.
+ # @option options [Boolean] :omitDefault (false)
+ # a flag specifying that properties that are missing from the JSON-LD
+ # input should be omitted from the output.
+ # @yield jsonld
+ # @yieldparam [Hash] jsonld
# The framed JSON-LD document
- def self.frame(input, frame, options = {})
- end
+ # @return [Array<Hash>]
+ # The framed JSON-LD document
+ # @raise [InvalidFrame]
+ # @see http://json-ld.org/spec/latest/json-ld-api/#framing-algorithm
+ def self.frame(input, frame, callback = nil, options = {})
+ result = nil
+ match_limit = 0
+ framing_state = {
+ :embed => true,
+ :explicit => false,
+ :omitDefault => false,
+ :embeds => nil,
+ }
+ framing_state[:embed] = options[:embed] if options.has_key?(:embed)
+ framing_state[:explicit] = options[:explicit] if options.has_key?(:explicit)
+ framing_state[:omitDefault] = options[:omitDefault] if options.has_key?(:omitDefault)
- ##
- # Normalizes the given input according to the steps in the Normalization Algorithm. The input must be copied, normalized and
- # returned if there are no errors. If the compaction fails, null must be returned.
- #
- # @param [IO, Hash, Array] input
- # The JSON-LD object to copy and perform the normalization upon.
- # @param [IO, Hash, Array] context
- # An external context to use additionally to the context embedded in input when expanding the input.
- # @param [Hash{Symbol => Object}] options
- # @raise [InvalidContext]
- # @return [Hash]
- # The normalized JSON-LD document
- def self.normalize(input, object, context = nil, options = {})
+ # de-reference frame to create the framing object
+ frame = case frame
+ when Hash then frame.dup
+ when IO, StringIO then JSON.parse(frame.read)
+ when String
+ content = nil
+ RDF::Util::File.open_file(frame) {|f| content = JSON.parse(f.read)}
+ content
+ end
+
+ # Expand frame to simplify processing
+ expanded_frame = API.expand(frame)
+
+ # Expand input to simplify processing
+ expanded_input = API.expand(input)
+
+ # Initialize input using frame as context
+ API.new(expanded_input, nil, options) do
+ debug(".frame") {"context from frame: #{context.inspect}"}
+ debug(".frame") {"expanded frame: #{expanded_frame.to_json(JSON_STATE)}"}
+ debug(".frame") {"expanded input: #{value.to_json(JSON_STATE)}"}
+
+ # Get framing subjects from expanded input, replacing Blank Node identifiers as necessary
+ @subjects = Hash.ordered
+ depth {get_framing_subjects(@subjects, value, BlankNodeNamer.new("t"))}
+ debug(".frame") {"subjects: #{@subjects.to_json(JSON_STATE)}"}
+
+ result = []
+ frame(framing_state, @subjects, expanded_frame[0], result, nil)
+ debug(".frame") {"after frame: #{result.to_json(JSON_STATE)}"}
+
+ # Initalize context from frame
+ @context = depth {@context.parse(frame['@context'])}
+ # Compact result
+ compacted = depth {compact(result, nil)}
+ compacted = [compacted] unless compacted.is_a?(Array)
+
+ # Add the given context to the output
+ kwgraph = context.compact_iri('@graph', :quiet => true)
+ result = context.serialize.merge({kwgraph => compacted})
+ debug(".frame") {"after compact: #{result.to_json(JSON_STATE)}"}
+ result = cleanup_preserve(result)
+ end
+
+ callback.call(result) if callback
+ yield result if block_given?
+ result
end
##
- # Processes the input according to the RDF Conversion Algorithm, calling the provided tripleCallback for each triple generated.
+ # Processes the input according to the RDF Conversion Algorithm, calling the provided callback for each triple generated.
#
- # @param [IO, Hash, Array] input
- # The JSON-LD object to process when outputting triples.
- # @param [IO, Hash, Array] context
+ # Note that for Ruby, if the callback is not provided and a block is given, it will be yielded
+ #
+ # @param [String, #read, Hash, Array] input
+ # The JSON-LD object to process when outputting statements.
+ # @param [String, #read, Hash, Array] context
# An external context to use additionally to the context embedded in input when expanding the input.
+ # @param [Proc] callback (&block)
+ # Alternative to using block, with same parameteres.
+ # @param [{Symbol,String => Object}] options
+ # Options passed to {#expand}
# @param [Hash{Symbol => Object}] options
# @raise [InvalidContext]
# @yield statement
# @yieldparam [RDF::Statement] statement
- # @return [Hash]
- # The normalized JSON-LD document
- def self.triples(input, object, context = nil, options = {})
+ def self.toRDF(input, context = nil, callback = nil, options = {})
+ # 1) Perform the Expansion Algorithm on the JSON-LD input.
+ # This removes any existing context to allow the given context to be cleanly applied.
+ expanded = expand(input, context, nil, options)
+
+ API.new(expanded, nil, options) do
+ debug(".expand") {"expanded input: #{value.to_json(JSON_STATE)}"}
+ # Start generating statements
+ statements("", value, nil, nil, nil) do |statement|
+ callback.call(statement) if callback
+ yield statement if block_given?
+ end
+ end
end
- private
- # Add debug event to debug array, if specified
+ ##
+ # Take an ordered list of RDF::Statements and turn them into a JSON-LD document.
#
- # @param [String] message
- # @yieldreturn [String] appended to message, to allow for lazy-evaulation of message
- def debug(*args)
- return unless ::JSON::LD.debug? || @options[:debug]
- list = args
- list << yield if block_given?
- message = " " * (@depth || 0) * 2 + (list.empty? ? "" : list.join(": "))
- puts message if JSON::LD::debug?
- @options[:debug] << message if @options[:debug].is_a?(Array)
- end
+ # The resulting `Array` is returned via the provided callback.
+ #
+ # Note that for Ruby, if the callback is not provided and a block is given, it will be yielded
+ #
+ # @param [Array<RDF::Statement>] input
+ # @param [Proc] callback (&block)
+ # Alternative to using block, with same parameteres.
+ # @param [Hash{Symbol => Object}] options
+ # @yield jsonld
+ # @yieldparam [Hash] jsonld
+ # The JSON-LD document in expanded form
+ # @return [Array<Hash>]
+ # The JSON-LD document in expanded form
+ def self.fromRDF(input, callback = nil, options = {})
+ result = nil
- # Increase depth around a method invocation
- def depth(options = {})
- old_depth = @depth || 0
- @depth = (options[:depth] || old_depth) + 1
- ret = yield
- @depth = old_depth
- ret
+ API.new(nil, nil, options) do |api|
+ result = api.from_statements(input, BlankNodeNamer.new("t"))
+ end
+
+ callback.call(result) if callback
+ yield result if block_given?
+ result
end
end
end