Skip to content

Commit

Permalink
Refactor Extended Bounded Description to own class
Browse files Browse the repository at this point in the history
Creates an `ExtendedBoundedDescription` class to provide an
`RDF::Enumerable`/`RDF::Queryable` running directly over the source
graph. This avoids the need to copy statements into an array and
enumerate them multiple times.
  • Loading branch information
Tom Johnson committed Jul 26, 2016
1 parent 809b7cf commit 8963a31
Show file tree
Hide file tree
Showing 4 changed files with 184 additions and 41 deletions.
48 changes: 10 additions & 38 deletions lib/active_triples/util/buffered_transaction.rb
Original file line number Diff line number Diff line change
@@ -1,12 +1,9 @@
require 'active_triples/util/extended_bounded_description'

module ActiveTriples
##
# A buffered trasaction for use with `ActiveTriples::ParentStrategy`.
#
# Reads are projected onto a specialized "Extended Bounded Description"
# subgraph. Compare to Concise Bounded Description
# (https://www.w3.org/Submission/CBD/), the common subgraph scope used for
# SPARQL DESCRIBE queries.
#
# If an `ActiveTriples::RDFSource` instance is passed as the underlying
# repository, this transaction will try to find an existing
# `BufferedTransaction` to use as the basis for a snapshot. When the
Expand All @@ -15,7 +12,13 @@ module ActiveTriples
#
# If a `RDF::Transaction::TransactionError` is raised on commit, this
# transaction optimistically attempts to replay the changes.
class BufferedTransaction < RDF::Repository::Implementation::SerializedTransaction
#
# Reads are projected onto a specialized "Extended Bounded Description"
# subgraph.
#
# @see ActiveTriples::Util::ExtendedBoundedDescription
class BufferedTransaction <
RDF::Repository::Implementation::SerializedTransaction
# @!attribute snapshot [r]
# @return RDF::Dataset
# @!attribute subject [r]
Expand Down Expand Up @@ -117,38 +120,7 @@ def execute

def read_target
return super unless subject
extended_bounded_description(super, subject, ancestors.dup)
end

##
# By analogy to Concise Bounded Description.
#
# Include in the subgraph:
# 1. All statements in the source graph where the subject of the statement
# is the starting node.
# 2. Recursively, for all statements already in the subgraph, include in
# the subgraph the Extended Bounded Description for each object node,
# unless the object is in the ancestor's list.
#
# @param target [RDF::Queryable]
# @param subject [RDF::Term]
# @param ancestors [Array<RDF::Term>]
#
# @return [RDF::Enumerable, RDF::Queryable]
def extended_bounded_description(target, subject, ancestors)
statements = RDF::Repository.new << target.query(subject: subject)

ancestors ||= []
ancestors << subject

statements.each_object do |object|
next if object.literal? || ancestors.include?(object)

statements << extended_bounded_description(target, object, ancestors)
ancestors << object
end

statements
ExtendedBoundedDescription.new(super, subject, ancestors)
end
end
end
75 changes: 75 additions & 0 deletions lib/active_triples/util/extended_bounded_description.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
module ActiveTriples
##
# Bounds the scope of an `RDF::Queryable` to a subgraph defined from a source
# graph, a starting node, and a list of "ancestors" terms, by the following
# process:
#
# Include in the subgraph:
# 1. All statements in the source graph where the subject of the statement
# is the starting node.
# 2. Add the starting node to the ancestors list.
# 2. Recursively, for all statements already in the subgraph, include in
# the subgraph the Extended Bounded Description for each object node,
# unless the object is in the ancestors list.
#
# The list of "ancestors" is empty by default.
#
# This subgraph this process yields can be considered as a description of
# the starting node.
#
# Compare to Concise Bounded Description
# (https://www.w3.org/Submission/CBD/), the common subgraph scope used for
# SPARQL DESCRIBE queries.
#
# @note this implementation requires that the `source_graph` remain unchanged
# while iterating over the description. The safest way to achive this is to
# use an immutable `RDF::Dataset` (e.g. a `Repository#snapshot`).
class ExtendedBoundedDescription
include RDF::Enumerable
include RDF::Queryable

##
# @!attribute ancestors [r]
# @return Array<RDF::Term>
# @!attribute source_graph [r]
# @return RDF::Queryable
# @!attribute starting_node [r]
# @return RDF::Term
attr_reader :ancestors, :source_graph, :starting_node

