ruby-rdf/rdf

View on GitHub
lib/rdf/model/graph.rb

Summary

Maintainability
A
3 hrs
Test Coverage
module RDF
  ##
  # An RDF graph.
  #
  # An {RDF::Graph} contains a unique set of {RDF::Statement}. It is
  # based on an underlying data object, which may be specified when the
  # graph is initialized, and will default to a {RDF::Repository} without
  # support for named graphs otherwise.
  #
  # Note that in RDF 1.1, graphs are not named, but are associated with
  # a graph name in a Dataset, as a pair of <name, graph>.
  # This class allows a name to be associated with a graph when it is
  # a projection of an underlying {RDF::Repository} supporting graph_names.
  #
  # @example Creating an empty unnamed graph
  #   graph = RDF::Graph.new
  #
  # @example Loading graph data from a URL
  #   graph = RDF::Graph.load("http://ruby-rdf.github.io/rdf/etc/doap.nt")
  #
  # @example Loading graph data from a URL
  #   require 'rdf/rdfxml'  # for RDF/XML support
  #   
  #   graph = RDF::Graph.load("http://www.bbc.co.uk/programmes/b0081dq5.rdf")
  #
  # @example Accessing a specific named graph within a {RDF::Repository}
  #   require 'rdf/trig'  # for TriG support
  #
  #   repository = graph = RDF::Repository.load("https://raw.githubusercontent.com/ruby-rdf/rdf-trig/develop/etc/doap.trig", format: :trig))
  #   graph = RDF::Graph.new(graph_name: RDF::URI("http://greggkellogg.net/foaf#me"), data: repository)
  class Graph
    include RDF::Value
    include RDF::Countable
    include RDF::Durable
    include RDF::Enumerable
    include RDF::Queryable
    include RDF::Mutable
    include RDF::Transactable

    ##
    # Returns the options passed to this graph when it was constructed.
    #
    # @!attribute [r] options
    # @return [Hash{Symbol => Object}]
    attr_reader :options

    ##
    # Name of this graph, if it is part of an {RDF::Repository}
    # @!attribute [rw] graph_name
    # @return [RDF::Resource]
    # @since 1.1.18
    attr_accessor :graph_name

    alias_method :name, :graph_name
    alias_method :name=, :graph_name=

    ##
    # {RDF::Queryable} backing this graph.
    # @!attribute [rw] data
    # @return [RDF::Queryable]
    attr_accessor :data

    ##
    # Creates a new `Graph` instance populated by the RDF data returned by
    # dereferencing the given graph_name Resource.
    #
    # @param  [String, #to_s] url
    # @param  [RDF::Resource] graph_name
    #   Set set graph name of each loaded statement
    # @param  [Hash{Symbol => Object}] options
    #   Options from {RDF::Reader.open}
    # @yield  [graph]
    # @yieldparam [Graph] graph
    # @return [Graph]
    # @since  0.1.7
    def self.load(url, graph_name: nil, **options, &block)
      self.new(graph_name: graph_name, **options) do |graph|
        graph.load(url, graph_name: graph_name, **options)

        if block_given?
          case block.arity
            when 1 then block.call(graph)
            else graph.instance_eval(&block)
          end
        end
      end
    end

    ##
    # @param  [RDF::Resource] graph_name
    #   The graph_name from the associated {RDF::Queryable} associated
    #   with this graph as provided with the `:data` option
    #   (only for {RDF::Queryable} instances supporting
    #   named graphs).
    # @param [RDF::Queryable] data (RDF::Repository.new)
    #   Storage behind this graph.
    #
    # @raise [ArgumentError] if a `data` does not support named graphs.
    # @note
    #   Graph names are only useful when used as a projection
    #   on a `:data` which supports named graphs. Otherwise, there is no
    #   such thing as a named graph in RDF 1.1, a repository may have
    #   graphs which are named, but the name is not a property of the graph.
    # @yield  [graph]
    # @yieldparam [Graph]
    def initialize(graph_name: nil, data: nil, **options, &block)
      @graph_name = case graph_name
        when nil then nil
        when RDF::Resource then graph_name
        else RDF::URI.new(graph_name)
      end

      @options = options.dup
      @data = data || RDF::Repository.new(with_graph_name: false)

      raise ArgumentError, "Can't apply graph_name unless initialized with `data` supporting graph_names" if
        @graph_name && !@data.supports?(:graph_name)

      if block_given?
        case block.arity
          when 1 then block.call(self)
          else instance_eval(&block)
        end
      end
    end

    ##
    # (re)loads the graph from the specified location, or from the location associated with the graph name, if any
    # @return [void]
    # @see    RDF::Mutable#load
    def load!(*args)
      case
        when args.empty?
          raise ArgumentError, "Can't reload graph without a graph_name" unless graph_name.is_a?(RDF::URI)
          load(graph_name.to_s, base_uri: graph_name)
        else super
      end
    end

    ##
    # @overload graph?
    #   Returns `true` to indicate that this is a graph.
    #
    #   @return [Boolean]
    # @overload graph?(name)
    #   Returns `true` if `self` contains the given RDF graph_name.
    #
    #   @param  [RDF::Resource, false] graph_name
    #     Use value `false` to query for the default graph_name
    #   @return [Boolean]
    def graph?(*args)
      case args.length
      when 0 then true
      when 1 then graph_name == args.first
      else raise ArgumentError("wrong number of arguments (given #{args.length}, expected 0 or 1)")
      end
    end

    ##
    # Returns `true` if this is a named graph.
    #
    # @return [Boolean]
    # @note The next release, graphs will not be named, this will return false
    def named?
      !unnamed?
    end

    ##
    # Returns `true` if this is a unnamed graph.
    #
    # @return [Boolean]
    # @note The next release, graphs will not be named, this will return true
    def unnamed?
      graph_name.nil?
    end

    ##
    # A graph is durable if it's underlying data model is durable
    #
    # @return [Boolean]
    # @see    RDF::Durable#durable?
    def durable?
      @data.durable?
    end

    ##
    # Returns all unique RDF names for this graph.
    #
    # @return [Enumerator<RDF::Resource>]
    def graph_names(unique: true)
      (named? ? [graph_name] : []).extend(RDF::Countable)
    end

    ##
    # Returns the {RDF::Resource} representation of this graph.
    #
    # @return [RDF::Resource]
    def to_uri
      graph_name
    end

    ##
    # Returns a string representation of this graph.
    #
    # @return [String]
    def to_s
      named? ? graph_name.to_s : "default"
    end

    ##
    # Returns `true` if this graph has an anonymous graph, `false` otherwise.
    #
    # @return [Boolean]
    # @note The next release, graphs will not be named, this will return true
    def anonymous?
      graph_name.nil? ? false : graph_name.anonymous?
    end

    ##
    # Returns the number of RDF statements in this graph.
    #
    # @return [Integer]
    # @see    RDF::Enumerable#count
    def count
      @data.query({graph_name: graph_name || false}).count
    end

    ##
    # @overload statement?
    #   Returns `false` indicating this is not an RDF::Statemenet.
    #   @see RDF::Value#statement?
    #   @return [Boolean]
    # @overload statement?(statement)
    #   Returns `true` if this graph contains the given RDF statement.
    #
    #   A statement is in a graph if the statement if it has the same triples without regard to graph_name.
    #
    #   @param  [Statement] statement
    #   @return [Boolean]
    #   @see    RDF::Enumerable#statement?
    def statement?(*args)
      case args.length
      when 0 then false
      when 1
        statement = args.first.dup
        statement.graph_name = graph_name
        @data.statement?(statement)
      else raise ArgumentError("wrong number of arguments (given #{args.length}, expected 0 or 1)")
      end
    end
    alias_method :has_statement?, :statement?

    ##
    # Enumerates each RDF statement in this graph.
    #
    # @yield  [statement]
    # @yieldparam [Statement] statement
    # @return [Enumerator]
    # @see    RDF::Enumerable#each_statement
    def each(&block)
      if @data.respond_to?(:query)
        @data.query({graph_name: graph_name || false}, &block)
      elsif @data.respond_to?(:each)
        @data.each(&block)
      else
        @data.to_a.each(&block)
      end
    end

    ##
    # @private
    # @see RDF::Enumerable#project_graph
    def project_graph(graph_name, &block)
      if block_given?
        self.each(&block) if graph_name == self.graph_name
      else
        graph_name == self.graph_name ? self : RDF::Graph.new
      end
    end

    ##
    # Graph equivalence based on the contents of each graph being _exactly_
    # the same. To determine if the have the same _meaning_, consider
    # [rdf-isomorphic](https://rubygems.org/gems/rdf-isomorphic).
    #
    # @param [RDF::Graph] other
    # @return [Boolean]
    # @see https://rubygems.org/gems/rdf-isomorphic
    def ==(other)
      other.is_a?(RDF::Graph) &&
      graph_name == other.graph_name &&
      statements.to_a == other.statements.to_a
    end

    ##
    # @private
    # @see RDF::Queryable#query_pattern
    def query_pattern(pattern, **options, &block)
      pattern = pattern.dup
      pattern.graph_name = graph_name || false
      @data.query(pattern, &block)
    end

    ##
    # @private
    # @see RDF::Mutable#insert
    def insert_statement(statement)
      if statement.embedded? && !@data.supports?(:quoted_triples)
        raise ArgumentError, "Graph does not support quoted triples"
      end
      if statement.object && statement.object.literal? && statement.object.direction? &&  !@data.supports?(:base_direction)
        raise ArgumentError, "Graph does not support directional languaged-tagged strings"
      end
      statement = statement.dup
      statement.graph_name = graph_name
      @data.insert(statement)
    end

    ##
    # @private
    # @see RDF::Mutable#insert_statements
    def insert_statements(statements)
      enum = Enumerable::Enumerator.new do |yielder|
        
        statements.send(statements.respond_to?(:each_statement) ? :each_statement : :each) do |s|
          s = s.dup
          s.graph_name = graph_name
          yielder << s
        end
      end
      @data.insert(enum)
    end

    ##
    # @private
    # @see RDF::Mutable#delete
    def delete_statement(statement)
      statement = statement.dup
      statement.graph_name = graph_name
      @data.delete(statement)
    end

    ##
    # @private
    # @see RDF::Mutable#clear
    def clear_statements
      @data.delete(graph_name: graph_name || false)
    end

    ##
    # @private
    # Opens a transaction over the graph
    # @see RDF::Transactable#begin_transaction
    def begin_transaction(mutable: false, graph_name: @graph_name)
      @data.send(:begin_transaction, mutable: mutable, graph_name: graph_name)
    end

    protected :query_pattern
    protected :insert_statement
    protected :delete_statement
    protected :clear_statements
    protected :begin_transaction

    ##
    # @private
    # @see    RDF::Enumerable#graphs
    # @since  0.2.0
    def graphs
      Array(enum_graph)
    end

    ##
    # @private
    # @see    RDF::Enumerable#each_graph
    # @since  0.2.0
    def each_graph
      if block_given?
        yield self
      else
        enum_graph
      end
    end
  end
end