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