##
# By analogy to Concise Bounded Description.
#
# @param source_graph [RDF::Queryable]
# @param starting_node [RDF::Term]
# @param ancestors [Array<RDF::Term>] default: []
def initialize(source_graph, starting_node, ancestors = [])
@source_graph = source_graph
@starting_node = starting_node
@ancestors = ancestors
end

##
# @see RDF::Enumerable#each
def each_statement
ancestors = @ancestors.dup

if block_given?
statements = source_graph.query(subject: starting_node).each
statements.each_statement { |st| yield st }

ancestors << starting_node

statements.each_object do |object|
next if object.literal? || ancestors.include?(object)
ExtendedBoundedDescription
.new(source_graph, object, ancestors).each do |statement|
yield statement
end
end
end
enum_statement
end
alias_method :each, :each_statement
end
end
4 changes: 1 addition & 3 deletions spec/active_triples/util/buffered_transaction_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -60,9 +60,7 @@
end
end

shared_examples 'an optimist' do

end
shared_examples 'an optimist' do; end

# @see lib/rdf/spec/transaction.rb in rdf-spec
it_behaves_like 'an RDF::Transaction', ActiveTriples::BufferedTransaction
Expand Down
98 changes: 98 additions & 0 deletions spec/active_triples/util/extended_bounded_description_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
# frozen_string_literal: true
require "spec_helper"
require 'rdf/spec/enumerable'
require 'rdf/spec/queryable'
require 'active_triples/util/extended_bounded_description'

describe ActiveTriples::ExtendedBoundedDescription do
subject { described_class.new(source_graph, starting_node, ancestors) }

let(:ancestors) { [] }
let(:source_graph) { RDF::Repository.new }
let(:starting_node) { RDF::Node.new }

it { is_expected.not_to be_mutable }

shared_examples 'a bounded description' do
before do
source_graph.insert(*included_statements)
source_graph.insert(*excluded_statements)
end

it 'projects over a bounded description' do
expect(subject).to contain_exactly(*included_statements)
end

it 'can iterate repeatedly' do
expect(subject).to contain_exactly(*included_statements)
expect(subject).to contain_exactly(*included_statements)
end

it 'is queryable' do
expect(subject.query([nil, nil, nil]))
.to contain_exactly(*included_statements)
end
end

##
# *** We don't pass these RDF::Spec examples.
# They try to test boring things, like having the triples we put in.
#
# @see lib/rdf/spec/enumerable.rb in rdf-spec
# it_behaves_like 'an RDF::Enumerable' do
# let(:graph) { RDF::Repository.new.insert(*statements) }
# let(:statements) { RDF::Spec.quads }
#
# let(:enumerable) do
# ActiveTriples::ExtendedBoundedDescription
# .new(graph, statements.first.subject)
# end
# end
#
# @see lib/rdf/spec/queryable.rb in rdf-spec
# it_behaves_like 'an RDF::Queryable' do
# let(:graph) { RDF::Repository.new.insert(*statements) }
# let(:statements) { RDF::Spec.quads }
#
# let(:queryable) do
# ActiveTriples::ExtendedBoundedDescription
# .new(graph, statements.first.subject)
# end
# end
#
# *** end boring stuff
##

let(:included_statements) do
[RDF::Statement(starting_node, RDF::URI('p1'), 'o'),
RDF::Statement(starting_node, RDF::URI('p2'), 0),
RDF::Statement(starting_node, RDF::URI('p3'), :o1),
RDF::Statement(:o1, RDF::URI('p4'), :o2),
RDF::Statement(:o1, RDF::URI('p5'), 'w0w'),
RDF::Statement(:o2, RDF::URI('p6'), :o1)]
end

let(:excluded_statements) do
[RDF::Statement(RDF::URI('s1'), RDF::URI('p1'), 'o'),
RDF::Statement(:s2, RDF::URI('p2'), 0),
RDF::Statement(:s3, RDF::URI('p3'), :o),
RDF::Statement(:s3, RDF::URI('p3'), starting_node)]
end

it_behaves_like 'a bounded description'

context 'with ancestors' do
before do
included_statements <<
RDF::Statement(starting_node, RDF::URI('a'), ancestor)

excluded_statements <<
RDF::Statement(ancestor, RDF::URI('a'), starting_node)
end

let(:ancestor) { RDF::Node.new(:ancestor) }
let(:ancestors) { [ancestor] }

it_behaves_like 'a bounded description'
end
end

0 comments on commit 8963a31

Please sign in to comment.