lib/json/ld/writer.rb in json-ld-0.0.8 vs lib/json/ld/writer.rb in json-ld-0.1.0

- old
+ new

@@ -32,64 +32,41 @@ # end # end # # The writer will add prefix definitions, and use them for creating @context definitions, and minting CURIEs # - # @example Creating @base, @vocab and @context prefix definitions in output + # @example Creating @@context prefix definitions in output # JSON::LD::Writer.buffer( - # :base_uri => "http://example.com/", - # :vocab => "http://example.net/" # :prefixes => { # nil => "http://example.com/ns#", # :foaf => "http://xmlns.com/foaf/0.1/"} # ) do |writer| # graph.each_statement do |statement| # writer << statement # end # end # - # Select the :normalize option to output JSON-LD in canonical form + # Select the :expand option to output JSON-LD in expanded form # # @see http://json-ld.org/spec/ED/20110507/ # @see http://json-ld.org/spec/ED/20110507/#the-normalization-algorithm # @author [Gregg Kellogg](http://greggkellogg.net/) class Writer < RDF::Writer format Format - # @attr [Graph] Graph of statements serialized + # @attr [RDF::Graph] Graph of statements serialized attr :graph - # @attr [URI] Base IRI used for relativizing IRIs - attr :base_uri - # @attr [String] Vocabulary prefix used for relativizing IRIs - attr :vocab + + # @attr [EvaluationContext] context used to load and administer contexts + attr :context - # Type coersion to use for serialization. Defaults to DEFAULT_COERCION - # - # Maintained as a reverse mapping of `property` => `type`. - # - # @attr [Hash{String => String}] - attr :coerce, true - ## - # Local implementation of ruby Hash class to allow for ordering in 1.8.x implementations. - # - # @return [Hash, InsertOrderPreservingHash] - def self.new_hash - if RUBY_VERSION < "1.9" - InsertOrderPreservingHash.new - else - Hash.new - end - end - def new_hash; self.class.new_hash; end - - ## # Return the pre-serialized Hash before turning into JSON # # @return [Hash] def self.hash(*args, &block) - hash = new_hash + hash = Hash.new self.new(hash, *args, &block) hash end ## @@ -101,30 +78,38 @@ # any additional options # @option options [Encoding] :encoding (Encoding::UTF_8) # the encoding to use on the output stream (Ruby 1.9+) # @option options [Boolean] :canonicalize (false) # whether to canonicalize literals when serializing - # @option options [Boolean] :normalize (false) - # Output document in [normalized form](http://json-ld.org/spec/latest/#normalization-1) # @option options [Hash] :prefixes (Hash.new) # the prefix mappings to use (not supported by all writers) - # @option options [#to_s] :base_uri (nil) - # Base IRI used for relativizing IRIs - # @option options [#to_s] :vocab (nil) - # Vocabulary prefix used for relativizing IRIs # @option options [Boolean] :standard_prefixes (false) # Add standard prefixes to @prefixes, if necessary. + # @option options [IO, Array, Hash, String, EvaluationContext] :context (Hash.new) + # context to use when serializing. Constructed context for native serialization. + # @option options [Boolean] :automatic (true) + # Automatically create context coercions and generate compacted form + # @option options [Boolean] :expand (false) + # Output document in [expanded form](http://json-ld.org/spec/latest/json-ld-api/#expansion) + # @option options [Boolean] :compact (false) + # Output document in [compacted form](http://json-ld.org/spec/latest/json-ld-api/#compaction). + # Requires a referenced evaluation context + # @option options [Boolean] :normalize (false) + # Output document in [normalized form](http://json-ld.org/spec/latest/json-ld-api/#normalization) + # @option options [IO, Array, Hash, String] :frame + # Output document in [framed form](http://json-ld.org/spec/latest/json-ld-api/#framing) + # using the referenced document as a frame. # @yield [writer] `self` # @yieldparam [RDF::Writer] writer # @yieldreturn [void] # @yield [writer] # @yieldparam [RDF::Writer] writer def initialize(output = $stdout, options = {}, &block) super do @graph = RDF::Graph.new - @iri_to_prefix = DEFAULT_CONTEXT.dup.delete_if {|k,v| k == '@coerce'}.invert - @coerce = DEFAULT_COERCE.merge(options[:coerce] || {}) + @options[:automatic] = true unless [:automatic, :expand, :compact, :frame, :normalize].any? {|k| options.has_key?(k)} + if block_given? case block.arity when 0 then instance_eval(&block) else block.call(self) end @@ -136,11 +121,11 @@ # Write whole graph # # @param [Graph] graph # @return [void] def write_graph(graph) - add_debug {"Add graph #{graph.inspect}"} + debug {"Add graph #{graph.inspect}"} @graph = graph end ## # Addes a statement to be serialized @@ -166,42 +151,58 @@ # Outputs the Serialized JSON-LD representation of all stored triples. # # @return [void] # @see #write_triple def write_epilogue - @base_uri = RDF::URI(@options[:base_uri]) if @options[:base_uri] && !@options[:normalize] - @vocab = @options[:vocab] unless @options[:normalize] @debug = @options[:debug] reset + + raise RDF::WriterError, "Compaction requres a context" if @options[:compact] && !@options[:context] - add_debug {"\nserialize: graph: #{@graph.size}"} + @context = EvaluationContext.new(@options) + @context = @context.parse(@options[:context]) if @options[:context] + @context.language = @options[:language] if @options[:language] + @context.lists.each {|p| @list_range[p] = true} + debug {"\nserialize: graph: #{@graph.size}"} + debug {"=> options: #{@options.reject {|k,v| k == :debug}.inspect}"} + debug {"=> context: #{@context.inspect}"} + preprocess - - # Don't generate context for canonical output - json_hash = @options[:normalize] ? new_hash : start_document + # Update prefix mappings to those defined in context + @options[:prefixes] = {} + @context.iri_to_term.each_pair do |iri, term| + debug {"add prefix #{term.inspect} for #{iri}"} + prefix(term, iri) # Define for output + end + + # Don't generate context for expanded or normalized output + json_hash = (@options[:expand] || @options[:normalize]) ? Hash.new : context.serialize(:depth => @depth) + elements = [] order_subjects.each do |subject| unless is_done?(subject) elements << subject(subject, json_hash) end end return if elements.empty? + # If there are more than one top-level subjects, place in an array form if elements.length == 1 && elements.first.is_a?(Hash) json_hash.merge!(elements.first) else - json_hash['@subject'] = elements + json_hash['@id'] = elements end if @output.is_a?(Hash) @output.merge!(json_hash) else json_state = if @options[:normalize] + # Normalization uses a compressed form JSON::State.new( :indent => "", :space => "", :space_before => "", :object_nl => "", @@ -221,50 +222,39 @@ end ## # Returns the representation of a IRI reference. # - # Spec confusion: should a subject URI be normalized? + # Spec confusion: should a subject IRI be normalized? # # @param [RDF::URI] value # @param [Hash{Symbol => Object}] options # @option options [:subject, :predicate, :object] position # Useful when determining how to serialize. # @option options [RDF::URI] property - # Property for object reference, which can be used to return - # bare strings, rather than {"iri":} + # Property for object reference, which can be used to return bare strings # @return [Object] - def format_uri(value, options = {}) - result = case options[:position] - when :subject - # attempt base_uri replacement - short = value.to_s.sub(base_uri.to_s, "") - short == value.to_s ? (get_curie(value) || value.to_s) : short - when :predicate - # attempt vocab replacement - short = '@type' if value == RDF.type - short ||= value.to_s.sub(@vocab.to_s, "") - short == value.to_s ? (get_curie(value) || value.to_s) : short - else - # Encode like a subject - iri_range?(options[:property]) ? - format_uri(value, :position => :subject) : - {'@iri' => format_uri(value, :position => :subject)} + def format_iri(value, options = {}) + debug {"format_iri(#{options.inspect}, #{value.inspect})"} + + result = context.compact_iri(value, {:depth => @depth}.merge(options)) + unless options[:position] != :object || iri_range?(options[:property]) + result = {"@id" => result} end - add_debug {"format_uri(#{options.inspect}, #{value.inspect}) => #{result.inspect}"} + debug {"=> #{result.inspect}"} result end ## # @param [RDF::Node] value # @param [Hash{Symbol => Object}] options # @return [String] # @raise [NotImplementedError] unless implemented in subclass - # @abstract + # @see {#format\_iri} def format_node(value, options = {}) - format_uri(value, options) + format_iri(value, options) end ## # Returns the representation of a literal. # @@ -272,197 +262,148 @@ # @param [Hash{Symbol => Object}] options # @option options [RDF::URI] property # Property referencing literal for type coercion # @return [Object] def format_literal(literal, options = {}) - if options[:normal] || @options[:normalize] - ret = new_hash - ret['@literal'] = literal.value - ret['@datatype'] = format_uri(literal.datatype, :position => :subject) if literal.has_datatype? - ret['@language'] = literal.language.to_s if literal.has_language? - return ret.delete_if {|k,v| v.nil?} - end + debug {"format_literal(#{options.inspect}, #{literal.inspect})"} - case literal - when RDF::Literal::Integer, RDF::Literal::Boolean + value = Hash.new + value['@literal'] = literal.value + value['@type'] = literal.datatype.to_s if literal.has_datatype? + value['@language'] = literal.language.to_s if literal.has_language? + + result = case literal + when RDF::Literal::Boolean, RDF::Literal::Integer, RDF::Literal::Double literal.object - when RDF::Literal - if datatype_range?(options[:property]) || !(literal.has_datatype? || literal.has_language?) - # Datatype coercion where literal has the same datatype - literal.value - else - format_literal(literal, :normal => true) - end + else + context.compact_value(options[:property], value, {:depth => @depth}.merge(options)) end + + debug {"=> #{result.inspect}"} + result end ## # Serialize an RDF list + # # @param [RDF::URI] object # @param [Hash{Symbol => Object}] options # @option options [RDF::URI] property - # Property referencing literal for type coercion + # Property referencing literal for type and list coercion # @return [Hash{"@list" => Array<Object>}] def format_list(object, options = {}) predicate = options[:property] - list = [] + list = RDF::List.new(object, @graph) + ary = [] - add_debug {"format_list(#{object}, #{predicate})"} + debug {"format_list(#{list.inspect}, #{predicate})"} - @depth += 1 - while object do - subject_done(object) - p = @graph.properties(object) - item = p.fetch(RDF.first.to_s, []).first - if item - add_debug {"format_list serialize #{item.inspect}"} - list << if predicate || item.literal? - property(predicate, item) + depth do + list.each_statement do |st| + next unless st.predicate == RDF.first + debug {" format_list this: #{st.subject} first: #{st.object}"} + ary << if predicate || st.object.literal? + property(predicate, st.object) else - subject(item) + subject(st.object) end + subject_done(st.subject) end - object = p.fetch(RDF.rest.to_s, []).first end - @depth -= 1 - # Returns - add_debug {"format_list => #{{'@list' => list}.inspect}"} - {'@list' => list} + # Returns + ary = {'@list' => ary} unless predicate && list_range?(predicate) + debug {"format_list => #{ary.inspect}"} + ary end private - ## - # Generate @context - # @return [Hash] - def start_document - ctx = new_hash - ctx['@base'] = base_uri.to_s if base_uri - ctx['@vocab'] = vocab.to_s if vocab - - # Prefixes - prefixes.keys.sort {|a,b| a.to_s <=> b.to_s}.each do |k| - next if DEFAULT_CONTEXT.has_key?(k.to_s) - add_debug {"prefix[#{k}] => #{prefixes[k]}"} - ctx[k.to_s] = prefixes[k].to_s - end - - # Coerce - add_debug {"start_doc: coerce= #{coerce.inspect}"} - unless coerce == DEFAULT_COERCE - c_h = new_hash - coerce.keys.sort.each do |k| - next if ['@type', RDF.type.to_s].include?(k.to_s) - next if [DEFAULT_COERCE[k], false, RDF::XSD.integer.to_s, RDF::XSD.boolean.to_s].include?(coerce[k]) - k_iri = k == '@iri' ? '@iri' : format_uri(k, :position => :predicate) - d_iri = format_uri(coerce[k], :position => :subject) - add_debug {"coerce[#{k_iri}] => #{d_iri}, k=#{k.inspect}"} - case c_h[d_iri] - when nil - c_h[d_iri] = k_iri - when Array - c_h[d_iri] << k_iri - else - c_h[d_iri] = [c_h[d_iri], k_iri] - end - end - - ctx['@coerce'] = c_h unless c_h.empty? - end - - add_debug {"start_doc: context=#{ctx.inspect}"} - - # Return hash with @context, or empty - r = new_hash - r['@context'] = ctx unless ctx.empty? - r - end - # Perform any preprocessing of statements required def preprocess - # Load defined prefixes - (@options[:prefixes] || {}).each_pair do |k, v| - @iri_to_prefix[v.to_s] = k - end - @options[:prefixes] = new_hash # Will define actual used when matched - @graph.each {|statement| preprocess_statement(statement)} end # Perform any statement preprocessing required. This is used to perform reference counts and determine required # prefixes. + # # @param [Statement] statement def preprocess_statement(statement) - add_debug {"preprocess: #{statement.inspect}"} + debug {"preprocess: #{statement.inspect}"} references = ref_count(statement.object) + 1 @references[statement.object] = references @subjects[statement.subject] = true - # Pre-fetch qnames, to fill prefixes - get_curie(statement.subject) - get_curie(statement.predicate) - if statement.object.literal? - datatype_range?(statement.predicate) # To figure out coercion requirements - else - iri_range?(statement.predicate) - get_curie(statement.object) + depth do + # Pre-fetch qnames, to fill prefixes + format_iri(statement.subject, :position => :subject) + format_iri(statement.predicate, :position => :predicate) + + # To figure out coercion requirements + if statement.object.literal? + format_literal(statement.object, :property => statement.predicate) + datatype_range?(statement.predicate) + else + format_iri(statement.object, :position => :object) + iri_range?(statement.predicate) + end + list_range?(statement.predicate) end @references[statement.predicate] = ref_count(statement.predicate) + 1 end # Serialize a subject # Option contains referencing property, if this is recursive # @return [Hash] def subject(subject, options = {}) - defn = new_hash + defn = Hash.new raise RDF::WriterError, "Illegal use of subject #{subject.inspect}, not supported" unless subject.resource? subject_done(subject) properties = @graph.properties(subject) - add_debug {"subject: #{subject.inspect}, props: #{properties.inspect}"} + debug {"subject: #{subject.inspect}, props: #{properties.inspect}"} @graph.query(:subject => subject).each do |st| raise RDF::WriterError, "Illegal use of predicate #{st.predicate.inspect}, not supported in RDF/XML" unless st.predicate.uri? end - if subject.node? && ref_count(subject) > (options[:property] ? 1 : 0) && options[:normalize] + if subject.node? && ref_count(subject) > (options[:property] ? 1 : 0) && options[:expand] raise RDF::WriterError, "Can't serialize named node when normalizing" end # Subject may be a list if is_valid_list?(subject) - add_debug "subject is a list" - defn['@subject'] = format_list(subject) + debug "subject is a list" + defn['@id'] = format_list(subject) properties.delete(RDF.first.to_s) properties.delete(RDF.rest.to_s) # Special case, if there are no properties, then we can just serialize the list itself return defn if properties.empty? elsif subject.uri? || ref_count(subject) > 1 - add_debug "subject is a uri" + debug "subject is an iri or it's a node referenced multiple times" # Don't need to set subject if it's a Node without references - defn['@subject'] = format_uri(subject, :position => :subject) + defn['@id'] = format_iri(subject, :position => :subject) else - add_debug "subject is an unreferenced BNode" + debug "subject is an unreferenced BNode" end prop_list = order_properties(properties) - #add_debug {"=> property order: #{prop_list.to_sentence}"} + debug {"=> property order: #{prop_list.inspect}"} prop_list.each do |prop| predicate = RDF::URI.intern(prop) - p_iri = format_uri(predicate, :position => :predicate) - @depth += 1 - defn[p_iri] = property(predicate, properties[prop]) - add_debug {"prop(#{p_iri}) => #{properties[prop]} => #{defn[p_iri].inspect}"} - @depth -= 1 + p_iri = format_iri(predicate, :position => :predicate) + depth do + defn[p_iri] = property(predicate, properties[prop]) + debug {"prop(#{p_iri}) => #{properties[prop]} => #{defn[p_iri].inspect}"} + end end - add_debug {"subject: #{subject} has defn: #{defn.inspect}"} + debug {"subject: #{subject} has defn: #{defn.inspect}"} defn end ## # Serialize objects for a property @@ -482,95 +423,46 @@ format_literal(objects, options.merge(:property => predicate)) else if is_valid_list?(objects) format_list(objects, :property => predicate) elsif is_done?(objects) || !@subjects.include?(objects) - format_uri(objects, :position => :object, :property => predicate) + format_iri(objects, :position => :object, :property => predicate) else subject(objects, :property => predicate) end end end ## - # Return a CURIE for the IRI, or nil. Adds namespace of CURIE to defined prefixes - # @param [RDF::Resource] resource - # @return [String, nil] value to use to identify IRI - def get_curie(resource) - add_debug {"get_curie(#{resource.inspect})"} - case resource - when RDF::Node - return resource.to_s - when String - iri = resource - resource = RDF::URI(resource) - return nil unless resource.absolute? - when RDF::URI - iri = resource.to_s - return iri if options[:normalize] - else - return nil - end - - curie = case - when @iri_to_curie.has_key?(iri) - return @iri_to_curie[iri] - when u = @iri_to_prefix.keys.detect {|u| iri.index(u.to_s) == 0} - # Use a defined prefix - prefix = @iri_to_prefix[u] - prefix(prefix, u) # Define for output - iri.sub(u.to_s, "#{prefix}:") - when @options[:standard_prefixes] && vocab = RDF::Vocabulary.detect {|v| iri.index(v.to_uri.to_s) == 0} - prefix = vocab.__name__.to_s.split('::').last.downcase - @iri_to_prefix[vocab.to_uri.to_s] = prefix - prefix(prefix, vocab.to_uri) # Define for output - iri.sub(vocab.to_uri.to_s, "#{prefix}:") - else - nil - end - - @iri_to_curie[iri] = curie - rescue Addressable::URI::InvalidURIError => e - raise RDF::WriterError, "Invalid IRI #{resource.inspect}: #{e.message}" - end - - ## # Take a hash from predicate IRIs to lists of values. # Sort the lists of values. Return a sorted list of properties. # @param [Hash{String => Array<Resource>}] properties A hash of Property to Resource mappings # @return [Array<String>}] Ordered list of properties. def order_properties(properties) # Make sorted list of properties prop_list = [] - properties.keys.sort do |a,b| - format_uri(a, :position => :predicate) <=> format_uri(b, :position => :predicate) + properties.keys.sort do |a, b| + format_iri(a, :position => :predicate) <=> format_iri(b, :position => :predicate) end.each do |prop| prop_list << prop.to_s end prop_list end # Order subjects for output. Override this to output subjects in another order. # - # Uses #base_uri. # @return [Array<Resource>] Ordered list of subjects def order_subjects seen = {} subjects = [] return @subjects.keys.sort do |a,b| format_iri(a, :position => :subject) <=> format_iri(b, :position => :subject) - end if @options[:normalize] + end unless @options[:automatic] - # Start with base_uri - if base_uri && @subjects.keys.include?(base_uri) - subjects << base_uri - seen[base_uri] = true - end - # Sort subjects by resources over bnodes, ref_counts and the subject URI itself recursable = @subjects.keys. select {|s| !seen.include?(s)}. map {|r| [r.is_a?(RDF::Node) ? 1 : 0, ref_count(r), r]}. sort @@ -587,100 +479,134 @@ ## # Does predicate have a range of IRI? # @param [RDF::URI] predicate # @return [Boolean] def iri_range?(predicate) - return false if predicate.nil? || @options[:normalize] + return false if predicate.nil? || [RDF.first, RDF.rest].include?(predicate) || @options[:expand] + return true if predicate == RDF.type - unless coerce.has_key?(predicate.to_s) - # objects of all statements with the predicate may not be literal - coerce[predicate.to_s] = @graph.query(:predicate => predicate).to_a.any? {|st| st.object.literal?} ? - false : '@iri' + unless context.coerce(predicate) + not_iri = !@options[:automatic] + #debug {" (automatic) = #{(!not_iri).inspect}"} + + # Any literal object makes it not so + not_iri ||= @graph.query(:predicate => predicate).to_a.any? do |st| + l = RDF::List.new(st.object, @graph) + #debug {" o.literal? #{st.object.literal?.inspect}"} + #debug {" l.valid? #{l.valid?.inspect}"} + #debug {" l.any.valid? #{l.to_a.any?(&:literal?).inspect}"} + st.object.literal? || (l.valid? && l.to_a.any?(&:literal?)) + end + #debug {" (literal) = #{(!not_iri).inspect}"} + + # FIXME: detect when values are all represented through chaining + + context.coerce(predicate, not_iri ? false : '@id') end - add_debug {"iri_range(#{predicate}) = #{coerce[predicate.to_s].inspect}"} - coerce[predicate.to_s] == '@iri' + debug {"iri_range(#{predicate}) = #{context.coerce(predicate).inspect}"} + context.coerce(predicate) == '@id' end ## # Does predicate have a range of specific typed literal? # @param [RDF::URI] predicate # @return [Boolean] def datatype_range?(predicate) - unless coerce.has_key?(predicate.to_s) + unless context.coerce(predicate) # objects of all statements with the predicate must be literal # and have the same non-nil datatype dt = nil - @graph.query(:predicate => predicate) do |st| - if st.object.literal? && st.object.has_datatype? - dt = st.object.datatype.to_s if dt.nil? - dt = false unless dt == st.object.datatype.to_s - else - dt = false + if @options[:automatic] + @graph.query(:predicate => predicate) do |st| + debug {"datatype_range? literal? #{st.object.literal?.inspect} dt? #{(st.object.literal? && st.object.has_datatype?).inspect}"} + if st.object.literal? && st.object.has_datatype? + dt = st.object.datatype.to_s if dt.nil? + debug {"=> dt: #{st.object.datatype}"} + dt = false unless dt == st.object.datatype.to_s + else + dt = false + end end + # Cause necessary prefixes to be output + format_iri(dt, :position => :datatype) if dt && !NATIVE_DATATYPES.include?(dt.to_s) + debug {"range(#{predicate}) = #{dt.inspect}"} + else + dt = false end - add_debug {"range(#{predicate}) = #{dt.inspect}"} - coerce[predicate.to_s] = dt + context.coerce(predicate, dt) end - coerce[predicate.to_s] + context.coerce(predicate) end + ## + # Is every use of the predicate an RDF Collection? + # + # @param [RDF::URI] predicate + # @return [Boolean] + def list_range?(predicate) + return false if [RDF.first, RDF.rest].include?(predicate) + + unless @list_range.include?(predicate.to_s) + # objects of all statements with the predicate must be a list + @list_range[predicate.to_s] = if @options[:automatic] + @graph.query(:predicate => predicate).to_a.all? do |st| + is_valid_list?(st.object) + end + else + false + end + context.list(predicate, true) if @list_range[predicate.to_s] + + debug {"list(#{predicate}) = #{@list_range[predicate.to_s].inspect}"} + end + + @list_range[predicate.to_s] + end + # Reset internal helper instance variables def reset @depth = 0 @references = {} @serialized = {} @subjects = {} - @iri_to_curie = {} + @list_range = {} end - # Add debug event to debug array, if specified - # - # @param [String] message - # @yieldreturn [String] appended to message, to allow for lazy-evaulation of message - def add_debug(message = "") - return unless ::JSON::LD.debug? || @options[:debug] - message = message + yield if block_given? - msg = "#{" " * @depth * 2}#{message}" - STDERR.puts msg if ::JSON::LD::debug? - @debug << msg if @debug.is_a?(Array) - end - # Checks if l is a valid RDF list, i.e. no nodes have other properties. def is_valid_list?(l) - props = @graph.properties(l) - unless l.node? && props.has_key?(RDF.first.to_s) || l == RDF.nil - add_debug {"is_valid_list: false, #{l.inspect}: #{props.inspect}"} - return false - end - - while l && l != RDF.nil do - #add_debug {"is_valid_list(length): #{props.length}"} - return false unless props.has_key?(RDF.first.to_s) && props.has_key?(RDF.rest.to_s) - n = props[RDF.rest.to_s] - unless n.is_a?(Array) && n.length == 1 - add_debug {"is_valid_list: false, #{n.inspect}"} - return false - end - l = n.first - unless l.node? || l == RDF.nil - add_debug {"is_valid_list: false, #{l.inspect}"} - return false - end - props = @graph.properties(l) - end - add_debug {"is_valid_list: valid"} - true + #debug {"is_valid_list: #{l.inspect}"} + return RDF::List.new(l, @graph).valid? end def is_done?(subject) @serialized.include?(subject) end # Mark a subject as done. def subject_done(subject) @serialized[subject] = true + end + + # Add debug event to debug array, if specified + # + # @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] + message = " " * @depth * 2 + (args.empty? ? "" : args.join(": ")) + message += yield if block_given? + puts message if JSON::LD::debug? + @options[:debug] << message if @options[:debug].is_a?(Array) + end + + # Increase depth around a method invocation + def depth + @depth += 1 + ret = yield + @depth -= 1 + ret end end end