lib/rdf/changeset.rb
module RDF
##
# An RDF changeset that can be applied to an {RDF::Mutable}.
#
# Changesets consist of a sequence of RDF statements to delete from and a
# sequence of RDF statements to insert into a target dataset.
#
# @example Applying a Changeset with block syntax
# graph = RDF::Graph.new
# graph << [RDF::URI('s_del'), RDF::URI('p_del'), RDF::URI('o_del')]
#
# RDF::Changeset.apply(graph) do |c|
# c.insert [RDF::URI('s1'), RDF::URI('p1'), RDF::URI('o1')]
# c.insert [RDF::URI('s2'), RDF::URI('p2'), RDF::URI('o2')]
# c.delete [RDF::URI('s_del'), RDF::URI('p_del'), RDF::URI('o_del')]
# end
#
# @example Defining a changeset for later application to a Mutable
# changes = RDF::Changeset.new do |c|
# c.insert [RDF::URI('s1'), RDF::URI('p1'), RDF::URI('o1')]
# c.insert [RDF::URI('s2'), RDF::URI('p2'), RDF::URI('o2')]
# c.delete [RDF::URI('s_del'), RDF::URI('p_del'), RDF::URI('o_del')]
# end
#
# graph = RDF::Graph.new
# graph << [RDF::URI('s_del'), RDF::URI('p_del'), RDF::URI('o_del')]
#
# changes.apply(graph) # or graph.apply_changeset(changes)
#
# @note When applying a Changeset, deletes are resolved before inserts.
#
# @since 2.0.0
class Changeset
# include RDF::Mutable
include RDF::Util::Coercions
##
# Applies a changeset to the given {RDF::Mutable} object.
#
# @param [RDF::Mutable] mutable
# @param [Hash{Symbol => Object}] options
# @yield [changes]
# @yieldparam [RDF::Changeset] changes
# @return [void]
def self.apply(mutable, **options, &block)
self.new(&block).apply(mutable, **options)
end
##
# RDF statements to delete when applied.
#
# @return [RDF::Enumerable]
attr_reader :deletes
##
# RDF statements to insert when applied.
#
# @return [RDF::Enumerable]
attr_reader :inserts
##
# Any additional options for this changeset.
#
# @return [Hash{Symbol => Object}]
attr_reader :options
##
# Initializes this changeset.
#
# @param [RDF::Enumerable] insert (RDF::Graph.new)
# @param [RDF::Enumerable] delete (RDF::Graph.new)
# @yield [changes]
# @yieldparam [RDF::Changeset] changes
def initialize(insert: [], delete: [], &block)
@inserts = insert
@deletes = delete
@inserts.extend(RDF::Enumerable) unless @inserts.kind_of?(RDF::Enumerable)
@deletes.extend(RDF::Enumerable) unless @deletes.kind_of?(RDF::Enumerable)
if block_given?
case block.arity
when 1 then block.call(self)
else self.instance_eval(&block)
end
end
end
##
# Returns `false` to indicate that this changeset is append-only.
#
# Changesets do not support the `RDF::Enumerable` protocol directly.
# To enumerate the RDF statements to be inserted or deleted, use the
# {RDF::Changeset#inserts} and {RDF::Changeset#deletes} accessors.
#
# @return [Boolean]
# @see RDF::Readable#readable?
def readable?
false
end
##
# Returns `false` as changesets are not {RDF::Writable}.
#
# @return [Boolean]
# @see RDF::Writable#writable?
def writable?
false
end
##
# Returns `false` as changesets are not {RDF::Mutable}.
#
# @return [Boolean]
# @see RDF::Mutable#mutable?
def mutable?
false
end
##
# Applies this changeset to the given mutable RDF::Enumerable.
#
# This operation executes as a single write transaction.
#
# @param [RDF::Mutable] mutable
# @param [Hash{Symbol => Object}] options
# @return [void]
def apply(mutable, **options)
mutable.apply_changeset(self)
end
##
# @return [Boolean] `true` iff inserts and deletes are both empty
def empty?
deletes.empty? && inserts.empty?
end
##
# Returns a developer-friendly representation of this changeset.
#
# @return [String]
def inspect
sprintf("#<%s:%#0x(deletes: %d, inserts: %d)>", self.class.name,
self.__id__, self.deletes.count, self.inserts.count)
end
##
# Outputs a developer-friendly representation of this changeset to
# `$stderr`.
#
# @return [void]
def inspect!
$stderr.puts(self.inspect)
end
##
# Returns the sum of both the `inserts` and `deletes` counts.
#
# @return [Integer]
def count
inserts.count + deletes.count
end
# Append statements to `inserts`. Statements _should_ be constant
# as variable statements will at best be ignored or at worst raise
# an error when applied.
#
# @param statements [Enumerable, RDF::Statement] Some statements
# @return [self]
def insert(*statements)
coerce_statements(statements) do |stmts|
append_statements :inserts, stmts
end
self
end
alias_method :insert!, :insert
alias_method :<<, :insert
# Append statements to `deletes`. Statements _may_ contain
# variables, although support will depend on the {RDF::Mutable}
# target.
#
# @param statements [Enumerable, RDF::Statement] Some statements
# @return [self]
def delete(*statements)
coerce_statements(statements) do |stmts|
append_statements :deletes, stmts
end
self
end
alias_method :delete!, :delete
alias_method :>>, :delete
private
##
# Append statements to the appropriate target. This is a little
# shim to go in between the other shim and the target.
#
# @param target [Symbol] the method to send
# @param arg [Enumerable, RDF::Statement]
#
def append_statements(target, arg)
# coerce to an enumerator
stmts = case
when arg.is_a?(RDF::Statement)
[arg]
when arg.respond_to?(:each_statement)
arg.each_statement
when arg.respond_to?(:each)
arg
else
raise ArgumentError, "Invalid statement: #{arg.class}"
end
stmts.each { |s| send(target) << s }
end
# This simply returns its argument as a query in order to trick
# {RDF::Mutable#delete} into working.
def query(stmt)
RDF::Query.new RDF::Query::Pattern.from(stmt)
end
undef_method :load
end # Changeset
end # RDF