├── .rspec ├── .autotest ├── lib ├── tripod │ ├── version.rb │ ├── extensions.rb │ ├── errors │ │ ├── timeout.rb │ │ ├── rdf_parse_failed.rb │ │ ├── field_not_present.rb │ │ ├── sparql_response_too_large.rb │ │ ├── uri_not_set.rb │ │ ├── rdf_type_not_set.rb │ │ ├── graph_uri_not_set.rb │ │ ├── bad_data_request.rb │ │ ├── bad_sparql_request.rb │ │ ├── resource_not_found.rb │ │ ├── validations.rb │ │ └── sparql_query_missing_variables.rb │ ├── callbacks.rb │ ├── locale │ │ └── en.yml │ ├── graphs.rb │ ├── errors.rb │ ├── embedded_resource.rb │ ├── links │ │ ├── linked_from.rb │ │ └── linked_to.rb │ ├── extensions │ │ └── module.rb │ ├── serialization.rb │ ├── rdf_type.rb │ ├── validations │ │ └── is_url.rb │ ├── components.rb │ ├── embeds │ │ └── many.rb │ ├── embeds.rb │ ├── fields │ │ └── standard.rb │ ├── state.rb │ ├── cache_stores │ │ └── memcached_cache_store.rb │ ├── dirty.rb │ ├── streaming.rb │ ├── attributes.rb │ ├── predicates.rb │ ├── sparql_query.rb │ ├── criteria.rb │ ├── resource.rb │ ├── repository.rb │ ├── resource_collection.rb │ ├── criteria │ │ └── execution.rb │ ├── eager_loading.rb │ ├── sparql_client.rb │ ├── persistence.rb │ ├── fields.rb │ ├── links.rb │ └── finders.rb └── tripod.rb ├── spec ├── app │ └── models │ │ ├── flea.rb │ │ ├── resource.rb │ │ ├── dog.rb │ │ └── person.rb ├── tripod │ ├── embedded_resource_spec.rb │ ├── graphs_spec.rb │ ├── validations │ │ └── is_url_spec.rb │ ├── embeds_spec.rb │ ├── dirty_spec.rb │ ├── serialization_spec.rb │ ├── sparql_client_spec.rb │ ├── state_spec.rb │ ├── memcached_cache_store_spec.rb │ ├── streaming_spec.rb │ ├── repository_spec.rb │ ├── resource_spec.rb │ ├── links_spec.rb │ ├── fields_spec.rb │ ├── predicates_spec.rb │ ├── criteria_spec.rb │ ├── attributes_spec.rb │ ├── sparql_query_spec.rb │ ├── eager_loading_spec.rb │ ├── finders_spec.rb │ ├── persistence_spec.rb │ └── criteria_execution_spec.rb └── spec_helper.rb ├── travis ├── run_fuseki.sh ├── bootstrap_fuseki.sh └── config_tripod.ttl ├── .gitignore ├── Gemfile ├── Rakefile ├── .travis.yml ├── LICENSE ├── tripod.gemspec └── README.md /.rspec: -------------------------------------------------------------------------------- 1 | --color -------------------------------------------------------------------------------- /.autotest: -------------------------------------------------------------------------------- 1 | require "autotest/bundler" -------------------------------------------------------------------------------- /lib/tripod/version.rb: -------------------------------------------------------------------------------- 1 | module Tripod 2 | VERSION = "0.18.1" 3 | end 4 | -------------------------------------------------------------------------------- /lib/tripod/extensions.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | require "tripod/extensions/module" -------------------------------------------------------------------------------- /lib/tripod/errors/timeout.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | module Tripod::Errors 3 | class Timeout < StandardError 4 | end 5 | end -------------------------------------------------------------------------------- /lib/tripod/errors/rdf_parse_failed.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | module Tripod::Errors 3 | 4 | class RdfParseFailed < StandardError 5 | end 6 | 7 | end -------------------------------------------------------------------------------- /lib/tripod/errors/field_not_present.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | module Tripod::Errors 3 | 4 | # field not present error. 5 | class FieldNotPresent < StandardError 6 | end 7 | 8 | end -------------------------------------------------------------------------------- /lib/tripod/errors/sparql_response_too_large.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | module Tripod::Errors 3 | 4 | # sparql response too large. 5 | class SparqlResponseTooLarge < StandardError 6 | end 7 | 8 | end -------------------------------------------------------------------------------- /lib/tripod/callbacks.rb: -------------------------------------------------------------------------------- 1 | module Tripod::Callbacks 2 | extend ActiveSupport::Concern 3 | 4 | included do 5 | extend ActiveModel::Callbacks 6 | define_model_callbacks :initialize, :save, :destroy 7 | end 8 | end -------------------------------------------------------------------------------- /lib/tripod/errors/uri_not_set.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | module Tripod::Errors 3 | 4 | # Action attempted on a resource which requires the uri to be set. 5 | class UriNotSet < StandardError 6 | 7 | end 8 | 9 | end -------------------------------------------------------------------------------- /spec/app/models/flea.rb: -------------------------------------------------------------------------------- 1 | class Flea 2 | include Tripod::EmbeddedResource 3 | 4 | rdf_type 'http://example.com/flea' 5 | 6 | field :name, 'http://example.com/flea/name' 7 | validates_presence_of :name 8 | end 9 | -------------------------------------------------------------------------------- /lib/tripod/errors/rdf_type_not_set.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | module Tripod::Errors 3 | 4 | # Action attempted on a resource which requires the rdf type to be set. 5 | class RdfTypeNotSet < StandardError 6 | 7 | end 8 | 9 | end -------------------------------------------------------------------------------- /lib/tripod/errors/graph_uri_not_set.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | module Tripod::Errors 3 | 4 | # Action attempted on a resource which requires the graph uri to be set. 5 | class GraphUriNotSet < StandardError 6 | 7 | end 8 | 9 | end -------------------------------------------------------------------------------- /spec/app/models/resource.rb: -------------------------------------------------------------------------------- 1 | class Resource 2 | 3 | include Tripod::Resource 4 | 5 | field :pref_label, RDF::SKOS.prefLabel 6 | field :label, RDF::RDFS.label 7 | field :title, RDF::DC.title 8 | 9 | end 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /travis/run_fuseki.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | FUSEKI_HOME=/opt/fuseki/jena-fuseki1-1.5.0 4 | cd /opt/fuseki/jena-fuseki1-1.5.0 5 | env JAVA_HOME=/usr/lib/jvm/java-8-oracle/bin/java JVM_ARGS=-Xmx4096M ./fuseki-server --config="/opt/fuseki/config/config.ttl" > /dev/null & -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | *.iml 3 | *.gem 4 | *.rbc 5 | .bundle 6 | .config 7 | .yardoc 8 | Gemfile.lock 9 | InstalledFiles 10 | _yardoc 11 | coverage 12 | doc/ 13 | lib/bundler/man 14 | pkg 15 | rdoc 16 | spec/reports 17 | test/tmp 18 | test/version_tmp 19 | tmp 20 | .DS_Store 21 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Specify your gem's dependencies in tripod.gemspec 4 | gemspec 5 | 6 | gem 'rake' 7 | 8 | group :test do 9 | gem "rspec", "~> 3.11.0" 10 | gem "webmock" 11 | gem 'pry', '~> 0.9.12.6' 12 | gem 'binding_of_caller', '~> 1.0.0' 13 | gem 'rest-client', '2.0.0' 14 | end -------------------------------------------------------------------------------- /spec/tripod/embedded_resource_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | module Tripod 4 | describe EmbeddedResource do 5 | describe 'an instance' do 6 | let(:flea) { Flea.new } 7 | 8 | it 'should have getters & setters on fields' do 9 | flea.name = 'Bob' 10 | expect(flea.name).to eq('Bob') 11 | end 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env rake 2 | require "bundler/gem_tasks" 3 | require "rspec/core/rake_task" 4 | 5 | RSpec::Core::RakeTask.new("spec") do |spec| 6 | spec.pattern = "spec/**/*_spec.rb" 7 | end 8 | 9 | RSpec::Core::RakeTask.new('spec:progress') do |spec| 10 | spec.rspec_opts = %w(--format progress) 11 | spec.pattern = "spec/**/*_spec.rb" 12 | end 13 | 14 | task :default => :spec 15 | 16 | -------------------------------------------------------------------------------- /lib/tripod/errors/bad_data_request.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | module Tripod::Errors 3 | 4 | # field not present error. 5 | class BadDataRequest < StandardError 6 | 7 | attr_accessor :parent_bad_request 8 | 9 | def initialize(message=nil, parent_bad_request_error=nil) 10 | super(message) 11 | parent_bad_request = parent_bad_request_error 12 | end 13 | end 14 | 15 | end -------------------------------------------------------------------------------- /lib/tripod/errors/bad_sparql_request.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | module Tripod::Errors 3 | 4 | # field not present error. 5 | class BadSparqlRequest < StandardError 6 | 7 | attr_accessor :parent_bad_request 8 | 9 | def initialize(message=nil, parent_bad_request_error=nil) 10 | super(message) 11 | parent_bad_request = parent_bad_request_error 12 | end 13 | end 14 | 15 | end -------------------------------------------------------------------------------- /lib/tripod/locale/en.yml: -------------------------------------------------------------------------------- 1 | en: 2 | activemodel: 3 | errors: 4 | # The default format to use in full error messages. 5 | format: "%{attribute} %{message}" 6 | 7 | # The values :model, :attribute and :value are always available for interpolation 8 | # The value :count is available when applicable. Can be used for pluralization. 9 | messages: 10 | is_url: "is not a valid URL" -------------------------------------------------------------------------------- /lib/tripod/errors/resource_not_found.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | module Tripod::Errors 3 | 4 | # not found error. 5 | class ResourceNotFound < StandardError 6 | 7 | attr_accessor :uri 8 | 9 | def initialize(uri=nil) 10 | @uri = uri 11 | end 12 | 13 | def message 14 | msg = "Resource Not Found" 15 | msg += ": #{@uri.to_s}" if @uri 16 | msg 17 | end 18 | end 19 | 20 | end -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | cache: bundler 3 | sudo: required 4 | jdk: 5 | - oraclejdk8 6 | services: 7 | - memcached 8 | - mongodb 9 | - elasticsearch 10 | rvm: 11 | - 3.0.6 12 | 13 | before_install: 14 | - sudo chmod +rx ./travis/* 15 | - sudo chown -R travis ./travis/* 16 | - "./travis/bootstrap_fuseki.sh" 17 | before_script: 18 | - "./travis/run_fuseki.sh" 19 | - sleep 30 20 | script: travis_retry bundle exec rspec spec --fail-fast 21 | 22 | -------------------------------------------------------------------------------- /travis/bootstrap_fuseki.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | mkdir -p /home/travis/tdb_data 4 | mkdir -p /opt/fuseki 5 | mkdir /opt/fuseki/config 6 | sudo apt-get update 7 | sudo apt-get -y install tar wget 8 | wget http://apache.mirror.anlx.net/jena/binaries/jena-fuseki1-1.5.0-distribution.tar.gz -O /opt/fuseki/jena-fuseki-1.5.0.tar.gz 9 | tar -xvzf /opt/fuseki/jena-fuseki-1.5.0.tar.gz -C /opt/fuseki 10 | mv ./travis/config_tripod.ttl /opt/fuseki/config/config.ttl -------------------------------------------------------------------------------- /lib/tripod/graphs.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | # This module defines behaviour for resources with data across multiple graphs 4 | module Tripod::Graphs 5 | extend ActiveSupport::Concern 6 | 7 | def graphs 8 | select_query = "SELECT DISTINCT ?g WHERE { GRAPH ?g {<#{uri.to_s}> ?p ?o } }" 9 | result = Tripod::SparqlClient::Query.select(select_query) 10 | 11 | if result.length > 0 12 | result.select{|r| r.keys.length > 0 }.map{|r| r["g"]["value"] } 13 | else 14 | [] 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/tripod/errors/validations.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | module Tripod::Errors 3 | 4 | # Raised when a persistence method ending in ! fails validation. The message 5 | # will contain the full error messages from the +Resource+ in question. 6 | # 7 | # @example Create the error. 8 | # Validations.new(person.errors) 9 | class Validations < StandardError 10 | attr_reader :resource 11 | alias :record :resource 12 | 13 | def initialize(resource) 14 | @resource = resource 15 | end 16 | end 17 | 18 | end 19 | -------------------------------------------------------------------------------- /lib/tripod/errors.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | require 'tripod/errors/field_not_present' 3 | require 'tripod/errors/resource_not_found' 4 | require 'tripod/errors/uri_not_set' 5 | require 'tripod/errors/graph_uri_not_set' 6 | require 'tripod/errors/rdf_type_not_set' 7 | require 'tripod/errors/validations' 8 | require 'tripod/errors/rdf_parse_failed' 9 | require 'tripod/errors/timeout' 10 | require 'tripod/errors/sparql_response_too_large' 11 | require 'tripod/errors/bad_sparql_request' 12 | require 'tripod/errors/bad_data_request' 13 | require 'tripod/errors/sparql_query_missing_variables' -------------------------------------------------------------------------------- /lib/tripod/errors/sparql_query_missing_variables.rb: -------------------------------------------------------------------------------- 1 | module Tripod 2 | class SparqlQueryMissingVariables < StandardError 3 | attr_reader :missing_variables, :expected_variables, :received_variables 4 | 5 | def initialize(missing_variables, expected_variables, received_variables) 6 | raise ArgumentError.new("Missing parameters should be an array") unless missing_variables.is_a?(Array) 7 | @missing_variables = missing_variables 8 | @expected_variables = expected_variables 9 | @received_variables = received_variables 10 | end 11 | 12 | def to_s 13 | "Missing parameters: #{@missing_variables.map(&:to_s).join(', ')}" 14 | end 15 | end 16 | end -------------------------------------------------------------------------------- /lib/tripod/embedded_resource.rb: -------------------------------------------------------------------------------- 1 | module Tripod::EmbeddedResource 2 | extend ActiveSupport::Concern 3 | 4 | include ActiveModel::Validations 5 | 6 | include Tripod::Predicates 7 | include Tripod::Attributes 8 | include Tripod::Validations 9 | include Tripod::Fields 10 | include Tripod::Dirty 11 | include Tripod::RdfType 12 | 13 | attr_reader :uri 14 | 15 | def initialize(opts={}) 16 | @uri = opts.fetch(:node, RDF::Node.new) # use a blank node for the URI 17 | @repository = opts.fetch(:repository, RDF::Repository.new) 18 | set_rdf_type 19 | end 20 | 21 | def to_statements 22 | @repository.statements 23 | end 24 | 25 | def ==(resource) 26 | (@uri == resource.uri) 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /lib/tripod/links/linked_from.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | module Tripod::Links 3 | # Defines the behaviour for defined links in the resource. 4 | class LinkedFrom 5 | 6 | # Set readers for the instance variables. 7 | attr_accessor :name, :incoming_field, :options, :incoming_field_name, :class_name 8 | 9 | # Create the new link with a name and optional additional options. 10 | def initialize(name, incoming_field_name, options = {}) 11 | @name = name 12 | @options = options 13 | @incoming_field_name = incoming_field_name 14 | # if class name not supplied, guess from the field name 15 | @class_name = options[:class_name] || name.to_s.singularize.classify 16 | 17 | end 18 | end 19 | end -------------------------------------------------------------------------------- /lib/tripod/extensions/module.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | module Tripod::Extensions 3 | module Module 4 | 5 | # Redefine the method. Will undef the method if it exists or simply 6 | # just define it. 7 | # 8 | # @example Redefine the method. 9 | # Object.re_define_method("exists?") do 10 | # self 11 | # end 12 | # 13 | # @param [ String, Symbol ] name The name of the method. 14 | # @param [ Proc ] block The method body. 15 | # 16 | # @return [ Method ] The new method. 17 | def re_define_method(name, &block) 18 | undef_method(name) if method_defined?(name) 19 | define_method(name, &block) 20 | end 21 | end 22 | end 23 | 24 | ::Module.__send__(:include, Tripod::Extensions::Module) 25 | -------------------------------------------------------------------------------- /spec/app/models/dog.rb: -------------------------------------------------------------------------------- 1 | class Dog 2 | 3 | include Tripod::Resource 4 | 5 | rdf_type 'http://example.com/dog' 6 | graph_uri 'http://example.com/graph' 7 | 8 | field :name, 'http://example.com/name' 9 | 10 | linked_to :owner, 'http://example.com/owner', class_name: 'Person' 11 | linked_to :person, 'http://example.com/person' 12 | linked_to :friends, 'http://example.com/friend', multivalued: true, class_name: 'Dog' 13 | linked_to :previous_owner, 'http://example.com/prevowner', class_name: 'Person', field_name: :prev_owner_uri 14 | 15 | linked_to :arch_enemy, 'http://example.com/archenemy', class_name: 'Dog' 16 | linked_to :enemies, 'http://example.com/enemy', class_name: 'Dog' 17 | 18 | embeds :fleas, 'http://example.com/fleas' 19 | end 20 | -------------------------------------------------------------------------------- /lib/tripod/serialization.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | module Tripod::Serialization 4 | extend ActiveSupport::Concern 5 | 6 | # Serialises this resource's triples to rdf/xml 7 | def to_rdf 8 | retrieve_triples_from_database(accept_header="application/rdf+xml") 9 | end 10 | 11 | # Serialises this resource's triples to turtle 12 | def to_ttl 13 | retrieve_triples_from_database(accept_header="text/turtle") 14 | end 15 | 16 | # Serialises this resource's triples to n-triples 17 | def to_nt 18 | retrieve_triples_from_database(accept_header=Tripod.ntriples_header_str) 19 | end 20 | 21 | # Serialises this resource's triples to JSON-LD 22 | def to_json(opts={}) 23 | get_triples_for_this_resource.dump(:jsonld) 24 | end 25 | 26 | def to_text 27 | to_nt 28 | end 29 | 30 | end -------------------------------------------------------------------------------- /lib/tripod/links/linked_to.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | module Tripod::Links 3 | # Defines the behaviour for defined links in the resource. 4 | class LinkedTo 5 | 6 | # Set readers for the instance variables. 7 | attr_accessor :name, :predicate, :options, :multivalued, :field_name, :class_name 8 | alias_method :multivalued?, :multivalued 9 | 10 | # Create the new link with a name and optional additional options. 11 | def initialize(name, predicate, options = {}) 12 | @name = name 13 | @options = options 14 | @predicate = RDF::URI.new(predicate.to_s) 15 | @multivalued = options[:multivalued] || false 16 | @class_name = options[:class_name] || @name.to_s.classify 17 | @field_name = options[:field_name] || (@name.to_s + ( @multivalued ? "_uris" : "_uri" )).to_sym 18 | 19 | end 20 | end 21 | end -------------------------------------------------------------------------------- /spec/app/models/person.rb: -------------------------------------------------------------------------------- 1 | class Person 2 | 3 | include Tripod::Resource 4 | 5 | rdf_type 'http://example.com/person' 6 | graph_uri 'http://example.com/graph' 7 | 8 | field :name, 'http://example.com/name' 9 | field :father, 'http://example.com/father', :is_uri => true 10 | field :knows, 'http://example.com/knows', :multivalued => true, :is_uri => true 11 | field :aliases, 'http://exmample.com/alias', :multivalued => true 12 | field :age, 'http://example.com/age', :datatype => RDF::XSD.integer 13 | field :important_dates, 'http://example.com/importantdates', :datatype => RDF::XSD.date, :multivalued => true 14 | 15 | linked_from :owns_dogs, :owner, class_name: 'Dog' 16 | linked_from :dogs, :person 17 | 18 | before_save :pre_save 19 | before_destroy :pre_destroy 20 | 21 | def pre_save;; end 22 | def pre_destroy;; end 23 | end -------------------------------------------------------------------------------- /lib/tripod/rdf_type.rb: -------------------------------------------------------------------------------- 1 | module Tripod::RdfType 2 | extend ActiveSupport::Concern 3 | 4 | included do 5 | # every instance of a resource has an rdf type field, which is set at the class level 6 | class_attribute :_RDF_TYPE 7 | end 8 | 9 | def set_rdf_type 10 | self.rdf_type = self.class.get_rdf_type if respond_to?(:rdf_type=) && self.class.get_rdf_type 11 | end 12 | 13 | module ClassMethods 14 | # makes a "field" on this model called rdf_type 15 | # and sets a class level _RDF_TYPE variable with the rdf_type passed in. 16 | def rdf_type(new_rdf_type) 17 | self._RDF_TYPE = RDF::URI.new(new_rdf_type.to_s) 18 | field :rdf_type, RDF.type, :multivalued => true, :is_uri => true # things can have more than 1 type and often do 19 | end 20 | 21 | def get_rdf_type 22 | self._RDF_TYPE 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/tripod/validations/is_url.rb: -------------------------------------------------------------------------------- 1 | module Tripod::Validations 2 | class IsUrlValidator < ActiveModel::EachValidator 3 | def validate_each(resource, attribute, value) 4 | return unless value # nil values get passed over. 5 | is_valid = value.is_a?(Array) ? value.all?{|v| is_url?(v)} : is_url?(value) 6 | resource.errors.add(attribute, :is_url) unless is_valid 7 | end 8 | 9 | private 10 | 11 | def is_url?(value) 12 | uri = nil 13 | begin 14 | uri = URI.parse(value.to_s) 15 | rescue 16 | return false 17 | end 18 | return false unless ['http', 'https', 'mailto'].include?(uri.scheme) 19 | unless uri.scheme == "mailto" 20 | return false unless uri.host && (uri.host.split('.').length > 1 || uri.host.split('.')[0] == 'localhost') 21 | end 22 | true 23 | end 24 | end 25 | end -------------------------------------------------------------------------------- /spec/tripod/graphs_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe Tripod::Graphs do 4 | describe "#graphs" do 5 | let(:uri) { 'http://example.com/foobar' } 6 | let(:graph_uri) { 'http://example.com/irl' } 7 | let(:another_graph_uri) { 'http://example.com/make-believe' } 8 | let(:person) do 9 | p = Person.new(uri, graph_uri: graph_uri) 10 | p.write_predicate('http://example.com/vocation', RDF::URI.new('http://example.com/accountant')) 11 | p.save! 12 | p 13 | end 14 | 15 | before do 16 | p2 = Person.new(uri, graph_uri: another_graph_uri) 17 | p2.write_predicate('http://example.com/vocation', RDF::URI.new('http://example.com/lion-tamer')) 18 | p2.save! 19 | p2 20 | end 21 | 22 | it 'should return an array of all the graphs for which there are triples about this URI' do 23 | person.graphs.should =~ [graph_uri, another_graph_uri] 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /lib/tripod/components.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | # All modules that a +Resource+ is composed of are defined in this 4 | # module, to keep the resource module from getting too cluttered. 5 | module Tripod::Components 6 | extend ActiveSupport::Concern 7 | 8 | included do 9 | end 10 | 11 | include ActiveModel::Conversion # to_param, to_key etc. 12 | # include ActiveModel::MassAssignmentSecurity 13 | include ActiveModel::Naming 14 | include ActiveModel::Validations 15 | 16 | include Tripod::Predicates 17 | include Tripod::Attributes 18 | include Tripod::Callbacks 19 | include Tripod::Validations 20 | include Tripod::RdfType 21 | include Tripod::Persistence 22 | include Tripod::Fields 23 | include Tripod::Links 24 | include Tripod::Embeds 25 | include Tripod::Dirty 26 | include Tripod::Finders 27 | include Tripod::Repository 28 | include Tripod::EagerLoading 29 | include Tripod::Serialization 30 | include Tripod::State 31 | include Tripod::Graphs 32 | end 33 | -------------------------------------------------------------------------------- /spec/tripod/validations/is_url_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe Tripod::Validations::IsUrlValidator do 4 | let(:person) { Person.new('http://example.com/barry') } 5 | 6 | it 'should be valid given a valid URL' do 7 | person.father = 'http://example.com/bob' 8 | person.should be_valid 9 | end 10 | 11 | it 'should be valid given a valid mailto URL' do 12 | person.father = 'mailto:hello@swirrl.com' 13 | person.should be_valid 14 | end 15 | 16 | it 'should invalidate given a non-http(s) URL' do 17 | person.father = 'ftp://example.com/bob.nt' 18 | person.should_not be_valid 19 | end 20 | 21 | it 'should invalidate given something unlike a URL' do 22 | person.father = 'http:Bob' 23 | person.should_not be_valid 24 | end 25 | 26 | it 'should invalidate given a domain without a TLD' do 27 | person.father = 'http://bob' 28 | person.should_not be_valid 29 | end 30 | 31 | it "should be valid with a port in the host" do 32 | person.father = 'http://localhost:3000/bob' 33 | person.should be_valid 34 | end 35 | end -------------------------------------------------------------------------------- /lib/tripod/embeds/many.rb: -------------------------------------------------------------------------------- 1 | module Tripod::Embeds 2 | class Many 3 | include Enumerable 4 | 5 | def initialize(klass, predicate, parent) 6 | @parent = parent 7 | @predicate = predicate 8 | nodes = @parent.read_predicate(@predicate) # gets the UUIDs of the associated blank nodes 9 | @resources = nodes.map do |node| 10 | repository = RDF::Repository.new 11 | @parent.repository.query([node, :predicate, :object]) {|statement| repository << statement} 12 | klass.new(node: node, repository: repository) 13 | end 14 | end 15 | 16 | def each(&block) 17 | @resources.each(&block) 18 | end 19 | 20 | def <<(resource) 21 | @parent.repository.insert(*resource.to_statements) 22 | @parent.append_to_predicate(@predicate, resource.uri) 23 | end 24 | 25 | def delete(resource) 26 | statements = @parent.repository.query([resource.uri, :predicate, :object]).to_a 27 | statements << [@parent.uri, RDF::URI.new(@predicate), resource.uri] 28 | @parent.repository.delete(*statements) 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012 Swirrl IT Limited. http://swirrl.com 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /lib/tripod/embeds.rb: -------------------------------------------------------------------------------- 1 | module Tripod::Embeds 2 | extend ActiveSupport::Concern 3 | 4 | included do 5 | validate :embedded_are_valid 6 | end 7 | 8 | def get_embeds(name, predicate, opts) 9 | klass = opts.fetch(:class, nil) 10 | klass ||= (self.class.name.deconstantize + '::' + name.to_s.classify).constantize 11 | Many.new(klass, predicate, self) 12 | end 13 | 14 | def embedded_are_valid 15 | self.class.get_embedded.each do |name| 16 | self.errors.add(name, 'contains an invalid resource') unless self.send(name).all? {|resource| resource.valid? } 17 | end 18 | end 19 | 20 | module ClassMethods 21 | def embeds(name, predicate, opts={}) 22 | re_define_method name do 23 | get_embeds(name, predicate, opts) 24 | end 25 | 26 | # use this as a way to get to all the embedded properties for validation 27 | @_EMBEDDED ||= [] 28 | @_EMBEDDED << name 29 | 30 | # add statements to our hydrate query so the repository is populated appropriately 31 | append_to_hydrate_construct ->(u) { "#{ u } <#{ predicate.to_s }> ?es . ?es ?ep ?eo ." } 32 | append_to_hydrate_where ->(u) { "OPTIONAL { #{ u } <#{ predicate.to_s }> ?es . ?es ?ep ?eo . }" } 33 | end 34 | 35 | def get_embedded 36 | @_EMBEDDED || [] 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /lib/tripod/fields/standard.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | module Tripod::Fields 3 | # Defines the behaviour for defined fields in the resource. 4 | class Standard 5 | 6 | # Set readers for the instance variables. 7 | attr_accessor :name, :predicate, :options, :datatype, :is_uri, :multivalued 8 | alias_method :is_uri?, :is_uri 9 | alias_method :multivalued?, :multivalued 10 | 11 | # Create the new field with a name and optional additional options. 12 | # 13 | # @example Create the new field. 14 | # Field.new(:name, 'http://foo', opts) 15 | # 16 | # @param [ String ] name The field name. 17 | # @param [ String, RDF::URI ] predicate The field's predicate. 18 | # @param [ Hash ] options The field options. 19 | # 20 | # @option options [ String, RDF::URI ] datatype The uri of the datatype for the field (will be used to create an RDF::Literal of the right type on the way in only). 21 | # @option options [ Boolean ] multivalued Is this a multi-valued field? Default is false. 22 | def initialize(name, predicate, options = {}) 23 | @name = name 24 | @options = options 25 | @predicate = RDF::URI.new(predicate.to_s) 26 | @datatype = RDF::URI.new(options[:datatype].to_s) if options[:datatype] 27 | @is_uri = !!options[:is_uri] 28 | @multivalued = options[:multivalued] || false 29 | end 30 | end 31 | end -------------------------------------------------------------------------------- /tripod.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | require File.expand_path('../lib/tripod/version', __FILE__) 3 | 4 | Gem::Specification.new do |gem| 5 | gem.authors = ["Ric Roberts", "Bill Roberts", "Asa Calow"] 6 | gem.email = ["ric@swirrl.com"] 7 | gem.description = %q{RDF ruby ORM} 8 | gem.summary = %q{Active Model style RDF ORM} 9 | gem.homepage = "http://github.com/Swirrl/tripod" 10 | 11 | gem.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) } 12 | gem.files = `git ls-files`.split("\n") 13 | gem.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") 14 | gem.name = "tripod" 15 | gem.require_paths = ["lib"] 16 | gem.license = 'MIT' 17 | gem.version = Tripod::VERSION 18 | 19 | gem.required_rubygems_version = ">= 1.3.6" 20 | 21 | gem.add_dependency "rest-client" 22 | gem.add_dependency "activemodel", ">= 5.2", "<= 6.2" 23 | gem.add_dependency "activesupport", ">= 5.2", "<= 6.2" 24 | gem.add_dependency "equivalent-xml" 25 | gem.add_dependency "rdf", ">= 3.2.0", "<=3.3.0" 26 | gem.add_dependency "rdf-rdfxml", ">= 3.2.0", "<=3.3.0" 27 | gem.add_dependency "rdf-turtle", ">= 3.2.0", "<=3.3.0" 28 | gem.add_dependency "rdf-json", ">= 3.2.0", "<=3.3.0" 29 | gem.add_dependency "json-ld", "~> 3.3.1" 30 | gem.add_dependency "guid" 31 | gem.add_dependency "dalli", "~> 2.7.0" 32 | gem.add_dependency "connection_pool", "~> 2.2" 33 | end 34 | -------------------------------------------------------------------------------- /lib/tripod/state.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | # This module contains the behaviour for getting the various states through which a 4 | # resource can transition. 5 | module Tripod::State 6 | 7 | extend ActiveSupport::Concern 8 | 9 | attr_writer :destroyed, :new_record 10 | 11 | # Returns true if the +Resource+ has not been persisted to the database, 12 | # false if it has. This is determined by the variable @new_record 13 | # and NOT if the object has an id. 14 | # 15 | # @example Is the resource new? 16 | # person.new_record? 17 | # 18 | # @return [ true, false ] True if new, false if not. 19 | def new_record? 20 | @new_record ||= false 21 | end 22 | 23 | # Checks if the resource has been saved to the database. Returns false 24 | # if the resource has been destroyed. 25 | # 26 | # @example Is the resource persisted? 27 | # person.persisted? 28 | # 29 | # @return [ true, false ] True if persisted, false if not. 30 | def persisted? 31 | !new_record? && !destroyed? 32 | end 33 | 34 | # Returns true if the +Resource+ has been succesfully destroyed, and false 35 | # if it hasn't. This is determined by the variable @destroyed and NOT 36 | # by checking the database. 37 | # 38 | # @example Is the resource destroyed? 39 | # person.destroyed? 40 | # 41 | # @return [ true, false ] True if destroyed, false if not. 42 | def destroyed? 43 | @destroyed ||= false 44 | end 45 | alias :deleted? :destroyed? 46 | 47 | end -------------------------------------------------------------------------------- /spec/tripod/embeds_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Tripod::Embeds do 4 | let(:uri) { 'http://example.com/id/spot' } 5 | let(:dog) { 6 | d = Dog.new(uri) 7 | d.name = "Spot" 8 | d 9 | } 10 | let(:flea) { 11 | f = Flea.new 12 | f.name = 'Starsky' 13 | f 14 | } 15 | 16 | it 'should set and get embedded resources through the proxy' do 17 | dog.fleas << flea 18 | expect(dog.fleas.include?(flea)).to eq(true) 19 | end 20 | 21 | it 'should validate embedded resources' do 22 | dog.fleas << Flea.new 23 | expect(dog.valid?).to eq(false) 24 | end 25 | 26 | context 'given a saved instance' do 27 | before do 28 | dog.fleas << flea 29 | dog.save 30 | end 31 | 32 | context 'retrieved by uri' do 33 | let(:dogg) { Dog.find(uri) } 34 | 35 | it 'should hydrate embedded resources from the triple store' do 36 | f = dogg.fleas.first 37 | expect(f.name).to eq(flea.name) 38 | end 39 | end 40 | 41 | context 'retrieved as part of a resource collection' do 42 | let(:dogg) { Dog.all.resources.first } 43 | 44 | it 'should hydrate embedded resources from the triple store' do 45 | f = dogg.fleas.first 46 | expect(f.name).to eq(flea.name) 47 | end 48 | end 49 | end 50 | 51 | 52 | describe 'delete' do 53 | before do 54 | dog.fleas << flea 55 | end 56 | 57 | it 'should remove all trace of the resource' do 58 | dog.fleas.delete(flea) 59 | expect(dog.fleas.include?(flea)).to eq(false) 60 | end 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /spec/tripod/dirty_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Tripod::Dirty do 4 | let(:dog) { Dog.new('http://example.com/dog/spot') } 5 | 6 | describe '#changes' do 7 | before do 8 | dog.name = 'Spot' 9 | end 10 | 11 | it 'should report the original and current values for a changed field' do 12 | expect(dog.changes[:name]).to eq([nil, 'Spot']) 13 | end 14 | 15 | context 'when the field is set more than once' do 16 | before do 17 | dog.name = 'Zit' 18 | end 19 | 20 | it 'should still report the original value correctly' do 21 | expect(dog.changes[:name]).to eq([nil, 'Zit']) 22 | end 23 | end 24 | 25 | context 'when the field is set back to its original value' do 26 | before do 27 | dog.name = nil 28 | end 29 | 30 | it 'should no longer report a change to the field' do 31 | expect(dog.changes.keys).to_not include(:name) 32 | end 33 | end 34 | 35 | context 'on save' do 36 | before { dog.save } 37 | 38 | it 'should reset changes' do 39 | expect(dog.changes).to be_empty 40 | end 41 | end 42 | end 43 | 44 | context 'field methods' do 45 | before { dog.name = 'Wrex' } 46 | 47 | it 'should create a _change method' do 48 | expect(dog.name_change).to eq([nil, 'Wrex']) 49 | end 50 | 51 | it 'should create a _changed? method' do 52 | expect(dog.name_changed?).to eq(true) 53 | end 54 | 55 | it 'should create a _was method' do 56 | expect(dog.name_was).to eq(nil) 57 | end 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.unshift(File.dirname(__FILE__)) 2 | $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), "..", "lib")) 3 | 4 | MODELS = File.join(File.dirname(__FILE__), "app/models") 5 | $LOAD_PATH.unshift(MODELS) 6 | 7 | require 'tripod' 8 | require 'rspec' 9 | require 'webmock/rspec' 10 | require 'binding_of_caller' 11 | require 'pry' 12 | 13 | RSpec.configure do |config| 14 | config.mock_with :rspec 15 | 16 | config.before(:each) do 17 | WebMock.disable! 18 | # delete from all graphs. 19 | # delete from default graph: 20 | # Tripod::SparqlClient::Update.update(' 21 | # DELETE {?s ?p ?o} WHERE {?s ?p ?o}') 22 | # # delete from named graphs: 23 | # Tripod::SparqlClient::Update.update(' 24 | # DELETE {graph ?g {?s ?p ?o}} WHERE {graph ?g {?s ?p ?o}}; 25 | # ') 26 | Tripod::SparqlClient::Update.update('drop default') 27 | Tripod::SparqlClient::Update.update('drop all') 28 | end 29 | 30 | end 31 | 32 | # configure any settings for testing... 33 | Tripod.configure do |config| 34 | config.update_endpoint = 'http://127.0.0.1:3030/tripod-test/update' 35 | config.query_endpoint = 'http://127.0.0.1:3030/tripod-test/sparql' 36 | config.data_endpoint = 'http://127.0.0.1:3030/tripod-test/data' 37 | 38 | # config.update_endpoint = 'http://localhost:5820/test/update' 39 | # config.query_endpoint = 'http://localhost:5820/test/query' 40 | #config.data_endpoint = 'http://127.0.0.1:3030/tripod-test/data' 41 | end 42 | 43 | # Autoload every model for the test suite that sits in spec/app/models. 44 | Dir[ File.join(MODELS, "*.rb") ].sort.each do |file| 45 | name = File.basename(file, ".rb") 46 | autoload name.camelize.to_sym, name 47 | end 48 | -------------------------------------------------------------------------------- /spec/tripod/serialization_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe Tripod::Serialization do 4 | 5 | let(:person) do 6 | 7 | p2 = Person.new('http://example.com/fred') 8 | p2.name = "fred" 9 | p2.save! 10 | 11 | p = Person.new('http://example.com/garry') 12 | p.name = 'Garry' 13 | p.age = 30 14 | p.knows = p2.uri 15 | p 16 | end 17 | 18 | shared_examples_for "a serialisable resource" do 19 | describe "#to_rdf" do 20 | it "should get the data from the database as rdf/xml" do 21 | person.to_rdf.should == person.retrieve_triples_from_database(accept_header="application/rdf+xml") 22 | end 23 | end 24 | 25 | describe "#to_ttl" do 26 | it "should get the data from the database as text/turtle" do 27 | person.to_ttl.should == person.retrieve_triples_from_database(accept_header="text/turtle") 28 | end 29 | end 30 | 31 | describe "#to_nt" do 32 | it "should get the data from the database as application/n-triples" do 33 | person.to_nt.should == person.retrieve_triples_from_database(accept_header="application/n-triples") 34 | end 35 | end 36 | 37 | describe "#to_json" do 38 | it "should dump the triples for this resource only as json-ld" do 39 | person.to_json.should == person.get_triples_for_this_resource.dump(:jsonld) 40 | end 41 | end 42 | end 43 | 44 | context "where no eager loading has happened" do 45 | it_should_behave_like "a serialisable resource" 46 | end 47 | 48 | context "where eager loading has happened" do 49 | 50 | before do 51 | person.eager_load_predicate_triples! 52 | person.eager_load_object_triples! 53 | end 54 | 55 | it_should_behave_like "a serialisable resource" 56 | 57 | end 58 | 59 | end 60 | -------------------------------------------------------------------------------- /lib/tripod/cache_stores/memcached_cache_store.rb: -------------------------------------------------------------------------------- 1 | require 'dalli' 2 | require 'connection_pool' 3 | 4 | module Tripod 5 | module CacheStores 6 | 7 | # A Tripod::CacheStore that uses Memcached. 8 | # Note: Make sure you set the memcached -I (slab size) to big enough to store each result, 9 | # and set the -m (total size) to something quite big (or the cache will recycle too often). 10 | class MemcachedCacheStore 11 | 12 | # initialize a memcached cache store at the specified port (default 'localhost:11211') 13 | # a pool size should also be specified (defaults to 1 for development/local use reasons) 14 | def initialize(location, size = 1) 15 | @dalli_pool = ConnectionPool.new(:size => size, :timeout => 3) { Dalli::Client.new(location, :value_max_bytes => Tripod.response_limit_bytes) } 16 | end 17 | 18 | # takes a block 19 | def fetch(key) 20 | raise ArgumentError.new("expected a block") unless block_given? 21 | 22 | @dalli_pool.with do |client| 23 | client.fetch(key) { yield } 24 | end 25 | end 26 | 27 | def exist?(key) 28 | @dalli_pool.with do |client| 29 | !!client.get(key) 30 | end 31 | end 32 | 33 | def write(key, data) 34 | @dalli_pool.with do |client| 35 | client.set(key, data) 36 | end 37 | end 38 | 39 | def read(key) 40 | @dalli_pool.with do |client| 41 | client.get(key) 42 | end 43 | end 44 | 45 | def stats 46 | output = [] 47 | @dalli_pool.with do |client| 48 | output << client.stats 49 | end 50 | output 51 | end 52 | 53 | def clear! 54 | @dalli_pool.with do |client| 55 | client.flush 56 | end 57 | end 58 | 59 | end 60 | end 61 | end -------------------------------------------------------------------------------- /lib/tripod/dirty.rb: -------------------------------------------------------------------------------- 1 | module Tripod::Dirty 2 | extend ActiveSupport::Concern 3 | 4 | def changed_attributes 5 | @changed_attributes ||= {} 6 | end 7 | 8 | def changed 9 | changed_attributes.keys 10 | end 11 | 12 | def changes 13 | changed.reduce({}) do |memo, attr| 14 | change = attribute_change(attr) 15 | memo[attr] = change if change 16 | memo 17 | end 18 | end 19 | 20 | def attribute_will_change!(attr) 21 | changed_attributes[attr] = read_attribute(attr) unless changed_attributes.has_key?(attr) 22 | end 23 | 24 | def attribute_change(attr) 25 | [ changed_attributes[attr], read_attribute(attr) ] if attribute_changed?(attr) 26 | end 27 | 28 | def attribute_changed?(attr) 29 | return false unless changed_attributes.has_key?(attr) 30 | (changed_attributes[attr] != read_attribute(attr)) 31 | end 32 | 33 | def post_persist 34 | changed_attributes.clear 35 | end 36 | 37 | module ClassMethods 38 | def create_dirty_methods(name, meth) 39 | create_dirty_change_check(name, meth) 40 | create_dirty_change_accessor(name, meth) 41 | create_dirty_was_accessor(name, meth) 42 | end 43 | 44 | def create_dirty_change_accessor(name, meth) 45 | generated_methods.module_eval do 46 | re_define_method("#{meth}_change") do 47 | attribute_change(name) 48 | end 49 | end 50 | end 51 | 52 | def create_dirty_change_check(name, meth) 53 | generated_methods.module_eval do 54 | re_define_method("#{meth}_changed?") do 55 | attribute_changed?(name) 56 | end 57 | end 58 | end 59 | 60 | def create_dirty_was_accessor(name, meth) 61 | generated_methods.module_eval do 62 | re_define_method("#{meth}_was") do 63 | changed_attributes[name] 64 | end 65 | end 66 | end 67 | end 68 | end 69 | -------------------------------------------------------------------------------- /spec/tripod/sparql_client_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | module Tripod::SparqlClient 4 | describe Update do 5 | describe '.update' do 6 | context 'given a valid SPARQL query' do 7 | let(:uri) { RDF::URI.new("http://example.com/me") } 8 | let(:query) { "INSERT DATA { GRAPH { #{uri.to_base} \"world\" . } }" } 9 | 10 | it 'should return true' do 11 | Update.update(query).should == true 12 | end 13 | 14 | it 'should execute the update' do 15 | Update.update(query) 16 | Resource.find(uri).should_not be_nil 17 | end 18 | 19 | context 'and some additional endpoint params' do 20 | it 'should include the additional params in the query payload' 21 | end 22 | end 23 | end 24 | 25 | describe '.query' do 26 | let(:port) { 8080 } 27 | before do 28 | @query_endpoint = Tripod.query_endpoint 29 | Tripod.query_endpoint = "http://localhost:#{port}/sparql/query" 30 | @server_thread = Thread.new do 31 | Timeout::timeout(5) do 32 | listener = TCPServer.new port 33 | client = listener.accept 34 | 35 | # read request 36 | loop do 37 | line = client.gets 38 | break if line =~ /^\s*$/ 39 | end 40 | 41 | # write response 42 | client.puts "HTTP/1.1 503 Timeout" 43 | client.puts "Content-Length: 0" 44 | client.puts 45 | client.puts 46 | end 47 | end 48 | end 49 | 50 | after do 51 | Tripod.query_endpoint = @query_endpoint 52 | @server_thread.join 53 | end 54 | 55 | it 'should raise timeout error' do 56 | expect { Tripod::SparqlClient::Query.query('SELECT * WHERE { ?s ?p ?o }', 'application/n-triples') }.to raise_error(Tripod::Errors::Timeout) 57 | end 58 | end 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /spec/tripod/state_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe Tripod::State do 4 | 5 | describe "#new_record?" do 6 | 7 | context "when calling new on the resource" do 8 | 9 | let(:person) do 10 | Person.new('http://example.com/uri', 'http://example.com/graph') 11 | end 12 | 13 | it "returns true" do 14 | person.should be_a_new_record 15 | end 16 | end 17 | 18 | context "when the object has been saved" do 19 | 20 | let(:person) do 21 | p = Person.new('http://example.com/uri', 'http://example.com/graph') 22 | p.save 23 | p 24 | end 25 | 26 | it "returns false" do 27 | person.should_not be_a_new_record 28 | end 29 | end 30 | end 31 | 32 | describe "#persisted?" do 33 | 34 | let(:person) do 35 | Person.new('http://example.com/uri', 'http://example.com/graph') 36 | end 37 | 38 | it "delegates to new_record?" do 39 | person.should_not be_persisted 40 | end 41 | 42 | context "when the object has been destroyed" do 43 | before do 44 | person.save.should == true # check it worked 45 | person.destroy 46 | end 47 | 48 | it "returns false" do 49 | person.should_not be_persisted 50 | end 51 | end 52 | end 53 | 54 | describe "destroyed?" do 55 | 56 | let(:person) do 57 | Person.new('http://example.com/uri', 'http://example.com/graph') 58 | end 59 | 60 | context "when destroyed is true" do 61 | 62 | before do 63 | person.destroyed = true 64 | end 65 | 66 | it "returns true" do 67 | person.should be_destroyed 68 | end 69 | end 70 | 71 | context "when destroyed is false" do 72 | 73 | before do 74 | person.destroyed = false 75 | end 76 | 77 | it "returns true" do 78 | person.should_not be_destroyed 79 | end 80 | end 81 | 82 | context "when destroyed is nil" do 83 | 84 | before do 85 | person.destroyed = nil 86 | end 87 | 88 | it "returns false" do 89 | person.should_not be_destroyed 90 | end 91 | end 92 | end 93 | end 94 | -------------------------------------------------------------------------------- /spec/tripod/memcached_cache_store_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe Tripod::CacheStores, :caching_tests => true do 4 | 5 | let(:query) { "SELECT * WHERE {?s ?p ?o}" } 6 | let(:accept_header) { "application/sparql-results+json" } 7 | let(:params) { {:query => query}.to_query } 8 | let(:streaming_opts) { {:accept => accept_header, :timeout_seconds => Tripod.timeout_seconds} } 9 | 10 | before do 11 | Tripod.cache_store = Tripod::CacheStores::MemcachedCacheStore.new('localhost:11211', 10) 12 | 13 | p = Person.new('http://example.com/id/garry') 14 | p.name = "garry" 15 | p.save! 16 | p 17 | end 18 | 19 | # if Tripod cache_store is not reset to nil, other tests will fail due to caching 20 | after(:all) do 21 | Tripod.cache_store = nil 22 | end 23 | 24 | describe "sending a query with caching enabled" do 25 | before do 26 | @query_result = Tripod::SparqlClient::Query.query(query, accept_header) 27 | @cache_key = 'SPARQL-QUERY-' + Digest::SHA2.hexdigest([accept_header, query].join("-")) 28 | @stream_data = -> { Tripod::Streaming.get_data(Tripod.query_endpoint, params, streaming_opts) } 29 | end 30 | 31 | it "should set the data in the cache" do 32 | Tripod.cache_store.fetch(@cache_key, &@stream_data).should_not be_nil 33 | end 34 | 35 | describe "with a large number of subsequent requests" do 36 | before do 37 | @number_of_memcache_get_calls = Tripod.cache_store.stats[0]["localhost:11211"]["cmd_get"] 38 | @number_of_memcache_set_calls = Tripod.cache_store.stats[0]["localhost:11211"]["cmd_set"] 39 | 40 | 100.times do 41 | Thread.new do 42 | Tripod::SparqlClient::Query.query(query, accept_header) 43 | end 44 | end 45 | end 46 | 47 | it "should increase number of memcache get calls" do 48 | Tripod.cache_store.stats[0]["localhost:11211"]["cmd_get"].to_i.should be > @number_of_memcache_get_calls.to_i 49 | end 50 | 51 | it "should not increase cache size" do 52 | Tripod.cache_store.stats[0]["localhost:11211"]["cmd_set"].to_i.should == @number_of_memcache_set_calls.to_i 53 | end 54 | end 55 | end 56 | 57 | end -------------------------------------------------------------------------------- /spec/tripod/streaming_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe Tripod::Streaming do 4 | 5 | let(:url) { 'http://example.com' } 6 | let(:query) { 'select * where {?s ?p ?o}' } 7 | let(:response_length) { 64.kilobytes } 8 | 9 | before do 10 | WebMock.enable! 11 | stub_http_request(:post, url).with(:body => query, :headers => {'Accept' => "*/*"}).to_return(:body => ("0" * response_length)) 12 | end 13 | 14 | describe ".get_data" do 15 | it "should make a request to the url passed in" do 16 | Tripod::Streaming.get_data(url, query) 17 | end 18 | 19 | context "with timeout option" do 20 | it "should set the read_timeout to that value" do 21 | Net::HTTP.any_instance.should_receive(:read_timeout=).with(28) 22 | Tripod::Streaming.get_data(url, query, :timeout_seconds => 28) 23 | end 24 | end 25 | 26 | context "with no timeout option" do 27 | it "should set the read_timeout to the default (10s)" do 28 | Net::HTTP.any_instance.should_receive(:read_timeout=).with(10) 29 | Tripod::Streaming.get_data(url, query) 30 | end 31 | end 32 | 33 | context "with an accept header option" do 34 | it "should use that header for the request " do 35 | stub_http_request(:post, url).with(:body => query, :headers => {'Accept' => "application/json"}) 36 | Tripod::Streaming.get_data(url, query, :accept => 'application/json') 37 | end 38 | end 39 | 40 | # these tests actually download remote resources (from jQuery's CDN) to test the streaming bits 41 | # TODO: move this out so it doesn't run with the normal rake task?? 42 | context "streaming" do 43 | context "with no limit" do 44 | it "should return the full body" do 45 | response = Tripod::Streaming.get_data(url, query, :no_response_limit => true) 46 | response.length.should == response_length 47 | end 48 | end 49 | 50 | context "with a limit" do 51 | it "should raise an exception if it's bigger than the limit" do 52 | lambda { 53 | Tripod::Streaming.get_data(url, query, :response_limit_bytes => 32.kilobytes) 54 | }.should raise_error(Tripod::Errors::SparqlResponseTooLarge) 55 | end 56 | end 57 | end 58 | end 59 | 60 | after do 61 | WebMock.disable! 62 | end 63 | 64 | end 65 | -------------------------------------------------------------------------------- /travis/config_tripod.ttl: -------------------------------------------------------------------------------- 1 | @prefix tdb: . 2 | @prefix rdf: . 3 | @prefix rdfs: . 4 | @prefix ja: . 5 | @prefix fuseki: . 6 | 7 | [] rdf:type fuseki:Server ; 8 | # Services available. Only explicitly listed services are configured. 9 | # If there is a service description not linked from this list, it is ignored. 10 | fuseki:services ( 11 | <#tripod-development> 12 | <#tripod-test> 13 | ) . 14 | 15 | [] ja:loadClass "com.hp.hpl.jena.tdb.TDB" . 16 | tdb:DatasetTDB rdfs:subClassOf ja:RDFDataset . 17 | tdb:GraphTDB rdfs:subClassOf ja:Model . 18 | 19 | <#tripod-development> rdf:type fuseki:Service ; 20 | fuseki:name "tripod-development" ; # http://host:port/pmddev 21 | fuseki:serviceQuery "sparql" ; # SPARQL query service http://host:port/pmddev/sparql?query=... 22 | fuseki:serviceUpdate "update" ; # SPARQL update servicehttp://host:port/pmddev/update?query= 23 | fuseki:serviceReadWriteGraphStore "data" ; # SPARQL Graph store protocol (read and write) 24 | fuseki:dataset <#dataset-tripod-development> ; 25 | . 26 | 27 | <#dataset-tripod-development> rdf:type tdb:DatasetTDB ; 28 | tdb:location "/home/travis/tdb_data/tripod-development" ; # change to suit your local installation 29 | # Query timeout on this dataset (1s, 1000 milliseconds) 30 | ja:context [ ja:cxtName "arq:queryTimeout" ; ja:cxtValue "10000" ] ; 31 | tdb:unionDefaultGraph true ; 32 | . 33 | 34 | <#tripod-test> rdf:type fuseki:Service ; 35 | fuseki:name "tripod-test" ; # http://host:port/pmdtest 36 | fuseki:serviceQuery "sparql" ; # SPARQL query service http://host:port/pmdtest/sparql?query=... 37 | fuseki:serviceUpdate "update" ; # SPARQL update servicehttp://host:port/pmdtest/update?query= 38 | fuseki:serviceReadWriteGraphStore "data" ; # SPARQL Graph store protocol (read and write) 39 | fuseki:dataset <#dataset-tripod-test> ; 40 | . 41 | 42 | <#dataset-tripod-test> rdf:type tdb:DatasetTDB ; 43 | tdb:location "/home/travis/tdb_data/tripod-test" ; # change to suit your local installation 44 | # Query timeout on this dataset (1s, 1000 milliseconds) 45 | ja:context [ ja:cxtName "arq:queryTimeout" ; ja:cxtValue "10000" ] ; 46 | tdb:unionDefaultGraph true ; 47 | . 48 | -------------------------------------------------------------------------------- /spec/tripod/repository_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe Tripod::Repository do 4 | 5 | describe "#hydrate" do 6 | 7 | context 'uri set' do 8 | 9 | before do 10 | @uri = 'http://example.com/foobar' 11 | @uri2 = 'http://example.com/bazbar' 12 | @graph_uri = 'http://example.com/graph' 13 | 14 | p1 = Person.new(@uri, @graph_uri) 15 | p1.write_predicate('http://example.com/pred', RDF::URI.new('http://example.com/obj')) 16 | p1.write_predicate('http://example.com/pred2', RDF::URI.new('http://example.com/obj2')) 17 | p1.write_predicate('http://example.com/pred3', 'literal') 18 | p1.save! 19 | end 20 | 21 | let(:person) do 22 | Person.new(@uri, @graph_uri) 23 | end 24 | 25 | let(:graphless_resource) do 26 | Resource.new(@uri) 27 | end 28 | 29 | context 'no graph passed' do 30 | 31 | context 'graph_uri set on object' do 32 | it 'populates the object with triples, restricted to the graph_uri' do 33 | Tripod::SparqlClient::Query.should_receive(:query).with(Person.all_triples_query(person.uri, graph_uri: person.graph_uri), Tripod.ntriples_header_str).and_call_original 34 | person.hydrate! 35 | person.repository.should_not be_empty 36 | end 37 | end 38 | 39 | context 'graph_uri not set on object' do 40 | it 'populates the object with triples, not to a graph' do 41 | Tripod::SparqlClient::Query.should_receive(:query).with(Person.all_triples_query(person.uri), Tripod.ntriples_header_str).and_call_original 42 | graphless_resource.hydrate! 43 | graphless_resource.repository.should_not be_empty 44 | end 45 | end 46 | 47 | end 48 | 49 | context 'graph passed' do 50 | it 'populates the repository with the graph of triples passed in' do 51 | @graph = RDF::Graph.new 52 | 53 | person.repository.statements.each do |s| 54 | @graph << s 55 | end 56 | 57 | @graph << RDF::Statement.new( RDF::URI('http://example.com/anotherresource'), RDF::URI('http://example.com/pred'), RDF::URI('http://example.com/obj')) 58 | @graph.statements.count.should == 2 # there'll already be a statement about type in the person. 59 | 60 | person.hydrate!(:graph => @graph) 61 | person.repository.should_not be_empty 62 | person.repository.statements.count.should == 2 # not the extra ones 63 | person.repository.statements.to_a.should == @graph.statements.to_a 64 | end 65 | end 66 | 67 | end 68 | 69 | end 70 | 71 | 72 | end 73 | -------------------------------------------------------------------------------- /lib/tripod/streaming.rb: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | require 'net/http' 3 | 4 | module Tripod 5 | module Streaming 6 | 7 | # stream data from a url 8 | # opts 9 | #  :accept => "*/*" 10 | # :timeout_seconds = 10 11 | # :response_limit_bytes = nil 12 | def self.get_data(request_url, payload, opts={}) 13 | 14 | accept = opts[:accept] 15 | timeout_in_seconds = opts[:timeout_seconds] || 10 16 | limit_in_bytes = opts[:response_limit_bytes] 17 | 18 | # set request headers 19 | headers = opts[:extra_headers] || {} 20 | 21 | # if explicit accept option is given, set it in the headers (and overwrite any existing value in the extra_headers map) 22 | # if none is given accept */* 23 | headers['Accept'] = accept || headers['Accept'] || '*/*' 24 | 25 | uri = URI(request_url) 26 | 27 | http = Net::HTTP.new(uri.host, uri.port) 28 | http.use_ssl = true if uri.port.to_s == "443" 29 | http.read_timeout = timeout_in_seconds 30 | 31 | total_bytes = 0 32 | 33 | request_start_time = Time.now if Tripod.logger.debug? 34 | 35 | response = StringIO.new 36 | 37 | begin 38 | http.request_post(uri.request_uri, payload, headers) do |res| 39 | 40 | response_duration = Time.now - request_start_time if Tripod.logger.debug? 41 | 42 | Tripod.logger.debug "TRIPOD: received response code: #{res.code} in: #{response_duration} secs" 43 | 44 | if res.code.to_i == 503 45 | raise Tripod::Errors::Timeout.new 46 | elsif res.code.to_s != "200" 47 | raise Tripod::Errors::BadSparqlRequest.new(res.body) 48 | end 49 | 50 | stream_start_time = Time.now if Tripod.logger.debug? 51 | 52 | response.set_encoding('UTF-8') 53 | res.read_body do |seg| 54 | total_bytes += seg.bytesize 55 | raise Tripod::Errors::SparqlResponseTooLarge.new if limit_in_bytes && (total_bytes > limit_in_bytes) 56 | response << seg 57 | seg 58 | end 59 | 60 | if Tripod.logger.debug? 61 | stream_duration = Time.now - stream_start_time 62 | total_request_time = Time.now - request_start_time 63 | end 64 | 65 | if Tripod.logger.debug? 66 | Tripod.logger.debug "TRIPOD: #{total_bytes} bytes streamed in: #{stream_duration} secs" 67 | time_str = "TRIPOD: total request time: #{total_request_time} secs" 68 | time_str += "!!! SLOW !!! " if total_request_time >= 1.0 69 | Tripod.logger.debug time_str 70 | end 71 | 72 | end 73 | rescue Timeout::Error => timeout 74 | raise Tripod::Errors::Timeout.new 75 | end 76 | 77 | response.string 78 | end 79 | 80 | end 81 | end 82 | -------------------------------------------------------------------------------- /lib/tripod/attributes.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | # This module defines behaviour for attributes. 4 | module Tripod::Attributes 5 | 6 | extend ActiveSupport::Concern 7 | 8 | # Reads an attribute from this resource, based on a defined field 9 | # Returns the value(s) for the named (or given) field 10 | # 11 | # @example Read the value associated with a predicate. 12 | # class Person 13 | # field :name, 'http://name' 14 | # end 15 | # 16 | # person.read_attribute(:name) 17 | # 18 | # @param [ String ] name The name of the field for which to get the value. 19 | # @param [ Field ] field An optional Field object 20 | # 21 | # @return Native Ruby object (e.g. String, DateTime) or array of them, depending on whether the field is multivalued or not 22 | def read_attribute(name, field=nil) 23 | field ||= self.class.get_field(name) 24 | 25 | attr_values = read_predicate(field.predicate) 26 | 27 | if field.multivalued 28 | # If the field is multivalued, return an array of the results 29 | # just return the uri or the value of the literal. 30 | attr_values.map { |v| field.is_uri? ? v : v.object } 31 | else 32 | # If it's not multivalued, return the first (should be only) result. 33 | if field.is_uri? 34 | attr_values.first 35 | else 36 | # try to get it in english if it's there. (TODO: make it configurable what the default is) 37 | val = attr_values.select{ |v| v.language == :en }.first || attr_values.first 38 | val.object if val 39 | end 40 | end 41 | end 42 | alias :[] :read_attribute 43 | 44 | # Writes an attribute to the resource, based on a defined field 45 | # 46 | # @example Write the value associated with a predicate. 47 | # class Person 48 | # field :name, 'http://name' 49 | # end 50 | # 51 | # person.write_attribute(:name, 'Bob') 52 | # 53 | # @param [ String ] name The name of the field for which to set the value. 54 | # @param [ String ] value The value to set it to 55 | # @param [ Field ] field An optional Field object 56 | def write_attribute(name, value, field=nil) 57 | field ||= self.fields[name] 58 | raise Tripod::Errors::FieldNotPresent.new unless field 59 | 60 | if value.kind_of?(Array) 61 | if field.multivalued 62 | new_val = [] 63 | value.each do |v| 64 | new_val << write_value_for_field(v, field) 65 | end 66 | else 67 | new_val = write_value_for_field(value.first, field) 68 | end 69 | else 70 | new_val = write_value_for_field(value, field) 71 | end 72 | 73 | attribute_will_change!(name) 74 | write_predicate(field.predicate, new_val) 75 | end 76 | alias :[]= :write_attribute 77 | 78 | private 79 | 80 | def write_value_for_field(value, field) 81 | return if value.blank? 82 | 83 | if field.is_uri? 84 | uri = RDF::URI.new(value.to_s.strip) 85 | elsif field.datatype 86 | RDF::Literal.new(value, :datatype => field.datatype) 87 | else 88 | value 89 | end 90 | end 91 | end 92 | -------------------------------------------------------------------------------- /spec/tripod/resource_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe Tripod::Resource do 4 | 5 | describe "#initialize" do 6 | 7 | it "should raise an error if the URI is given as nil" do 8 | lambda { Person.new(nil) }.should raise_error(Tripod::Errors::UriNotSet) 9 | end 10 | 11 | context 'with a URI' do 12 | let(:person) do 13 | Person.new('http://example.com/foobar') 14 | end 15 | 16 | it 'sets the uri instance variable' do 17 | person.uri.should == RDF::URI.new('http://example.com/foobar') 18 | end 19 | 20 | it 'sets the graph_uri instance variable from the class by default' do 21 | person.graph_uri.should == RDF::URI.new('http://example.com/graph') 22 | end 23 | 24 | context "with rdf_type specified at class level" do 25 | it "sets the rdf type from the class" do 26 | person.rdf_type.should == [RDF::URI.new('http://example.com/person')] 27 | end 28 | end 29 | 30 | it "initialises a repo" do 31 | person.repository.class.should == RDF::Repository 32 | end 33 | end 34 | 35 | context 'with a URI and a graph URI' do 36 | let(:person) do 37 | Person.new('http://example.com/foobar', :graph_uri => 'http://example.com/foobar/graph') 38 | end 39 | 40 | it "overrides the default graph URI with what's given" do 41 | person.graph_uri.should == RDF::URI.new('http://example.com/foobar/graph') 42 | end 43 | end 44 | 45 | context 'with a URI, ignoring the graph URI' do 46 | let(:person) do 47 | Person.new('http://example.com/foobar', :ignore_graph => true) 48 | end 49 | 50 | it "should ignore the class-level graph URI" do 51 | person.graph_uri.should be_nil 52 | end 53 | end 54 | end 55 | 56 | describe "#<=>" do 57 | 58 | let(:person) do 59 | Person.new('http://example.com/foobar', :graph_uri => 'http://example.com/foobar/graph') 60 | end 61 | 62 | let(:person_two) do 63 | Person.new('http://example.com/foobay', :graph_uri => 'http://example.com/foobar/graph') 64 | end 65 | 66 | let(:person_three) do 67 | Person.new('http://example.com/foobaz', :graph_uri => 'http://example.com/foobar/graph') 68 | end 69 | 70 | it "should sort the resources" do 71 | [person_two, person_three, person].sort { |a,b| a <=> b }.should eq [person, person_two, person_three] 72 | end 73 | 74 | end 75 | 76 | describe "#==" do 77 | 78 | let(:person) do 79 | Person.new('http://example.com/foobar', :graph_uri => 'http://example.com/foobar/graph') 80 | end 81 | 82 | let(:person_two) do 83 | Person.new('http://example.com/foobay', :graph_uri => 'http://example.com/foobar/graph') 84 | end 85 | 86 | it "correctly identifies the same resource" do 87 | (person == person).should be true 88 | end 89 | 90 | it "identifies two instances of the same class" do 91 | person.class.name.should == person_two.class.name 92 | (person == person_two).should be false 93 | end 94 | 95 | end 96 | end -------------------------------------------------------------------------------- /lib/tripod/predicates.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | # This module defines behaviour for predicates. 4 | module Tripod::Predicates 5 | 6 | extend ActiveSupport::Concern 7 | 8 | # returns a list of predicates (as RDF::URIs) for this resource 9 | def predicates 10 | pred_uris = [] 11 | 12 | @repository.query( [@uri, :predicate, :object] ) do |statement| 13 | pred_uris << statement.predicate unless pred_uris.include?(statement.predicate) 14 | end 15 | 16 | pred_uris 17 | end 18 | 19 | # Reads values from this resource's in-memory statement repository, where the predicate matches that of the uri passed in. 20 | # Returns an Array of RDF::Terms object. 21 | # 22 | # @example Read the value associated with a predicate. 23 | # person.read_predicate('http://foo') 24 | # person.read_predicate(RDF::URI.new('http://foo')) 25 | # 26 | # @param [ String, RDF::URI ] uri The uri of the predicate to get. 27 | # 28 | # @return [ Array ] An array of RDF::Terms. 29 | def read_predicate(predicate_uri) 30 | values = [] 31 | @repository.query( [@uri, RDF::URI.new(predicate_uri.to_s), :object] ) do |statement| 32 | values << statement.object 33 | end 34 | values 35 | end 36 | 37 | # Replace the statement-values for a single predicate in this resource's in-memory repository. 38 | # 39 | # @example Write the predicate. 40 | # person.write_predicate('http://title', "Mr.") 41 | # person.write_predicate('http://title', ["Mrs.", "Ms."]) 42 | # 43 | # @param [ String, RDF::URI ] predicate_uri The name of the attribute to update. 44 | # @param [ Object, Array ] value The values to set for the attribute. Can be an array, or single item. They should be compatible with RDF::Terms 45 | def write_predicate(predicate_uri, objects) 46 | # remove existing 47 | remove_predicate(predicate_uri) 48 | 49 | if objects 50 | # ... and replace 51 | objects = [objects] unless objects.kind_of?(Array) 52 | objects.each do |object| 53 | @repository << RDF::Statement.new( @uri, RDF::URI.new(predicate_uri.to_s), object ) 54 | end 55 | end 56 | 57 | # returns the new values 58 | read_predicate(predicate_uri) 59 | end 60 | 61 | # Append the statement-values for a single predicate in this resource's in-memory repository. Basically just adds a new statement for this ((resource's uri)+predicate) 62 | # 63 | # @example Write the attribute. 64 | # person.append_to_predicate('http://title', "Mrs.") 65 | # person.append_to_predicate('http://title', "Ms.") 66 | # 67 | # @param [ String, RDF::URI ] predicate_uri The uri of the attribute to update. 68 | # @param [ Object ] value The values to append for the attribute. Should compatible with RDF::Terms 69 | def append_to_predicate(predicate_uri, object ) 70 | raise Tripod::Errors::UriNotSet.new() unless @uri 71 | 72 | @repository << RDF::Statement.new(self.uri, RDF::URI.new(predicate_uri.to_s), object) 73 | end 74 | 75 | def remove_predicate(predicate_uri) 76 | @repository.query( [self.uri, RDF::URI.new(predicate_uri.to_s), :object] ) do |statement| 77 | @repository.delete( statement ) 78 | end 79 | end 80 | alias :delete :remove_predicate 81 | 82 | end -------------------------------------------------------------------------------- /lib/tripod/sparql_query.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | module Tripod 3 | 4 | class SparqlQueryError < StandardError; end 5 | 6 | class SparqlQuery 7 | 8 | attr_reader :query # the original query string 9 | attr_reader :query_type # symbol representing the type (:select, :ask etc) 10 | attr_reader :body # the body of the query 11 | attr_reader :prefixes # any prefixes the query may have 12 | 13 | cattr_accessor :PREFIX_KEYWORDS 14 | @@PREFIX_KEYWORDS = %w(BASE PREFIX) 15 | cattr_accessor :KEYWORDS 16 | @@KEYWORDS = %w(CONSTRUCT ASK DESCRIBE SELECT) 17 | 18 | def initialize(query_string, interpolations=nil) 19 | query_string.strip! 20 | 21 | @query = interpolate_query(query_string, interpolations) if interpolations 22 | 23 | @query ||= query_string 24 | 25 | if self.has_prefixes? 26 | @prefixes, @body = self.extract_prefixes 27 | else 28 | @body = self.query 29 | end 30 | 31 | @query_type = get_query_type 32 | end 33 | 34 | def has_prefixes? 35 | self.class.PREFIX_KEYWORDS.each do |k| 36 | return true if /^#{k}/i.match(query) 37 | end 38 | return false 39 | end 40 | 41 | def extract_prefixes 42 | i = self.class.KEYWORDS.map {|k| self.query.index(/#{k}/i) || self.query.size+1 }.min 43 | p = query[0..i-1] 44 | b = query[i..-1] 45 | return p.strip, b.strip 46 | end 47 | 48 | def check_subqueryable! 49 | # only allow for selects 50 | raise SparqlQueryError.new("Can't turn this into a subquery") unless self.query_type == :select 51 | end 52 | 53 | def as_count_query_str 54 | check_subqueryable! 55 | 56 | count_query = "SELECT (COUNT(*) as ?tripod_count_var) { 57 | #{self.body} 58 | }" 59 | count_query = "#{self.prefixes} #{count_query}" if self.prefixes 60 | 61 | # just returns the string representing the count query for this query. 62 | count_query 63 | end 64 | 65 | def as_first_query_str 66 | check_subqueryable! 67 | 68 | first_query = "SELECT * { #{self.body} } LIMIT 1" 69 | first_query = "#{self.prefixes} #{first_query}" if self.prefixes 70 | 71 | # just returns the string representing the 'first' query for this query. 72 | first_query 73 | end 74 | 75 | def self.get_expected_variables(query_string) 76 | query_string.scan(/[.]?\%\{(\w+)\}[.]?/).flatten.uniq.map(&:to_sym) 77 | end 78 | 79 | private 80 | 81 | def interpolate_query(query_string, interpolations) 82 | expected_variables = self.class.get_expected_variables(query_string) 83 | interpolations = interpolations.symbolize_keys.select{ |k,v| v && v.length > 0 } # remove ones that have no value 84 | missing_variables = expected_variables - interpolations.keys 85 | 86 | if missing_variables.any? 87 | raise SparqlQueryMissingVariables.new(missing_variables, expected_variables, interpolations) 88 | end 89 | 90 | query_string % interpolations # do the interpolating 91 | end 92 | 93 | def get_query_type 94 | if /^CONSTRUCT/i.match(self.body) 95 | :construct 96 | elsif /^ASK/i.match(self.body) 97 | :ask 98 | elsif /^DESCRIBE/i.match(self.body) 99 | :describe 100 | elsif /^SELECT/i.match(self.body) 101 | :select 102 | else 103 | :unknown 104 | end 105 | end 106 | 107 | 108 | end 109 | 110 | end -------------------------------------------------------------------------------- /lib/tripod/criteria.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | require "tripod/criteria/execution" 3 | 4 | module Tripod 5 | 6 | # This module defines behaviour for criteria 7 | class Criteria 8 | 9 | include Tripod::CriteriaExecution 10 | 11 | # the resource class that this criteria is for. 12 | attr_accessor :resource_class 13 | 14 | attr_accessor :where_clauses 15 | attr_accessor :extra_clauses 16 | 17 | attr_accessor :limit_clause 18 | attr_accessor :order_clause 19 | attr_accessor :offset_clause 20 | attr_accessor :graph_uri 21 | attr_accessor :graph_lambdas 22 | 23 | def initialize(resource_class) 24 | self.resource_class = resource_class 25 | self.where_clauses = [] 26 | self.extra_clauses = [] 27 | self.graph_lambdas = [] 28 | 29 | if resource_class._RDF_TYPE 30 | self.where("?uri a <#{resource_class._RDF_TYPE.to_s}>") 31 | end 32 | 33 | self.graph_uri = resource_class._GRAPH_URI.to_s if resource_class._GRAPH_URI 34 | end 35 | 36 | # they're equal if they return the same query 37 | def ==(other) 38 | as_query == other.send(:as_query) 39 | end 40 | 41 | # Takes a string and adds a where clause to this criteria. 42 | # Returns a criteria object. 43 | # Note: the subject being returned by the query must be identified by ?uri 44 | # e.g. my_criteria.where("?uri a ") 45 | # 46 | def where(filter) 47 | if filter.is_a?(String) # we got a Sparql snippet 48 | where_clauses << filter 49 | elsif filter.is_a?(Hash) 50 | filter.each_pair do |key, value| 51 | field = resource_class.get_field(key) 52 | value = RDF::Literal.new(value) unless value.respond_to?(:to_base) 53 | where_clauses << "?uri <#{ field.predicate }> #{ value.to_base }" 54 | end 55 | end 56 | self 57 | end 58 | 59 | def query_where_clauses 60 | where_clauses.empty? ? ['?uri ?p ?o'] : where_clauses 61 | end 62 | # takes a string and adds an extra clause to this criteria. 63 | # e.g. my_criteria.extras("LIMIT 10 OFFSET 20").extrass 64 | # 65 | # TODO: make it also take a hash? 66 | def extras(sparql_snippet) 67 | extra_clauses << sparql_snippet 68 | self 69 | end 70 | 71 | # replaces this criteria's limit clause 72 | def limit(the_limit) 73 | self.limit_clause = "LIMIT #{the_limit.to_s}" 74 | self 75 | end 76 | 77 | # replaces this criteria's offset clause 78 | def offset(the_offset) 79 | self.offset_clause = "OFFSET #{the_offset.to_s}" 80 | self 81 | end 82 | 83 | # replaces this criteria's order clause 84 | def order(param) 85 | self.order_clause = "ORDER BY #{param}" 86 | self 87 | end 88 | 89 | # Restrict this query to the graph uri passed in 90 | # You may also pass a block to an unbound graph, ?g 91 | # then chain a where clause to the criteria returned to bind ?g 92 | # 93 | # @example .graph(RDF::URI.new('http://graphoid') 94 | # @example .graph('http://graphoid') 95 | # @example .graph(nil) { "?s ?p ?o" }.where("?uri ?p ?g") 96 | # 97 | # @param [ String, RDF::URI ] The graph uri 98 | # @param [ Block ] A string to be executed within an unbound graph, ?g 99 | # 100 | # @return [ Tripod::Criteria ] A criteria object 101 | def graph(graph_uri, &block) 102 | 103 | if block_given? 104 | self.graph_lambdas ||= [] 105 | self.graph_lambdas << block 106 | self 107 | else 108 | self.graph_uri = graph_uri.to_s 109 | self 110 | end 111 | end 112 | end 113 | end 114 | -------------------------------------------------------------------------------- /spec/tripod/links_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe Tripod::Links do 4 | 5 | let(:barry) do 6 | b = Person.new('http://example.com/id/barry') 7 | b.name = 'Barry' 8 | b.save! 9 | b 10 | end 11 | 12 | let(:gary) do 13 | g = Person.new('http://example.com/id/gary') 14 | g.name = 'Gary' 15 | g.save! 16 | g 17 | end 18 | 19 | let(:jonno) do 20 | j = Person.new('http://example.com/id/jonno') 21 | j.name = 'Jonno' 22 | j.save! 23 | j 24 | end 25 | 26 | let(:fido) do 27 | d = Dog.new('http://example.com/id/fido') 28 | d.name = "fido" 29 | d.save! 30 | d 31 | end 32 | 33 | let!(:spot) do 34 | d = Dog.new('http://example.com/id/spot') 35 | d.name = "spot" 36 | d.owner = barry 37 | d.save! 38 | d 39 | end 40 | 41 | let!(:rover) do 42 | d = Dog.new('http://example.com/id/rover') 43 | d.name = 'Rover' 44 | d.owner = barry 45 | d.person = gary 46 | d.previous_owner = jonno 47 | d.save! 48 | d 49 | end 50 | 51 | 52 | describe ".linked_from" do 53 | 54 | context "class name is specified" do 55 | it "creates a getter which returns the resources" do 56 | barry.owns_dogs.to_a == [rover, spot] 57 | end 58 | end 59 | 60 | context "class name is not specified" do 61 | it "creates a getter which returns the resources of the right class, based on the link name" do 62 | gary.dogs.to_a.should == [rover] 63 | end 64 | end 65 | 66 | end 67 | 68 | describe ".linked_to" do 69 | 70 | it "creates a getter for the field, with a default name, which returns the uri" do 71 | rover.owner_uri.should == barry.uri 72 | end 73 | 74 | it "creates a setter for the link" do 75 | rover.owner = gary 76 | rover.owner_uri.should == gary.uri 77 | end 78 | 79 | context 'the class name is specified' do 80 | it "creates a getter for the link, which returns a resource of the right type" do 81 | rover.owner.class.should == Person 82 | rover.owner.should == barry 83 | end 84 | end 85 | 86 | context 'the class name is not specified' do 87 | it "creates a getter for the link, which automatically returns a resource of the right type (from link name)" do 88 | rover.person.class.should == Person 89 | rover.person.should == gary 90 | end 91 | end 92 | 93 | context 'when the field name is set to an alternative field name' do 94 | it "uses that for the field name" do 95 | rover.prev_owner_uri.should == jonno.uri 96 | end 97 | end 98 | 99 | context 'its a multivalued field' do 100 | it "creates a getter and setter for multiple values, instantiating the right types of resource" do 101 | rover.friends = [fido, spot] 102 | 103 | rover.friends.each do |f| 104 | f.class.should == Dog 105 | end 106 | 107 | rover.friends.length.should == 2 108 | 109 | rover.friends.map {|f| f.uri}.should =~ [fido.uri, spot.uri] 110 | end 111 | 112 | it "creates field getters and setters with the _uris suffix" do 113 | rover.friends_uris = [fido.uri, spot.uri] 114 | rover.friends_uris.should == [fido.uri, spot.uri] 115 | end 116 | 117 | end 118 | 119 | context "when the value is not set for a link" do 120 | context "single valued" do 121 | it "should be nil" do 122 | rover.arch_enemy.should be_nil 123 | end 124 | end 125 | 126 | context "multivalued" do 127 | it "should be nil" do 128 | rover.enemies.should be_nil 129 | end 130 | end 131 | end 132 | end 133 | end -------------------------------------------------------------------------------- /lib/tripod/resource.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | # module for all domain objects that need to be persisted to the database 4 | # as resources 5 | module Tripod::Resource 6 | 7 | extend ActiveSupport::Concern 8 | 9 | include Tripod::Components 10 | 11 | included do 12 | # every resource needs a graph set. 13 | validates_presence_of :graph_uri 14 | # uri is a valid linked data url 15 | validates :uri, is_url: true 16 | # the Graph URI is set at the class level by default also, although this can be overridden in the constructor 17 | class_attribute :_GRAPH_URI 18 | end 19 | 20 | attr_reader :new_record 21 | attr_reader :graph_uri 22 | attr_reader :uri 23 | 24 | # Instantiate a +Resource+. 25 | # 26 | # @example Instantiate a new Resource 27 | # Person.new('http://swirrl.com/ric.rdf#me') 28 | # @example Instantiate a new Resource in a particular graph 29 | # Person.new('http://swirrl.com/ric.rdf#me', :graph_uri => 'http://swirrl.com/graph/people') 30 | # @example Instantiate a new Resource in a particular graph (DEPRECATED) 31 | # Person.new('http://swirrl.com/ric.rdf#me', 'http://swirrl.com/graph/people') 32 | # 33 | # @param [ String, RDF::URI ] uri The uri of the resource. 34 | # @param [ Hash, String, RDF::URI ] opts An options hash (see above), or the graph_uri where this resource will be saved to. If graph URI is ommitted and can't be derived from the object's class, this resource cannot be persisted. 35 | # 36 | # @return [ Resource ] A new +Resource+ 37 | def initialize(uri, opts={}) 38 | if opts.is_a?(Hash) 39 | graph_uri = opts.fetch(:graph_uri, nil) 40 | ignore_graph = opts.fetch(:ignore_graph, false) 41 | else 42 | graph_uri = opts 43 | end 44 | 45 | raise Tripod::Errors::UriNotSet.new('uri missing') unless uri 46 | @uri = RDF::URI(uri.to_s) 47 | @repository = RDF::Repository.new 48 | @new_record = true 49 | 50 | run_callbacks :initialize do 51 | graph_uri ||= self.class.get_graph_uri unless ignore_graph 52 | @graph_uri = RDF::URI(graph_uri) if graph_uri 53 | set_rdf_type 54 | end 55 | end 56 | 57 | # default comparison is via the uri 58 | def <=>(other) 59 | uri.to_s <=> other.uri.to_s 60 | end 61 | 62 | # performs equality checking on the uris 63 | def ==(other) 64 | self.class == other.class && 65 | uri.to_s == other.uri.to_s 66 | end 67 | 68 | # performs equality checking on the class 69 | def ===(other) 70 | other.class == Class ? self.class === other : self == other 71 | end 72 | 73 | # delegates to == 74 | def eql?(other) 75 | self == (other) 76 | end 77 | 78 | def hash 79 | identity.hash 80 | end 81 | 82 | # a resource is absolutely identified by it's class and id. 83 | def identity 84 | [ self.class, self.uri.to_s ] 85 | end 86 | 87 | # Return the key value for the resource. 88 | # 89 | # @example Return the key. 90 | # resource.to_key 91 | # 92 | # @return [ Object ] The uri of the resource or nil if new. 93 | def to_key 94 | (persisted? || destroyed?) ? [ uri.to_s ] : nil 95 | end 96 | 97 | def to_a 98 | [ self ] 99 | end 100 | 101 | module ClassMethods 102 | 103 | # Performs class equality checking. 104 | def ===(other) 105 | other.class == Class ? self <= other : other.is_a?(self) 106 | end 107 | 108 | def graph_uri(new_graph_uri) 109 | self._GRAPH_URI = new_graph_uri 110 | end 111 | 112 | def get_graph_uri 113 | self._GRAPH_URI 114 | end 115 | end 116 | 117 | end 118 | 119 | # causes any hooks to be fired, if they've been setup on_load of :tripod. 120 | ActiveSupport.run_load_hooks(:triploid, Tripod::Resource) 121 | -------------------------------------------------------------------------------- /lib/tripod/repository.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | # This module wraps access to an RDF::Repository 4 | module Tripod::Repository 5 | extend ActiveSupport::Concern 6 | 7 | attr_reader :repository 8 | 9 | # hydrates the resource's repo with statements from the db or passed in graph of statements. 10 | # where the subject is the uri of this resource. 11 | # 12 | # @example Hydrate the resource from the db 13 | # person.hydrate! 14 | # 15 | # @example Hydrate the resource from a passed in graph 16 | # person.hydrate!(:graph => my_graph) 17 | # 18 | # 19 | # @return [ RDF::Repository ] A reference to the repository for this instance. 20 | def hydrate!(opts = {}) 21 | 22 | graph = opts[:graph] 23 | 24 | # we require that the uri is set. 25 | raise Tripod::Errors::UriNotSet.new() unless @uri 26 | 27 | @repository = RDF::Repository.new # make sure that the repo is empty before we begin 28 | 29 | if graph 30 | graph.each_statement do |statement| 31 | # Note that we use all statements, even those not about this resource, in case we're being 32 | # passed eager-loaded ones. 33 | @repository << statement 34 | end 35 | else 36 | 37 | triples = retrieve_triples_from_database 38 | 39 | @repository = RDF::Repository.new 40 | RDF::Reader.for(:ntriples).new(triples) do |reader| 41 | reader.each_statement do |statement| 42 | @repository << statement 43 | end 44 | end 45 | end 46 | 47 | end 48 | 49 | # returns a graph of all triples in the repository 50 | def repository_as_graph 51 | g = RDF::Graph.new 52 | @repository.each_statement do |s| 53 | g << s 54 | end 55 | g 56 | end 57 | 58 | def retrieve_triples_from_database(accept_header=Tripod.ntriples_header_str) 59 | Tripod::SparqlClient::Query.query(self.class.all_triples_query(uri, graph_uri: self.graph_uri), accept_header) 60 | end 61 | 62 | # returns a graph of triples from the underlying repository where this resource's uri is the subject. 63 | def get_triples_for_this_resource 64 | triples_graph = RDF::Graph.new 65 | @repository.query([RDF::URI.new(self.uri), :predicate, :object]) do |stmt| 66 | triples_graph << stmt 67 | end 68 | triples_graph 69 | end 70 | 71 | module ClassMethods 72 | 73 | # for triples in the graph passed in, add them to the passed in repository obj, and return the repository objects 74 | # if no repository passed, make a new one. 75 | def add_data_to_repository(graph, repo=nil) 76 | 77 | repo ||= RDF::Repository.new() 78 | 79 | graph.each_statement do |statement| 80 | repo << statement 81 | end 82 | 83 | repo 84 | end 85 | 86 | def append_to_hydrate_construct(statement) 87 | @construct_statements ||= [] 88 | @construct_statements << statement 89 | end 90 | 91 | def append_to_hydrate_where(statement) 92 | @where_statements ||= [] 93 | @where_statements << statement 94 | end 95 | 96 | def all_triples_query(uri, opts={}) 97 | graph_uri = opts.fetch(:graph_uri, nil) 98 | graph_selector = graph_uri.present? ? "<#{graph_uri.to_s}>" : "?g" 99 | uri_selector = "<#{uri}>" 100 | "CONSTRUCT { #{uri_selector} ?p ?o . #{ all_triples_construct(uri_selector) } } WHERE { GRAPH #{graph_selector} { #{uri_selector} ?p ?o . #{ all_triples_where(uri_selector) } } }" 101 | end 102 | 103 | def all_triples_construct(uri) 104 | extra_construct = @construct_statements.map{|s| s.call(uri) }.join if @construct_statements.present? 105 | extra_construct || '' 106 | end 107 | 108 | def all_triples_where(uri) 109 | extra_where = @where_statements.map{|s| s.call(uri) }.join if @where_statements.present? 110 | extra_where || '' 111 | end 112 | 113 | end 114 | 115 | end 116 | -------------------------------------------------------------------------------- /spec/tripod/fields_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe Tripod::Fields do 4 | describe ".field" do 5 | 6 | let(:barry) do 7 | b = Person.new('http://example.com/id/barry') 8 | b.name = 'Barry' 9 | b 10 | end 11 | 12 | it "creates a getter for the field, which accesses data for the predicate, returning a single String" do 13 | barry.name.should == "Barry" 14 | end 15 | 16 | it "creates a setter for the field, which sets data for the predicate" do 17 | barry.name = "Basildon" 18 | barry.name.should == "Basildon" 19 | end 20 | 21 | it "creates a check? method, which returns true when the value is present" do 22 | barry.name?.should == true 23 | end 24 | 25 | context "when the value is not set" do 26 | before { barry.name = nil } 27 | 28 | it "should have a check? method which returns false" do 29 | barry.name?.should == false 30 | end 31 | end 32 | 33 | context "given a field of type URI where an invalid URI is given" do 34 | before { barry.father = 'Steven Notauri' } 35 | 36 | it "should not be valid" do 37 | barry.should_not be_valid 38 | end 39 | end 40 | end 41 | 42 | describe '.get_field' do 43 | it 'should raise an error if the field does not exist' do 44 | expect { Person.send(:get_field, :shoe_size) }.to raise_error(Tripod::Errors::FieldNotPresent) 45 | end 46 | 47 | it 'should return the field for the given name' do 48 | Person.send(:get_field, :age).name.should == :age 49 | end 50 | end 51 | end 52 | 53 | module Spec 54 | module Tripod 55 | module Inheritance 56 | BASE_PREDICATE = RDF::URI.new("http://base/predicate/overriden/from/SubSub/up") 57 | BASE_PREDICATE_OVERIDE = RDF::URI.new("http://overide/base/predicate") 58 | 59 | ANOTHER_PREDICATE = RDF::RDFS::label 60 | 61 | class Base 62 | include ::Tripod::Resource 63 | field :inherited, BASE_PREDICATE 64 | end 65 | 66 | class Sub < Base 67 | field :bar, ANOTHER_PREDICATE 68 | # expects inerited to be ANOTHER_PREDICATE 69 | end 70 | 71 | class SubSub < Sub 72 | field :inherited, BASE_PREDICATE_OVERIDE 73 | end 74 | 75 | class SubSubSub < SubSub 76 | # defines no new fields, used to test no NullPointerExceptions 77 | # etc on classes that don't define fields. 78 | end 79 | 80 | describe 'inheritance' do 81 | describe Base do 82 | subject(:base) { Base } 83 | 84 | it "does not inhert fields from subclasses" do 85 | expect(base.fields[:bar]).to be_nil 86 | end 87 | 88 | it "defines the :inherited field" do 89 | inherited_field = base.fields[:inherited] 90 | expect(inherited_field.predicate).to eq(BASE_PREDICATE) 91 | end 92 | end 93 | 94 | describe Sub do 95 | subject(:inherited) { Sub.get_field(:inherited) } 96 | it "does not redefine :inherited field" do 97 | expect(inherited.predicate).to eq(BASE_PREDICATE) 98 | end 99 | end 100 | 101 | describe SubSub do 102 | subject(:inherited) { SubSub.get_field(:inherited) } 103 | 104 | it "overrides the :inherited field" do 105 | expect(inherited.predicate).to eq(BASE_PREDICATE_OVERIDE) 106 | end 107 | end 108 | 109 | describe SubSubSub do 110 | it "inherits the :bar field from Sub" do 111 | bar = SubSubSub.get_field(:bar) 112 | expect(bar.predicate).to eq(ANOTHER_PREDICATE) 113 | end 114 | 115 | it "overrides the :inherited field in Base with the value from SubSub" do 116 | inherited = SubSubSub.get_field(:inherited) 117 | expect(inherited.predicate).to eq(BASE_PREDICATE_OVERIDE) 118 | end 119 | end 120 | end 121 | end 122 | end 123 | end 124 | -------------------------------------------------------------------------------- /lib/tripod/resource_collection.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | module Tripod 4 | 5 | # class that wraps a collection of resources, and allows them to be serialized 6 | class ResourceCollection 7 | 8 | include Enumerable 9 | 10 | attr_reader :resources 11 | attr_reader :criteria # the criteria used to generate this collection 12 | attr_reader :sparql_query_str # the sparql query used to generate this collection 13 | 14 | # options: 15 | # :criteria - the criteria used to create this collection 16 | # :sparql_query_str - the sparql used to create this collection 17 | # :return_graph - whether the original query returned the graphs or not. 18 | def initialize(resources, opts={}) 19 | @resources = resources 20 | @criteria = opts[:criteria] 21 | @sparql_query_str = opts[:sparql_query_str] 22 | @resource_class = opts[:resource_class] 23 | @return_graph = opts[:return_graph] 24 | end 25 | 26 | def length 27 | self.resources.length 28 | end 29 | 30 | def each 31 | self.resources.each { |e| yield(e) } 32 | end 33 | 34 | # return the underlying array 35 | def to_a 36 | resources 37 | end 38 | 39 | # allow index operator to act on underlying array of resources. 40 | def [](*args) 41 | resources[*args] 42 | end 43 | 44 | def ==(other) 45 | self.to_nt == other.to_nt 46 | end 47 | 48 | def to_text 49 | to_nt 50 | end 51 | 52 | # for n-triples we can just concatenate them 53 | def to_nt 54 | time_serialization('nt') do 55 | if @criteria 56 | @criteria.serialize(:return_graph => @return_graph, :accept_header => Tripod.ntriples_header_str) 57 | elsif @sparql_query_str && @resource_class 58 | # run the query as a describe. 59 | @resource_class._raw_describe_select_results(@sparql_query_str, :accept_header => Tripod.ntriples_header_str) 60 | else 61 | # for n-triples we can just concatenate them 62 | nt = "" 63 | resources.each do |resource| 64 | nt += resource.to_nt 65 | end 66 | nt 67 | end 68 | end 69 | end 70 | 71 | def to_json(opts={}) 72 | # most databases don't have a native json-ld implementation. 73 | time_serialization('json') do 74 | get_graph.dump(:jsonld) 75 | end 76 | end 77 | 78 | def to_rdf 79 | time_serialization('rdf') do 80 | if @criteria 81 | @criteria.serialize(:return_graph => @return_graph, :accept_header => "application/rdf+xml") 82 | elsif @sparql_query_str && @resource_class 83 | # run the query as a describe. 84 | @resource_class._raw_describe_select_results(@sparql_query_str, :accept_header => "application/rdf+xml") 85 | else 86 | get_graph.dump(:rdf) 87 | end 88 | end 89 | end 90 | 91 | def to_ttl 92 | time_serialization('ttl') do 93 | if @criteria 94 | @criteria.serialize(:return_graph => @return_graph, :accept_header => "text/turtle") 95 | elsif @sparql_query_str && @resource_class 96 | # run the query as a describe. 97 | @resource_class._raw_describe_select_results(@sparql_query_str, :accept_header =>"text/turtle") 98 | else 99 | get_graph.dump(:turtle) 100 | end 101 | end 102 | end 103 | 104 | private 105 | 106 | def time_serialization(format) 107 | start_serializing = Time.now if Tripod.logger.debug? 108 | result = yield if block_given? 109 | serializing_duration = Time.now - start_serializing if Tripod.logger.debug? 110 | Tripod.logger.debug( "TRIPOD: Serializing collection to #{format} took #{serializing_duration} secs" ) 111 | result 112 | end 113 | 114 | def get_graph 115 | graph = RDF::Graph.new 116 | RDF::Reader.for(:ntriples).new(self.to_nt) do |reader| 117 | reader.each_statement do |statement| 118 | graph << statement 119 | end 120 | end 121 | end 122 | 123 | end 124 | 125 | end -------------------------------------------------------------------------------- /spec/tripod/predicates_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe Tripod::Predicates do 4 | 5 | before do 6 | @uri = 'http://example.com/ric' 7 | @graph = RDF::Graph.new 8 | 9 | stmt = RDF::Statement.new 10 | stmt.subject = RDF::URI.new(@uri) 11 | stmt.predicate = RDF::URI.new('http://example.com/blog') 12 | stmt.object = RDF::URI.new('http://example.com/blog1') 13 | @graph << stmt 14 | 15 | stmt2 = RDF::Statement.new 16 | stmt2.subject = RDF::URI.new(@uri) 17 | stmt2.predicate = RDF::URI.new('http://example.com/blog') 18 | stmt2.object = RDF::URI.new('http://example.com/blog2') 19 | @graph << stmt2 20 | 21 | stmt3 = RDF::Statement.new 22 | stmt3.subject = RDF::URI.new(@uri) 23 | stmt3.predicate = RDF::URI.new('http://example.com/name') 24 | stmt3.object = "ric" 25 | @graph << stmt3 26 | 27 | # throw a random other statement (about name) in the mix! 28 | stmt4 = RDF::Statement.new 29 | stmt4.subject = RDF::URI.new('http://example.com/name') 30 | stmt4.predicate = RDF::RDFS.label 31 | stmt4.object = "name" 32 | @graph << stmt4 33 | end 34 | 35 | let(:person) do 36 | p = Person.new(@uri) 37 | p.hydrate!(:graph => @graph) 38 | p 39 | end 40 | 41 | describe "#read_predicate" do 42 | it 'returns the values where the predicate matches' do 43 | values = person.read_predicate('http://example.com/blog') 44 | values.length.should == 2 45 | values.first.should == RDF::URI('http://example.com/blog1') 46 | values[1].should == RDF::URI('http://example.com/blog2') 47 | end 48 | end 49 | 50 | describe '#write_predicate' do 51 | 52 | context 'single term passed' do 53 | it 'replaces the values where the predicate matches' do 54 | person.write_predicate('http://example.com/name', 'richard') 55 | person.read_predicate('http://example.com/name').should == [RDF::Literal.new('richard')] 56 | end 57 | end 58 | 59 | context 'multiple terms passed' do 60 | it 'replaces the values where the predicate matches' do 61 | person.write_predicate('http://example.com/name', ['richard', 'ric', 'ricardo']) 62 | person.read_predicate('http://example.com/name').should == [RDF::Literal.new('richard'), RDF::Literal.new('ric'), RDF::Literal.new('ricardo')] 63 | end 64 | end 65 | 66 | context 'given a nil value' do 67 | it 'just removes the predicate' do 68 | person.write_predicate('http://example.com/name', nil) 69 | person.read_predicate('http://example.com/name').should be_empty 70 | end 71 | end 72 | end 73 | 74 | describe '#remove_predicate' do 75 | it 'removes the values where the predicate matches' do 76 | person.remove_predicate('http://example.com/blog') 77 | person.read_predicate('http://example.com/blog').should be_empty 78 | end 79 | 80 | context 'when there are other triples in the repository that share the same predicate' do 81 | let(:subject) { RDF::URI.new('http://foo') } 82 | let(:predicate) { RDF::URI.new('http://example.com/blog') } 83 | before do 84 | person.repository << [subject, predicate, RDF::URI.new('http://foo.tumblr.com')] 85 | end 86 | 87 | it "doesn't remove a value where the subject of the triple isn't the resource's URI" do 88 | person.remove_predicate('http://example.com/blog') 89 | person.repository.query( [subject, predicate, :object] ).should_not be_empty 90 | end 91 | end 92 | end 93 | 94 | describe "#append_to_predicate" do 95 | it 'appends values to the existing values for the predicate' do 96 | person.append_to_predicate('http://example.com/name', 'rico') 97 | person.read_predicate('http://example.com/name').should == [RDF::Literal.new('ric'), RDF::Literal.new('rico')] 98 | end 99 | end 100 | 101 | describe "#predicates" do 102 | it "returns a list of unique RDF::URIs for the predicates set on this resource" do 103 | person.predicates.length.should == 2 104 | person.predicates.should == [RDF::URI('http://example.com/blog'), RDF::URI('http://example.com/name')] 105 | end 106 | 107 | 108 | end 109 | 110 | end -------------------------------------------------------------------------------- /lib/tripod/criteria/execution.rb: -------------------------------------------------------------------------------- 1 | # This module defines behaviour for criteria 2 | module Tripod 3 | 4 | # this module provides execution methods to a criteria object 5 | module CriteriaExecution 6 | 7 | extend ActiveSupport::Concern 8 | 9 | # Execute the query and return a +ResourceCollection+ of all hydrated resources 10 | # +ResourceCollection+ is an +Enumerable+, Array-like object. 11 | # 12 | # @option options [ String ] return_graph Indicates whether to return the graph as one of the variables. 13 | def resources(opts={}) 14 | Tripod::ResourceCollection.new( 15 | self.resource_class._resources_from_sparql(self.as_query(opts)), 16 | # pass in the criteria that was used to generate this collection, as well as whether the user specified return graph 17 | :return_graph => (opts.has_key?(:return_graph) ? opts[:return_graph] : true), 18 | :criteria => self 19 | ) 20 | end 21 | 22 | # run a query to get the raw serialisation of the results of this criteria object. 23 | # 24 | # @option options [ String ] return_graph Indicates whether to return the graph as one of the variables. 25 | # @option options [ String ] accept_header The accept header to use for serializing (defaults to application/n-triples) 26 | def serialize(opts={}) 27 | select_sparql = self.as_query(:return_graph => opts[:return_graph]) 28 | self.resource_class._raw_describe_select_results(select_sparql, :accept_header => opts[:accept_header]) # note that this method defaults to using application/n-triples. 29 | end 30 | 31 | # Execute the query and return the first result as a hydrated resource 32 | # 33 | # @option options [ String ] return_graph Indicates whether to return the graph as one of the variables. 34 | def first(opts={}) 35 | sq = Tripod::SparqlQuery.new(self.as_query(opts)) 36 | first_sparql = sq.as_first_query_str 37 | self.resource_class._resources_from_sparql(first_sparql).first 38 | end 39 | 40 | # Return how many records the current criteria would return 41 | # 42 | # @option options [ String ] return_graph Indicates whether to return the graph as one of the variables. 43 | def count(opts={}) 44 | sq = Tripod::SparqlQuery.new(self.as_query(opts)) 45 | count_sparql = sq.as_count_query_str 46 | result = Tripod::SparqlClient::Query.select(count_sparql) 47 | 48 | if result.length > 0 49 | result[0]["tripod_count_var"]["value"].to_i 50 | else 51 | return 0 52 | end 53 | end 54 | 55 | # turn this criteria into a query 56 | def as_query(opts={}) 57 | Tripod.logger.debug("TRIPOD: building select query for criteria...") 58 | 59 | return_graph = opts.has_key?(:return_graph) ? opts[:return_graph] : true 60 | 61 | Tripod.logger.debug("TRIPOD: with return_graph: #{return_graph.inspect}") 62 | 63 | select_query = "SELECT DISTINCT ?uri " 64 | 65 | if graph_lambdas.empty? 66 | 67 | if return_graph 68 | # if we are returning the graph, select it as a variable, and include either the or ?graph in the where clause 69 | if graph_uri 70 | select_query += "(<#{graph_uri}> as ?graph) WHERE { GRAPH <#{graph_uri}> { " 71 | else 72 | select_query += "?graph WHERE { GRAPH ?graph { " 73 | end 74 | else 75 | select_query += "WHERE { " 76 | # if we're not returning the graph, only restrict by the if there's one set at class level 77 | select_query += "GRAPH <#{graph_uri}> { " if graph_uri 78 | end 79 | 80 | select_query += self.query_where_clauses.join(" . ") 81 | select_query += " } " 82 | select_query += "} " if return_graph || graph_uri # close the graph clause 83 | 84 | else 85 | # whip through the graph lambdas and add into the query 86 | # we have multiple graphs so the above does not apply 87 | select_query += "WHERE { " 88 | 89 | graph_lambdas.each do |lambda_item| 90 | select_query += "GRAPH ?g { " 91 | select_query += lambda_item.call 92 | select_query += " } " 93 | end 94 | 95 | select_query += self.query_where_clauses.join(" . ") 96 | select_query += " } " 97 | end 98 | 99 | select_query += self.extra_clauses.join(" ") 100 | 101 | select_query += [order_clause, limit_clause, offset_clause].join(" ") 102 | 103 | select_query.strip 104 | end 105 | 106 | end 107 | end -------------------------------------------------------------------------------- /lib/tripod.rb: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2012 Swirrl IT Limited. http://swirrl.com 2 | 3 | # MIT License 4 | 5 | # Permission is hereby granted, free of charge, to any person obtaining 6 | # a copy of this software and associated documentation files (the 7 | # "Software"), to deal in the Software without restriction, including 8 | # without limitation the rights to use, copy, modify, merge, publish, 9 | # distribute, sublicense, and/or sell copies of the Software, and to 10 | # permit persons to whom the Software is furnished to do so, subject to 11 | # the following conditions: 12 | 13 | # The above copyright notice and this permission notice shall be 14 | # included in all copies or substantial portions of the Software. 15 | 16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | require "tripod/version" 25 | 26 | require "active_support" 27 | require "active_support/core_ext" 28 | require 'active_support/json' 29 | require "active_support/inflector" 30 | require "active_model" 31 | require "guid" 32 | 33 | require 'rdf' 34 | require 'rdf/rdfxml' 35 | require 'rdf/turtle' 36 | require 'rdf/json' 37 | require 'json/ld' 38 | require 'uri' 39 | require 'rest_client' 40 | 41 | module Tripod 42 | 43 | mattr_accessor :update_endpoint 44 | @@update_endpoint = 'http://127.0.0.1:3030/tripod/update' 45 | 46 | mattr_accessor :query_endpoint 47 | @@query_endpoint = 'http://127.0.0.1:3030/tripod/sparql' 48 | 49 | mattr_accessor :data_endpoint 50 | @@data_endpoint = 'http://127.0.0.1:3030/tripod/data' 51 | 52 | mattr_accessor :extra_endpoint_params 53 | @@extra_endpoint_params = {} 54 | 55 | mattr_accessor :extra_endpoint_headers 56 | @@extra_endpoint_headers = {} 57 | 58 | mattr_accessor :timeout_seconds 59 | @@timeout_seconds = 30 60 | 61 | mattr_accessor :response_limit_bytes 62 | @@response_limit_bytes = 5.megabytes 63 | 64 | # this is a default to cope with headers in stardog 65 | # override if you wish to remove the text/plain fallback 66 | mattr_accessor :ntriples_header_str 67 | @@ntriples_header_str = "application/n-triples, text/plain" 68 | 69 | mattr_accessor :cache_store 70 | 71 | mattr_accessor :logger 72 | @@logger = Logger.new(STDOUT) 73 | @@logger.level = Logger::WARN 74 | 75 | # Use +configure+ to override configuration in an app, (defaults shown) 76 | # 77 | # Tripod.configure do |config| 78 | # config.update_endpoint = 'http://127.0.0.1:3030/tripod/update' 79 | # config.query_endpoint = 'http://127.0.0.1:3030/tripod/sparql' 80 | # config.timeout_seconds = 30# 81 | # config.response_limit_bytes = 4.megabytes # omit for no limit 82 | # config.cache_store = nil #e.g Tripod::CacheStores::MemcachedCacheStore.new('localhost:11211') 83 | # # note: if using memcached, make sure you set the -I (slab size) to big enough to store each result 84 | # # and set the -m (total size) to something quite big (or the cache will recycle too often). 85 | # # also note that the connection pool size can be passed in as an optional second parameter. 86 | #  config.logger = Logger.new(STDOUT) # you can set this to the Rails.logger in a rails app. 87 | # end 88 | # 89 | def self.configure 90 | yield self 91 | end 92 | 93 | end 94 | 95 | require 'tripod/cache_stores/memcached_cache_store' 96 | 97 | require "tripod/extensions" 98 | require "tripod/streaming" 99 | require "tripod/sparql_client" 100 | require "tripod/sparql_query" 101 | require "tripod/resource_collection" 102 | 103 | require "tripod/predicates" 104 | require "tripod/attributes" 105 | require "tripod/callbacks" 106 | require "tripod/validations/is_url" 107 | require "tripod/rdf_type" 108 | require "tripod/errors" 109 | require "tripod/repository" 110 | require "tripod/fields" 111 | require "tripod/dirty" 112 | require "tripod/criteria" 113 | require "tripod/links" 114 | require "tripod/finders" 115 | require "tripod/persistence" 116 | require "tripod/eager_loading" 117 | require "tripod/serialization" 118 | require "tripod/state" 119 | require "tripod/graphs" 120 | require "tripod/embeds" 121 | require "tripod/embeds/many" 122 | require "tripod/embedded_resource" 123 | require "tripod/version" 124 | 125 | # these need to be at the end 126 | require "tripod/components" 127 | require "tripod/resource" 128 | 129 | require 'active_support/i18n' 130 | I18n.enforce_available_locales = true 131 | I18n.load_path << File.dirname(__FILE__) + '/tripod/locale/en.yml' 132 | -------------------------------------------------------------------------------- /lib/tripod/eager_loading.rb: -------------------------------------------------------------------------------- 1 | module Tripod::EagerLoading 2 | 3 | extend ActiveSupport::Concern 4 | 5 | # array of resources that represent the predicates of the triples of this resource 6 | attr_reader :predicate_resources 7 | 8 | # array of resources that represent the objects of the triples of this resource 9 | attr_reader :object_resources 10 | 11 | # get all the triples in the db where the predicate uri is their subject 12 | # stick the results in this resource's repo 13 | # options: labels_only (default false) 14 | # options: predicates (default nil) array of predicaets (as URIs or strings representing URIs) for fieldnames to fetch 15 | def eager_load_predicate_triples!(opts={}) 16 | 17 | if opts[:labels_only] 18 | construct_query = "CONSTRUCT { ?p <#{RDF::RDFS.label}> ?pred_label } WHERE { <#{self.uri.to_s}> ?p ?o . ?p <#{RDF::RDFS.label}> ?pred_label }" 19 | elsif (opts[:predicates] && opts[:predicates].length > 0) 20 | construct_query = build_multifield_query(opts[:predicates],"?p") 21 | else 22 | construct_query = "CONSTRUCT { ?p ?pred_pred ?pred_label } WHERE { <#{self.uri.to_s}> ?p ?o . ?p ?pred_pred ?pred_label }" 23 | end 24 | 25 | extra_triples = self.class._graph_of_triples_from_construct_or_describe construct_query 26 | self.class.add_data_to_repository(extra_triples, self.repository) 27 | end 28 | 29 | # get all the triples in the db where the object uri is their subject 30 | # stick the results in this resource's repo 31 | # options: labels_only (default false) 32 | # options: predicates (default nil) array of predicaets (as URIs or strings representing URIs) for fieldnames to fetch 33 | def eager_load_object_triples!(opts={}) 34 | object_uris = [] 35 | 36 | if opts[:labels_only] 37 | construct_query = "CONSTRUCT { ?o <#{RDF::RDFS.label}> ?obj_label } WHERE { <#{self.uri.to_s}> ?p ?o . ?o <#{RDF::RDFS.label}> ?obj_label }" 38 | elsif (opts[:predicates] && opts[:predicates].length > 0) 39 | construct_query = build_multifield_query(opts[:predicates],"?o") 40 | else 41 | construct_query = "CONSTRUCT { ?o ?obj_pred ?obj_label } WHERE { <#{self.uri.to_s}> ?p ?o . ?o ?obj_pred ?obj_label }" 42 | end 43 | 44 | extra_triples = self.class._graph_of_triples_from_construct_or_describe construct_query 45 | self.class.add_data_to_repository(extra_triples, self.repository) 46 | end 47 | 48 | # build a list of optional predicates 49 | # 50 | # for the input 51 | # build_multifield_query(RDF::SKOS.prefLabel, RDF::RDFS.label, RDF::DC.title],"?p") 52 | # 53 | # the follwing query is generated 54 | # 55 | # CONSTRUCT { 56 | # ?p ?pref_label . 57 | # ?p ?label . 58 | # ?p ?title . } 59 | # WHERE { 60 | # ?p ?o . 61 | # OPTIONAL { ?p ?pref_label . } 62 | # OPTIONAL { ?p ?label . } 63 | # OPTIONAL { ?p ?title . } 64 | # } 65 | # 66 | def build_multifield_query(predicates,subject_position_as) 67 | clauses = [] 68 | 69 | iter = 0 70 | predicates.each do |p| 71 | variable_name = "var#{iter.to_s}" 72 | clauses << "#{subject_position_as} <#{p.to_s}> ?#{variable_name} . " 73 | iter +=1 74 | end 75 | 76 | construct_query = "CONSTRUCT { " 77 | clauses.each do |c| 78 | construct_query += c 79 | end 80 | 81 | construct_query += " } WHERE { <#{self.uri.to_s}> ?p ?o . " 82 | clauses.each do |c| 83 | construct_query += " OPTIONAL { #{c} } " 84 | end 85 | construct_query += " }" # close WHERE 86 | end 87 | 88 | # get the resource that represents a particular uri. If there's triples in our repo where that uri 89 | # is the subject, use that to hydrate a resource, otherwise justdo a find against the db. 90 | def get_related_resource(resource_uri, class_of_resource_to_create) 91 | data_graph = RDF::Graph.new 92 | 93 | self.repository.query( [ RDF::URI.new(resource_uri.to_s), :predicate, :object] ) do |stmt| 94 | data_graph << stmt 95 | end 96 | 97 | if data_graph.empty? 98 | r = nil 99 | else 100 | # it's in our eager loaded repo 101 | r = class_of_resource_to_create.new(resource_uri) 102 | r.hydrate!(:graph => data_graph) 103 | r.new_record = false 104 | r 105 | end 106 | r 107 | end 108 | 109 | def has_related_resource?(resource_uri, class_of_resource) 110 | !!get_related_resource(resource_uri, class_of_resource) 111 | end 112 | 113 | end -------------------------------------------------------------------------------- /lib/tripod/sparql_client.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | # this module is responsible for connecting to an http sparql endpoint 4 | module Tripod::SparqlClient 5 | 6 | module Query 7 | 8 | # Runs a +sparql+ query against the endpoint. Returns a RestClient response object. 9 | # 10 | # @example Run a query 11 | # Tripod::SparqlClient::Query.query('SELECT * WHERE {?s ?p ?o}') 12 | # 13 | # @param [ String ] sparql The sparql query. 14 | # @param [ String ] accept_header The accept header to send with the request 15 | # @param [ Hash ] any extra params to send with the request 16 | # @return [ RestClient::Response ] 17 | def self.query(sparql, accept_header, extra_params={}, response_limit_bytes = :default, extra_headers = {}) 18 | 19 | non_sparql_params = (Tripod.extra_endpoint_params).merge(extra_params) 20 | params_hash = {:query => sparql} 21 | params = self.to_query(params_hash) 22 | request_url = URI(Tripod.query_endpoint).tap {|u| u.query = non_sparql_params.to_query}.to_s 23 | extra_headers.merge!(Tripod.extra_endpoint_headers) 24 | streaming_opts = {:accept => accept_header, :timeout_seconds => Tripod.timeout_seconds, :extra_headers => extra_headers} 25 | streaming_opts.merge!(_response_limit_options(response_limit_bytes)) if Tripod.response_limit_bytes 26 | 27 | # Hash.to_query from active support core extensions 28 | stream_data = -> { 29 | Tripod.logger.debug "TRIPOD: About to run query: #{sparql}" 30 | Tripod.logger.debug "TRIPOD: Streaming from url: #{request_url}" 31 | Tripod.logger.debug "TRIPOD: non sparql params #{non_sparql_params.to_s}" 32 | Tripod.logger.debug "TRIPOD: Streaming opts: #{streaming_opts.inspect}" 33 | Tripod::Streaming.get_data(request_url, params, streaming_opts) 34 | } 35 | 36 | if Tripod.cache_store # if a cache store is configured 37 | Tripod.logger.debug "TRIPOD: caching is on!" 38 | # SHA-2 the key to keep the it within the small limit for many cache stores (e.g. Memcached is 250bytes) 39 | # Note: SHA2's are pretty certain to be unique http://en.wikipedia.org/wiki/SHA-2. 40 | cache_key = 'SPARQL-QUERY-' + Digest::SHA2.hexdigest([extra_params, accept_header, sparql, Tripod.query_endpoint].join("-")) 41 | Tripod.cache_store.fetch(cache_key, &stream_data) 42 | else 43 | Tripod.logger.debug "TRIPOD caching is off!" 44 | stream_data.call() 45 | end 46 | 47 | end 48 | 49 | # Tripod helper to turn a hash to a query string, allowing multiple params in arrays 50 | # e.g. :query=>'foo', :graph=>['bar', 'baz'] 51 | # -> query=foo&graph=bar&graph=baz 52 | # based on the ActiveSupport implementation, but with different behaviour for arrays 53 | def self.to_query hash 54 | hash.collect_concat do |key, value| 55 | if value.class == Array 56 | value.collect { |v| v.to_query( key ) } 57 | else 58 | value.to_query(key) 59 | end 60 | end.sort * '&' 61 | end 62 | 63 | # Runs a SELECT +query+ against the endpoint. Returns a Hash of the results. 64 | # 65 | # @param [ String ] query The query to run 66 | # 67 | # @example Run a SELECT query 68 | #  Tripod::SparqlClient::Query.select('SELECT * WHERE {?s ?p ?o}') 69 | # 70 | # @return [ Hash, String ] 71 | def self.select(query) 72 | query_response = self.query(query, "application/sparql-results+json") 73 | if query_response.length >0 74 | JSON.parse(query_response)["results"]["bindings"] 75 | else 76 | [] 77 | end 78 | end 79 | 80 | def self._response_limit_options(response_limit_bytes) 81 | case response_limit_bytes 82 | when Integer 83 | {response_limit_bytes: response_limit_bytes} 84 | when :default 85 | {response_limit_bytes: Tripod.response_limit_bytes} 86 | when :no_response_limit 87 | {} 88 | end 89 | end 90 | end 91 | 92 | module Update 93 | 94 | # Runs a +sparql+ update against the endpoint. Returns true if success. 95 | # 96 | # @example Run a query 97 | # Tripod::SparqlClient::Update.update('DELETE {?s ?p ?o} WHERE {?s ?p ?o};') 98 | # 99 | # @return [ true ] 100 | def self.update(sparql) 101 | begin 102 | headers = Tripod.extra_endpoint_headers 103 | RestClient::Request.execute( 104 | :method => :post, 105 | :url => Tripod.update_endpoint, 106 | :timeout => Tripod.timeout_seconds, 107 | :payload => { update: sparql }.merge(Tripod.extra_endpoint_params), 108 | :headers => headers 109 | ) 110 | true 111 | rescue RestClient::BadRequest => e 112 | # just re-raise as a BadSparqlRequest Exception 113 | raise Tripod::Errors::BadSparqlRequest.new(e.http_body, e) 114 | end 115 | end 116 | 117 | end 118 | 119 | end 120 | -------------------------------------------------------------------------------- /spec/tripod/criteria_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe Tripod::Criteria do 4 | 5 | let(:person_criteria) { Person.all } 6 | 7 | let(:resource_criteria) { Resource.all } 8 | 9 | describe "#initialize" do 10 | 11 | it "should set the resource class accessor" do 12 | person_criteria.resource_class.should == Person 13 | end 14 | 15 | it "should initialize the extra clauses to a blank array" do 16 | person_criteria.extra_clauses.should == [] 17 | end 18 | 19 | it "should initialize the graph lambdas to a blank array" do 20 | person_criteria.graph_lambdas.should == [] 21 | end 22 | 23 | context "with rdf_type set on the class" do 24 | it "should initialize the where clauses to include a type restriction" do 25 | person_criteria.where_clauses.should == ["?uri a "] 26 | end 27 | end 28 | 29 | context "with no rdf_type set on the class" do 30 | it "should initialize the where clauses to ?uri ?p ?o" do 31 | resource_criteria.where_clauses.should == [] 32 | end 33 | end 34 | end 35 | 36 | describe "#where" do 37 | 38 | context 'given a string' do 39 | it "should add the sparql snippet to the where clauses" do 40 | resource_criteria.where("blah") 41 | resource_criteria.where_clauses.should == ["blah"] 42 | end 43 | 44 | it "should return an instance of Criteria" do 45 | resource_criteria.where("blah").class == Tripod::Criteria 46 | end 47 | 48 | it "should return an instance of Criteria with the where clauses added" do 49 | resource_criteria.where("blah").where_clauses.should == ["blah"] 50 | end 51 | end 52 | 53 | context 'given a hash' do 54 | context 'with a native Ruby value' do 55 | let(:value) { 'blah' } 56 | 57 | it 'should construct a sparql snippet with the appropriate predicate, treating the value as a literal' do 58 | criteria = resource_criteria.where(label: value) 59 | criteria.where_clauses[0].should == "?uri <#{ RDF::RDFS.label }> \"#{ value }\"" 60 | end 61 | end 62 | 63 | context 'with a native RDF value' do 64 | let(:value) { RDF::URI.new('http://example.com/bob') } 65 | 66 | it 'should construct a sparql snippet with the appropriate predicate' do 67 | criteria = resource_criteria.where(label: value) 68 | criteria.where_clauses[0].should == "?uri <#{ RDF::RDFS.label }> <#{ value.to_s }>" 69 | end 70 | end 71 | end 72 | end 73 | 74 | describe "#extras" do 75 | 76 | it "should add the sparql snippet to the extra clauses" do 77 | resource_criteria.extras("bleh") 78 | resource_criteria.extra_clauses.should == ["bleh"] 79 | end 80 | 81 | it "should return an instance of Criteria" do 82 | resource_criteria.extras("bleh").class == Tripod::Criteria 83 | end 84 | 85 | it "should return an instance of Criteria with the extra clauses added" do 86 | resource_criteria.extras("bleh").extra_clauses.should == ["bleh"] 87 | end 88 | end 89 | 90 | describe "#limit" do 91 | it "calls extras with the right limit clause" do 92 | resource_criteria.limit(10) 93 | resource_criteria.limit_clause.should == "LIMIT 10" 94 | end 95 | 96 | context 'calling it twice' do 97 | it 'should overwrite the previous version' do 98 | resource_criteria.limit(10) 99 | resource_criteria.limit(20) 100 | resource_criteria.limit_clause.should == "LIMIT 20" 101 | end 102 | end 103 | end 104 | 105 | describe "#offset" do 106 | it "calls extras with the right limit clause" do 107 | resource_criteria.offset(10) 108 | resource_criteria.offset_clause.should == "OFFSET 10" 109 | end 110 | 111 | context 'calling it twice' do 112 | it 'should overwrite the previous version' do 113 | resource_criteria.offset(10) 114 | resource_criteria.offset(30) 115 | resource_criteria.offset_clause.should == "OFFSET 30" 116 | end 117 | end 118 | end 119 | 120 | describe "#order" do 121 | it "calls extras with the right limit clause" do 122 | resource_criteria.order("DESC(?label)") 123 | resource_criteria.order_clause.should == "ORDER BY DESC(?label)" 124 | end 125 | 126 | context 'calling it twice' do 127 | it 'should overwrite the previous version' do 128 | resource_criteria.order("DESC(?label)") 129 | resource_criteria.order("ASC(?label)") 130 | resource_criteria.order_clause.should == "ORDER BY ASC(?label)" 131 | end 132 | end 133 | end 134 | 135 | describe "#graph" do 136 | it "sets the graph_uri for this criteria, as a string" do 137 | resource_criteria.graph(RDF::URI("http://example.com/foobar")) 138 | resource_criteria.graph_uri.should == "http://example.com/foobar" 139 | end 140 | 141 | context "with a block" do 142 | 143 | it "will convert each criteria in the block to a query" do 144 | resource_criteria.graph(nil) do 145 | "?uri ?p ?o" 146 | end.where("?uri ?p ?g") 147 | 148 | resource_criteria.graph_lambdas.should_not be_empty 149 | end 150 | 151 | end 152 | end 153 | 154 | end 155 | -------------------------------------------------------------------------------- /lib/tripod/persistence.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | # This module defines behaviour for persisting to the database. 4 | module Tripod::Persistence 5 | extend ActiveSupport::Concern 6 | 7 | class Tripod::Persistence::Transaction 8 | 9 | def initialize 10 | self.transaction_id = Guid.new.to_s 11 | end 12 | 13 | attr_accessor :transaction_id 14 | attr_accessor :query 15 | 16 | def commit 17 | Tripod::SparqlClient::Update.update(self.query) 18 | clear_transaction 19 | end 20 | 21 | def abort 22 | clear_transaction 23 | end 24 | 25 | def clear_transaction 26 | self.transaction_id = nil 27 | self.query = "" 28 | Tripod::Persistence.transactions.delete(self.transaction_id) 29 | end 30 | 31 | def self.valid_transaction(transaction) 32 | transaction && transaction.class == Tripod::Persistence::Transaction 33 | end 34 | 35 | def self.get_transaction(trans) 36 | transaction = nil 37 | 38 | if Tripod::Persistence::Transaction.valid_transaction(trans) 39 | 40 | transaction_id = trans.transaction_id 41 | 42 | Tripod::Persistence.transactions ||= {} 43 | 44 | if Tripod::Persistence.transactions[transaction_id] 45 | # existing transaction 46 | transaction = Tripod::Persistence.transactions[transaction_id] 47 | else 48 | # new transaction 49 | transaction = Tripod::Persistence.transactions[transaction_id] = trans 50 | end 51 | end 52 | 53 | transaction 54 | end 55 | 56 | end 57 | 58 | # hash of transactions against their ids. 59 | mattr_accessor :transactions 60 | 61 | # Save the resource. 62 | # Note: regardless of whether it's a new_record or not, we always make the 63 | # db match the contents of this resource's statements. 64 | # 65 | # @example Save the resource. 66 | # resource.save 67 | # 68 | # @return [ true, false ] True is success, false if not. 69 | def save(opts={}) 70 | run_callbacks :save do 71 | raise Tripod::Errors::GraphUriNotSet.new() unless @graph_uri 72 | 73 | transaction = Tripod::Persistence::Transaction.get_transaction(opts[:transaction]) 74 | 75 | if self.valid? 76 | graph_selector = self.graph_uri.present? ? "<#{graph_uri.to_s}>" : "?g" 77 | query = " 78 | DELETE {GRAPH #{graph_selector} {<#{@uri.to_s}> ?p ?o}} WHERE {GRAPH #{graph_selector} {<#{@uri.to_s}> ?p ?o}}; 79 | INSERT DATA { 80 | GRAPH <#{@graph_uri}> { 81 | #{ @repository.dump(:ntriples) } 82 | } 83 | }; 84 | " 85 | 86 | if transaction 87 | transaction.query ||= "" 88 | transaction.query += query 89 | else 90 | Tripod::SparqlClient::Update::update(query) 91 | end 92 | 93 | @new_record = false # if running in a trans, just assume it worked. If the query is dodgy, it will throw an exception later. 94 | post_persist 95 | true 96 | else 97 | false 98 | end 99 | end 100 | end 101 | 102 | # Save the resource, and raise an exception if it fails. 103 | # Note: As with save(), regardless of whether it's a new_record or not, we always make the 104 | # db match the contents of this resource's statements. 105 | # 106 | # @example Save the resource. 107 | # resource.save 108 | # 109 | # @raise [Tripod::Errors::Validations] if invalid 110 | # 111 | # @return [ true ] True is success. 112 | def save!(opts={}) 113 | # try to save 114 | unless self.save(opts) 115 | 116 | # if we get in here, save failed. 117 | 118 | # abort the transaction 119 | transaction = Tripod::Persistence::Transaction.get_transaction(opts[:transaction]) 120 | transaction.abort() if transaction 121 | 122 | self.class.fail_validate!(self) # throw an exception 123 | 124 | # TODO: similar stuff for callbacks? 125 | end 126 | return true 127 | end 128 | 129 | def destroy(opts={}) 130 | run_callbacks :destroy do 131 | transaction = Tripod::Persistence::Transaction.get_transaction(opts[:transaction]) 132 | 133 | query = " 134 | # delete from default graph: 135 | DELETE {<#{@uri.to_s}> ?p ?o} WHERE {<#{@uri.to_s}> ?p ?o}; 136 | # delete from named graphs: 137 | DELETE {GRAPH ?g {<#{@uri.to_s}> ?p ?o}} WHERE {GRAPH ?g {<#{@uri.to_s}> ?p ?o}}; 138 | " 139 | 140 | if transaction 141 | transaction.query ||= "" 142 | transaction.query += query 143 | else 144 | Tripod::SparqlClient::Update::update(query) 145 | end 146 | 147 | @destroyed = true 148 | true 149 | end 150 | end 151 | 152 | def update_attribute(name, value, opts={}) 153 | write_attribute(name, value) 154 | save(opts) 155 | end 156 | 157 | def update_attributes(attributes, opts={}) 158 | attributes.each_pair do |name, value| 159 | send "#{name}=", value 160 | end 161 | save(opts) 162 | end 163 | 164 | module ClassMethods #:nodoc: 165 | 166 | # Raise an error if validation failed. 167 | # 168 | # @example Raise the validation error. 169 | # Person.fail_validate!(person) 170 | # 171 | # @param [ Resource ] resource The resource to fail. 172 | def fail_validate!(resource) 173 | raise Tripod::Errors::Validations.new(resource) 174 | end 175 | 176 | end 177 | 178 | end 179 | -------------------------------------------------------------------------------- /spec/tripod/attributes_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe Tripod::Attributes do 4 | describe ".read_attribute" do 5 | 6 | let!(:other_person) do 7 | p = Person.new('http://example.com/id/garry') 8 | p.save! 9 | p 10 | end 11 | 12 | let(:person) do 13 | p = Person.new('http://example.com/id/barry') 14 | p.name = 'Barry' 15 | p.father = other_person.uri 16 | p 17 | end 18 | 19 | context "for a literal" do 20 | it "should return the native type" do 21 | person[:name].class.should == String 22 | end 23 | it "should read the given attribute" do 24 | person[:name].should == 'Barry' 25 | end 26 | end 27 | 28 | context "for a uri" do 29 | it "should return an RDF::URI" do 30 | person[:father].class.should == RDF::URI 31 | end 32 | 33 | it "should read the given attribute" do 34 | person[:father].should == other_person.uri 35 | end 36 | end 37 | 38 | context "where the attribute is multi-valued" do 39 | before do 40 | person.aliases = ['Boz', 'Baz', 'Bez'] 41 | end 42 | 43 | it "should return an array" do 44 | person[:aliases].should == ['Boz', 'Baz', 'Bez'] 45 | end 46 | end 47 | 48 | context "where field is given and single-valued" do 49 | let(:field) { Person.send(:field_for, :hat_type, 'http://example.com/hat', {}) } 50 | before do 51 | person.stub(:read_predicate).with('http://example.com/hat').and_return([RDF::Literal.new('fez')]) 52 | end 53 | 54 | it "should use the predicate name from the given field" do 55 | person.should_receive(:read_predicate).with('http://example.com/hat').and_return([RDF::Literal.new('fez')]) 56 | person.read_attribute(:hat_type, field) 57 | end 58 | 59 | it "should return a single value" do 60 | person.read_attribute(:hat_type, field).should == 'fez' 61 | end 62 | end 63 | 64 | context "where field is given and is multi-valued" do 65 | let(:field) { Person.send(:field_for, :hat_types, 'http://example.com/hat', {multivalued: true}) } 66 | before do 67 | person.stub(:read_predicate).with('http://example.com/hat').and_return([RDF::Literal.new('fez'), RDF::Literal.new('bowler')]) 68 | end 69 | 70 | it "should return an array of values" do 71 | person.read_attribute(:hat_types, field).should == ['fez', 'bowler'] 72 | end 73 | end 74 | 75 | context "where there is no field with the given name" do 76 | it "should raise a 'field not present' error" do 77 | expect { person.read_attribute(:hoof_size) }.to raise_error(Tripod::Errors::FieldNotPresent) 78 | end 79 | end 80 | end 81 | 82 | describe ".write_attribute" do 83 | let(:person) { Person.new('http://example.com/id/barry') } 84 | 85 | it "should write the given attribute" do 86 | person[:name] = 'Barry' 87 | person.name.should == 'Barry' 88 | end 89 | 90 | it "should co-erce the value given to the correct datatype" do 91 | person[:age] = 34 92 | person.read_predicate('http://example.com/age').first.datatype.should == RDF::XSD.integer 93 | end 94 | 95 | it "should not write the predicate given a blank value" do 96 | person[:name] = '' 97 | person.read_predicate('http://example.com/name').should be_empty 98 | end 99 | 100 | context "where the attribute is a uri" do 101 | it "should convert a string to an RDF::URI" do 102 | person[:father] = 'http://example.com/darth' 103 | person.read_predicate('http://example.com/father').first.should be_a(RDF::URI) 104 | end 105 | end 106 | 107 | context "where the attribute is multi-valued" do 108 | it "should co-erce all the values to the correct datatype" do 109 | person[:important_dates] = [Date.today] 110 | person.read_predicate('http://example.com/importantdates').first.datatype.should == RDF::XSD.date 111 | end 112 | end 113 | 114 | context "where field is given" do 115 | let(:field) { Person.send(:add_field, :hat_type, 'http://example.com/hat') } 116 | 117 | it "should derive the predicate name from the given field" do 118 | person.write_attribute(:hat_type, 'http://example.com/bowlerhat', field) 119 | person.read_predicate('http://example.com/hat').first.to_s.should == 'http://example.com/bowlerhat' 120 | end 121 | end 122 | 123 | context "where a field of a particular datatype is given" do 124 | let(:field) { Person.send(:add_field, :hat_size, 'http://example.com/hatsize', {datatype: RDF::XSD.integer}) } 125 | 126 | it "should derive the datatype from the given field" do 127 | person.write_attribute(:hat_size, 10, field) 128 | person.read_predicate('http://example.com/hatsize').first.datatype.should == RDF::XSD.integer 129 | end 130 | end 131 | 132 | context "where a multi-valued field of a given datatype is given" do 133 | let(:field) { Person.send(:add_field, :hat_heights, 'http://example.com/hatheight', {datatype: RDF::XSD.integer, multivalued: true}) } 134 | 135 | it "should co-erce the values passed" do 136 | person.write_attribute(:hat_heights, [5, 10, 15], field) 137 | person.read_predicate('http://example.com/hatheight').first.datatype.should == RDF::XSD.integer 138 | end 139 | end 140 | 141 | context "where there is no field with the given name" do 142 | it "should raise a 'field not present' error" do 143 | lambda { person.write_attribute(:hoof_size, 'A1') }.should raise_error(Tripod::Errors::FieldNotPresent) 144 | end 145 | end 146 | end 147 | end 148 | -------------------------------------------------------------------------------- /spec/tripod/sparql_query_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe Tripod::SparqlQuery do 4 | 5 | describe '#initialize' do 6 | context 'given a query without prefixes' do 7 | it 'should assign the given query to the body attribute' do 8 | q = Tripod::SparqlQuery.new('SELECT xyz') 9 | q.body.should == 'SELECT xyz' 10 | end 11 | end 12 | 13 | context 'given a query with prefixes' do 14 | it 'should separate the query into prefixes and body' do 15 | q = Tripod::SparqlQuery.new('PREFIX e: SELECT xyz') 16 | q.prefixes.should == 'PREFIX e: ' 17 | q.body.should == 'SELECT xyz' 18 | end 19 | end 20 | 21 | context 'given a query with a leading space' do 22 | it 'should successfully split up the query' do 23 | q = Tripod::SparqlQuery.new(' PREFIX e: SELECT xyz') 24 | q.prefixes.should == 'PREFIX e: ' 25 | q.body.should == 'SELECT xyz' 26 | end 27 | end 28 | 29 | context 'given a query with comments' do 30 | it 'should successfully split up the query' do 31 | q = Tripod::SparqlQuery.new('PREFIX e: 32 | SELECT xyz # foo 33 | WHERE abc # bar 34 | # baz' 35 | ) 36 | q.prefixes.should == 'PREFIX e: ' 37 | q.body.should == 'SELECT xyz # foo 38 | WHERE abc # bar 39 | # baz' 40 | end 41 | end 42 | 43 | context 'given a query with interpolations' do 44 | it 'should interpolate the query' do 45 | q = Tripod::SparqlQuery.new('SELECT xyz WHERE %{foo}', foo: 'bar') 46 | q.query.should == 'SELECT xyz WHERE bar' 47 | end 48 | 49 | context 'where there are missing interpolation values' do 50 | it "should raise a 'missing variables' exception" do 51 | expect { Tripod::SparqlQuery.new('SELECT xyz WHERE %{foo}', {bar: 'baz'}) }.to raise_error(Tripod::SparqlQueryMissingVariables) 52 | end 53 | end 54 | end 55 | end 56 | 57 | describe "#has_prefixes?" do 58 | 59 | context "for a query with prefixes" do 60 | it "should return true" do 61 | q = Tripod::SparqlQuery.new('PREFIX e: SELECT xyz') 62 | q.has_prefixes?.should be true 63 | end 64 | end 65 | 66 | context "for a query without prefixes" do 67 | it "should return false" do 68 | q = Tripod::SparqlQuery.new('SELECT xyz') 69 | q.has_prefixes?.should be false 70 | end 71 | end 72 | 73 | end 74 | 75 | describe "#query_type" do 76 | 77 | it 'should return :select given a SELECT query' do 78 | q = Tripod::SparqlQuery.new('SELECT xyz') 79 | q.query_type.should == :select 80 | end 81 | 82 | it 'should return :construct given a CONSTRUCT query' do 83 | q = Tripod::SparqlQuery.new('CONSTRUCT ') 84 | q.query_type.should == :construct 85 | end 86 | 87 | it 'should return :construct given a DESCRIBE query' do 88 | q = Tripod::SparqlQuery.new('DESCRIBE ') 89 | q.query_type.should == :describe 90 | end 91 | 92 | it 'should return :ask given an ASK query' do 93 | q = Tripod::SparqlQuery.new('ASK ') 94 | q.query_type.should == :ask 95 | end 96 | 97 | it "should return :unknown given an unknown type" do 98 | q = Tripod::SparqlQuery.new('FOO ') 99 | q.query_type.should == :unknown 100 | end 101 | end 102 | 103 | describe '#extract_prefixes' do 104 | it 'should return the prefixes and query body separately' do 105 | q = Tripod::SparqlQuery.new('PREFIX e: SELECT xyz') 106 | p, b = q.extract_prefixes 107 | p.should == 'PREFIX e: ' 108 | b.should == 'SELECT xyz' 109 | end 110 | end 111 | 112 | describe '#as_count_query_str' do 113 | context "for non-selects" do 114 | it "should throw an exception" do 115 | lambda { 116 | q = Tripod::SparqlQuery.new('ASK { ?s ?p ?o }') 117 | q.as_count_query_str 118 | }.should raise_error(Tripod::SparqlQueryError) 119 | end 120 | end 121 | 122 | context "for selects" do 123 | context 'without prefixes' do 124 | it "should return a new SparqlQuery with the original query wrapped in a count" do 125 | q = Tripod::SparqlQuery.new('SELECT ?s WHERE { ?s ?p ?o }') 126 | q.as_count_query_str.should == 'SELECT (COUNT(*) as ?tripod_count_var) { 127 | SELECT ?s WHERE { ?s ?p ?o } 128 | }' 129 | end 130 | end 131 | 132 | context 'with prefixes' do 133 | it "should move the prefixes to the start" do 134 | q = Tripod::SparqlQuery.new('PREFIX e: SELECT ?s WHERE { ?s ?p ?o }') 135 | q.as_count_query_str.should == 'PREFIX e: SELECT (COUNT(*) as ?tripod_count_var) { 136 | SELECT ?s WHERE { ?s ?p ?o } 137 | }' 138 | end 139 | end 140 | end 141 | end 142 | 143 | describe "#as_first_query_str" do 144 | context "for non-selects" do 145 | it "should throw an exception" do 146 | lambda { 147 | q = Tripod::SparqlQuery.new('ASK { ?s ?p ?o }') 148 | q.as_first_query_str 149 | }.should raise_error(Tripod::SparqlQueryError) 150 | end 151 | end 152 | 153 | context "for selects" do 154 | context 'without prefixes' do 155 | it "should return a new SparqlQuery with the original query wrapped in a count" do 156 | q = Tripod::SparqlQuery.new('SELECT ?s WHERE { ?s ?p ?o }') 157 | q.as_first_query_str.should == 'SELECT * { SELECT ?s WHERE { ?s ?p ?o } } LIMIT 1' 158 | end 159 | end 160 | 161 | context 'with prefixes' do 162 | it "should move the prefixes to the start" do 163 | q = Tripod::SparqlQuery.new('PREFIX e: SELECT ?s WHERE { ?s ?p ?o }') 164 | q.as_first_query_str.should == 'PREFIX e: SELECT * { SELECT ?s WHERE { ?s ?p ?o } } LIMIT 1' 165 | end 166 | end 167 | 168 | end 169 | 170 | end 171 | 172 | end -------------------------------------------------------------------------------- /lib/tripod/fields.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | require "tripod/fields/standard" 3 | 4 | # This module defines behaviour for fields. 5 | module Tripod::Fields 6 | extend ActiveSupport::Concern 7 | 8 | def self.included(base) 9 | base.instance_eval do 10 | @fields ||= {} 11 | end 12 | base.extend(ClassMethods) 13 | end 14 | 15 | def fields 16 | self.class.fields 17 | end 18 | 19 | module ClassMethods 20 | 21 | # Defines all the fields that are accessible on the Resource 22 | # For each field that is defined, a getter and setter will be 23 | # added as an instance method to the Resource. 24 | # 25 | # @example Define a field. 26 | # field :name, 'http://example.com/name' 27 | # 28 | # @example Define a field of a specific RDF type 29 | # field :modified_at, 'http://example.com/modified_at', datatype: RDF::XSD.DateTime 30 | # 31 | # @example Define a multi-valued field (can be combined with other options) 32 | # field :tags, 'http://example.com/tag', multivalued: true 33 | # 34 | # @example Define a field containing a URI to another RDF resource 35 | # field :knows, 'http://example.com/knows', is_uri: true 36 | # 37 | # @param [ Symbol ] name The name of the field. 38 | # @param [ String, RDF::URI ] predicate The predicate for the field. 39 | # @param [ Hash ] options The options to pass to the field. 40 | # 41 | # @option options [ String, RDF::URI ] datatype The uri of the datatype for the field (will be used to create an RDF::Literal of the right type on the way in only). 42 | # @option options [ Boolean ] multivalued Is this a multi-valued field? Default is false. 43 | # 44 | # @return [ Field ] The generated field 45 | def field(name, predicate, options = {}) 46 | @fields ||= {} 47 | add_field(name, predicate, options) 48 | end 49 | 50 | # Return the field object on a +Resource+ associated with the given name. 51 | # 52 | # @example Get the field. 53 | # Person.get_field(:name) 54 | # 55 | # @param [ Symbol ] name The name of the field. 56 | def get_field(name) 57 | @fields ||= {} 58 | field = fields[name] 59 | raise Tripod::Errors::FieldNotPresent.new unless field 60 | field 61 | end 62 | 63 | # Return all of the fields on a +Resource+ in a manner that 64 | # respects Ruby's inheritance rules. i.e. subclass fields should 65 | # override superclass fields with the same 66 | def fields 67 | tripod_superclasses.map { |c| c.instance_variable_get(:@fields) }.reduce do |acc,class_fields| 68 | class_fields.merge(acc) 69 | end 70 | end 71 | 72 | protected 73 | 74 | def tripod_superclasses 75 | self.ancestors.select { |a| a.class == Class && a.respond_to?(:fields)} 76 | end 77 | 78 | # Define a field attribute for the +Resource+. 79 | # 80 | # @example Set the field. 81 | # Person.add_field(:name, 'http://myfield') 82 | # 83 | # @param [ Symbol ] name The name of the field. 84 | # @param [ String, RDF::URI ] predicate The predicate for the field. 85 | # @param [ Hash ] options The hash of options. 86 | def add_field(name, predicate, options = {}) 87 | # create a field object and store it in our hash 88 | field = field_for(name, predicate, options) 89 | @fields ||= {} 90 | @fields[name] = field 91 | 92 | # set up the accessors for the fields 93 | create_accessors(name, name, options) 94 | 95 | # create a URL validation if appropriate 96 | # (format nabbed from https://gist.github.com/joshuap/948880) 97 | validates(name, is_url: true) if field.is_uri? 98 | 99 | field 100 | end 101 | 102 | # Create the field accessors. 103 | # 104 | # @example Generate the accessors. 105 | # Person.create_accessors(:name, "name") 106 | # person.name #=> returns the field 107 | # person.name = "" #=> sets the field 108 | # person.name? #=> Is the field present? 109 | # 110 | # @param [ Symbol ] name The name of the field. 111 | # @param [ Symbol ] meth The name of the accessor. 112 | # @param [ Hash ] options The options. 113 | def create_accessors(name, meth, options = {}) 114 | field = @fields[name] 115 | 116 | create_field_getter(name, meth, field) 117 | create_field_setter(name, meth, field) 118 | create_field_check(name, meth, field) 119 | 120 | # from dirty.rb 121 | create_dirty_methods(name, meth) 122 | end 123 | 124 | # Create the getter method for the provided field. 125 | # 126 | # @example Create the getter. 127 | # Model.create_field_getter("name", "name", field) 128 | # 129 | # @param [ String ] name The name of the attribute. 130 | # @param [ String ] meth The name of the method. 131 | # @param [ Field ] field The field. 132 | def create_field_getter(name, meth, field) 133 | generated_methods.module_eval do 134 | re_define_method(meth) do 135 | read_attribute(name, field) 136 | end 137 | end 138 | end 139 | 140 | # Create the setter method for the provided field. 141 | # 142 | # @example Create the setter. 143 | # Model.create_field_setter("name", "name") 144 | # 145 | # @param [ String ] name The name of the attribute. 146 | # @param [ String ] meth The name of the method. 147 | # @param [ Field ] field The field. 148 | def create_field_setter(name, meth, field) 149 | generated_methods.module_eval do 150 | re_define_method("#{meth}=") do |value| 151 | write_attribute(name, value, field) 152 | end 153 | end 154 | end 155 | 156 | # Create the check method for the provided field. 157 | # 158 | # @example Create the check. 159 | # Model.create_field_check("name", "name") 160 | # 161 | # @param [ String ] name The name of the attribute. 162 | # @param [ String ] meth The name of the method. 163 | def create_field_check(name, meth, field) 164 | generated_methods.module_eval do 165 | re_define_method("#{meth}?") do 166 | attr = read_attribute(name, field) 167 | attr == true || attr.present? 168 | end 169 | end 170 | end 171 | 172 | # Include the field methods as a module, so they can be overridden. 173 | # 174 | # @example Include the fields. 175 | # Person.generated_methods 176 | # 177 | # @return [ Module ] The module of generated methods. 178 | def generated_methods 179 | @generated_methods ||= begin 180 | mod = Module.new 181 | include(mod) 182 | mod 183 | end 184 | end 185 | 186 | 187 | # instantiates and returns a new standard field 188 | def field_for(name, predicate, options) 189 | Tripod::Fields::Standard.new(name, predicate, options) 190 | end 191 | end 192 | end 193 | -------------------------------------------------------------------------------- /lib/tripod/links.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | require "tripod/links/linked_to" 3 | require "tripod/links/linked_from" 4 | 5 | # This module defines behaviour for fields. 6 | module Tripod::Links 7 | extend ActiveSupport::Concern 8 | 9 | included do 10 | class_attribute :linked_tos 11 | class_attribute :linked_froms 12 | self.linked_tos = {} 13 | self.linked_froms = {} 14 | end 15 | 16 | module ClassMethods 17 | 18 | # Define a link to another resource. Creates relevant fields and getter/setter methods. Note that the getter only retrives saved resources from the db. 19 | # 20 | # @example Define a link away from resources of this class to resources of class Organisation 21 | # linked_to :organisation, 'http://example.com/name' 22 | # 23 | # @example Define a multivalued link away from resources of this class (can be combined with other options) 24 | # linked_to :organisations, 'http://example.com/modified_at', multivalued: true 25 | # 26 | # @example Define a link away from resources of this class, specifying the class and the field name that will be generated 27 | # linked_to :org, 'http://example.com/modified_at', class_name: 'Organisation', field: my_field 28 | # 29 | # @param [ Symbol ] name The name of the link. 30 | # @param [ String, RDF::URI ] predicate The predicate for the field. 31 | # @param [ Hash ] options The options to pass to the field. 32 | # 33 | # @option options [ Boolean ] multivalued Is this a multi-valued field? Default is false. 34 | # @option options [ String ] class_name The name of the class of resource which we're linking to (normally will derive this from the link name) 35 | # @option options [ Symbol ] field_name the symbol of the field that will be generated (normally will just add _uri or _uris to the link name) 36 | # 37 | # @return [ LinkedTo ] The generated link 38 | def linked_to(name, predicate, options = {}) 39 | add_linked_to(name, predicate, options) 40 | end 41 | 42 | 43 | # Define that another resource links to this one. Creates a getter with the name you specify. 44 | # For this to work, the incoming class needs to define a linked_to relationship. 45 | # Just creates the relevant getter which always return an array of objects. 46 | # 47 | # @example make a method called people which returns Dog objects, via the linked_to :owner field on Dog. We guess the class name based on the linked_from name. 48 | #  linked_from :dogs, :owner 49 | # 50 | # @example make a method called doggies which returns Dog objects, via the linked_to :person field on Dog. 51 | #  linked_from :doggies, :person, class_name: 'Dog' 52 | # 53 | # @param [ Symbol ] name The name of the link. 54 | # @param [ Symbol ] incoming_field_name The name of the linked_to relationship on the other class 55 | # @param [ Hash ] options The options to pass to the field. 56 | # 57 | # @option options [ String ] class_name The name of the class that links to this resource, if we can't guess it from the link name 58 | # 59 | # @return [ LinkedTo ] The generated link 60 | def linked_from(name, incoming_field_name, options = {}) 61 | add_linked_from(name, incoming_field_name, options) 62 | end 63 | 64 | protected 65 | 66 | def add_linked_to(name, predicate, options={}) 67 | link = linked_to_for(name, predicate, options) 68 | linked_tos[name] = link 69 | 70 | # create the field (always is_uri) 71 | add_field(link.field_name, predicate, options.merge(is_uri: true)) 72 | 73 | create_linked_to_accessors(name, name) 74 | end 75 | 76 | def add_linked_from(name, incoming_field_name, options={}) 77 | link = linked_from_for(name, incoming_field_name, options) 78 | linked_froms[name] = link 79 | 80 | create_linked_from_getter(name, name, link) 81 | end 82 | 83 | def create_linked_to_accessors(name, meth) 84 | link = linked_tos[name] 85 | 86 | create_linked_to_getter(name, meth, link) 87 | create_linked_to_setter(name, meth, link) 88 | end 89 | 90 | def create_linked_from_getter(name, meth, link) 91 | 92 | generated_methods.module_eval do 93 | re_define_method(meth) do 94 | klass = Kernel.const_get(link.class_name) 95 | 96 | incoming_link = klass.linked_tos[link.incoming_field_name.to_sym] 97 | incoming_predicate = klass.fields[incoming_link.field_name].predicate 98 | 99 | # note - this will only find saved ones. 100 | klass 101 | .where("?uri <#{incoming_predicate.to_s}> <#{self.uri.to_s}>") 102 | .resources 103 | end 104 | end 105 | end 106 | 107 | def create_linked_to_getter(name, meth, link) 108 | 109 | generated_methods.module_eval do 110 | re_define_method(meth) do 111 | 112 | klass = Kernel.eval(link.class_name) 113 | 114 | if link.multivalued? 115 | 116 | # # TODO: is there a more efficient way of doing this? 117 | 118 | # note that we can't just do a query like this: 119 | # `klass.where('<#{self.uri.to_s}> <#{predicate.to_s}> ?uri').resources` 120 | # ... because this will only find saved ones and we want to find resources 121 | # whose uris have been set on the resource but not saved to db yet. 122 | 123 | criteria = klass.where('?uri ?p ?o') 124 | 125 | uris = read_attribute(link.field_name) 126 | 127 | filter_str = "" 128 | 129 | if uris.any? 130 | filter_str += " VALUES ?uri { <" 131 | filter_str += uris.join("> <") 132 | filter_str += "> } " 133 | else 134 | filter_str += "FILTER (1 = 0)" 135 | end 136 | 137 | 138 | criteria.where(filter_str).resources 139 | else 140 | klass.find(read_attribute(link.field_name)) rescue nil #look it up by it's uri 141 | end 142 | 143 | end 144 | end 145 | end 146 | 147 | def create_linked_to_setter(name, meth, link) 148 | 149 | generated_methods.module_eval do 150 | re_define_method("#{meth}=") do |value| 151 | 152 | if link.multivalued? 153 | val = value.to_a.map{ |v| v.uri } 154 | write_attribute( link.field_name, val) 155 | else 156 | # set the uri from the passed in resource 157 | write_attribute( link.field_name, value.uri ) 158 | end 159 | end 160 | end 161 | end 162 | 163 | # instantiates and returns a new LinkedFrom 164 | def linked_from_for(name, incoming_field_name, options) 165 | Tripod::Links::LinkedFrom.new(name, incoming_field_name, options) 166 | end 167 | 168 | # instantiates and returns a new LinkTo 169 | def linked_to_for(name, predicate, options) 170 | Tripod::Links::LinkedTo.new(name, predicate, options) 171 | end 172 | 173 | end 174 | 175 | end -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/Swirrl/tripod.svg?branch=master)](https://travis-ci.org/Swirrl/tripod) 2 | 3 | # Tripod 4 | 5 | ActiveModel-style Ruby ORM for RDF Linked Data. Works with SPARQL 1.1 HTTP endpoints. 6 | 7 | * [ActiveModel](https://github.com/rails/rails/tree/master/activemodel)-compliant interface. 8 | * Inspired by [Durran Jordan's](https://github.com/durran) [Mongoid](http://mongoid.org/en/mongoid/) ORM for [MongoDB](http://www.mongodb.org/), and [Ben Lavender's](https://github.com/bhuga) RDF ORM, [Spira](https://github.com/ruby-rdf/spira). 9 | * Uses [Ruby-RDF](https://github.com/ruby-rdf/rdf) to manage the data internally. 10 | 11 | ## Quick start, for using in a rails app. 12 | 13 | 1. Add it to your Gemfile and bundle 14 | 15 | gem tripod 16 | 17 | $ bundle 18 | 19 | 2. Configure it (in application.rb, or development.rb/production.rb/test.rb) 20 | 21 | # (values shown are the defaults) 22 | Tripod.configure do |config| 23 | config.update_endpoint = 'http://127.0.0.1:3030/tripod/update' 24 | config.query_endpoint = 'http://127.0.0.1:3030/tripod/sparql' 25 | config.timeout_seconds = 30 26 | end 27 | 28 | 3. Include it in your model classes. 29 | 30 | class Person 31 | include Tripod::Resource 32 | 33 | # these are the default rdf-type and graph for resources of this class 34 | rdf_type 'http://example.com/person' 35 | graph_uri 'http://example.com/people' 36 | 37 | field :name, 'http://example.com/name' 38 | field :knows, 'http://example.com/knows', :multivalued => true, :is_uri => true 39 | field :aliases, 'http://example.com/alias', :multivalued => true 40 | field :age, 'http://example.com/age', :datatype => RDF::XSD.integer 41 | field :important_dates, 'http://example.com/importantdates', :datatype => RDF::XSD.date, :multivalued => true 42 | end 43 | 44 | # Note: Active Model validations are supported 45 | 46 | 4. Use it 47 | 48 | uri = 'http://example.com/ric' 49 | p = Person.new(uri) 50 | p.name = 'Ric' 51 | p.age = 31 52 | p.aliases = ['Rich', 'Richard'] 53 | p.important_dates = [Date.new(2011,1,1)] 54 | p.save! 55 | 56 | people = Person.all.resources #=> returns all people as an array 57 | 58 | ric = Person.find('http://example.com/ric') #=> returns a single Person object. 59 | 60 | ## Note: 61 | 62 | Tripod doesn't supply a database. You need to install one. I recommend [Fuseki](http://jena.apache.org/documentation/serving_data/index.html), which runs on port 3030 by default. 63 | 64 | 65 | ## Some Other interesting features 66 | 67 | ## Eager Loading 68 | 69 | asa = Person.find('http://example.com/asa') 70 | ric = Person.find('http://example.com/ric') 71 | ric.knows = asa.uri 72 | 73 | ric.eager_load_predicate_triples! #does a big DESCRIBE statement behind the scenes 74 | knows = ric.get_related_resource('http://example.com/knows', Resource) 75 | knows.label # this won't cause another database lookup 76 | 77 | ric.eager_load_object_triples! #does a big DESCRIBE statement behind the scenes 78 | asa = ric.get_related_resource('http://example.com/asa', Person) # returns a fully hydrated Person object for asa, without an extra lookup 79 | 80 | ## Defining a graph at instantiation-time 81 | 82 | class Resource 83 | include Tripod::Resource 84 | field :label, RDF::RDFS.label 85 | 86 | # notice also that you don't need to supply an rdf type or graph here! 87 | end 88 | 89 | r = Resource.new('http://example.com/foo', 'http://example.com/mygraph') 90 | r.label = "example" 91 | r.save 92 | 93 | # Note: Tripod assumes you want to store all resources in named graphs. 94 | # So if you don't supply a graph at any point (i.e. class or instance level), 95 | # you will get an error when you try to persist the resource. 96 | 97 | ## Reading and writing arbitrary predicates 98 | 99 | r.write_predicate(RDF.type, 'http://example.com/myresource/type') 100 | r.read_predicate(RDF.type) #=> [RDF::URI.new("http://example.com/myresource/type")] 101 | 102 | ## Finders and criteria 103 | 104 | # A Tripod::Criteria object defines a set of constraints for a SPARQL query. 105 | # It doesn't actually do anything against the DB until you run resources, first, or count on it. 106 | # (from Tripod::CriteriaExecution) 107 | 108 | Person.all #=> returns a Tripod::Criteria object which selects all resources of rdf_type http://example.com/person, in the http://example.com/people graph 109 | 110 | Resource.all #=> returns a criteria object to return resources in the database (as no rdf_type or graph_uri specified at class level) 111 | 112 | Person.all.resources #=> returns all the actual resources for the criteria object, as an array-like object 113 | 114 | Person.all.resources(:return_graph => false) #=> returns the actual resources, but without returning the graph_uri in the select (helps avoid pagination issues). Note: doesn't set the graph uri on the instantiated resources. 115 | 116 | Person.first #=> returns the first person (by crafting a sparql query under the covers that only returns 1 result) 117 | 118 | Person.first(:return_graph => false) # as with resources, doesn't return / set the graph_uri. 119 | 120 | Person.count #=> returns the count of all people (by crafting a count query under the covers that only returns a count) 121 | 122 | # note that you need to use ?uri as the variable for the subject. 123 | Person.where("?uri 'Joe'") #=> returns a Tripod::Criteria object 124 | 125 | Resource.graph("http://example.com/mygraph") #=> Retruns a criteria object with a graph restriction (note: if graph_uri set on the class, it will default to using this) 126 | 127 | Resource.find_by_sparql('SELECT ?uri ?graph WHERE { GRAPH ?graph { ?uri ?p ?o } }') #=> allows arbitrary sparql. Again, use ?uri for the variable of the subjects (and ?graph for the graph). 128 | 129 | ## Chainable criteria 130 | 131 | Person.all.where("?uri 'Ric'").where("?uri ).first 132 | 133 | Person.where("?uri ?name").limit(1).offset(0).order("DESC(?name)") 134 | 135 | ## Running tests 136 | 137 | With a Fuseki instance ready and up, edit the config in `spec/spec_helper.rb` to reflect your settings. Make sure you `bundle` to pull in all dependencies before trying to run the tests. 138 | 139 | Some tests require memcached to be set up and running. The tests that require memcached are tagged with `:caching_tests => true`; do with this information what you will. 140 | 141 | [Full Documentation](http://rubydoc.info/gems/tripod/frames) 142 | 143 | Copyright (c) 2012 [Swirrl IT Limited](http://swirrl.com). Released under MIT License -------------------------------------------------------------------------------- /spec/tripod/eager_loading_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe Tripod::EagerLoading do 4 | 5 | before do 6 | 7 | @name = Resource.new('http://example.com/name', 'http://example.com/names') 8 | @name.pref_label = "Nom" 9 | @name.label = "Name" 10 | @name.title = "Soubriquet" 11 | @name.write_predicate('http://example.com/name-other-pred', 'hello') # another predicate 12 | @name.save! 13 | 14 | @peter = Person.new('http://example.com/peter') 15 | @peter.name = "Peter" 16 | @peter.age = 30 17 | @peter.save! 18 | 19 | @john = Person.new('http://example.com/john') 20 | @john.name = "john" 21 | @john.knows = @peter.uri 22 | @john.save! 23 | 24 | end 25 | 26 | describe "#eager_load_predicate_triples!" do 27 | 28 | context "with no options passed" do 29 | before do 30 | @peter.eager_load_predicate_triples! 31 | end 32 | 33 | it "should add triples to the repository all for the predicates' predicates" do 34 | triples = @peter.repository.query([ RDF::URI.new('http://example.com/name'), :predicate, :object] ).to_a.sort{|a,b| a.to_s <=> b.to_s } 35 | triples.length.should == 4 36 | triples[0].predicate.should == RDF::URI('http://example.com/name-other-pred') 37 | triples[0].object.to_s.should == "hello" 38 | triples[1].predicate.should == RDF::DC.title 39 | triples[1].object.to_s.should == "Soubriquet" 40 | triples[2].predicate.should == RDF::RDFS.label 41 | triples[2].object.to_s.should == "Name" 42 | triples[3].predicate.should == RDF::SKOS.prefLabel 43 | triples[3].object.to_s.should == "Nom" 44 | end 45 | end 46 | 47 | context "with labels_only option" do 48 | before do 49 | @peter.eager_load_predicate_triples!(:labels_only => true) 50 | end 51 | 52 | it "should add triples to the repository all for the predicates labels only" do 53 | triples = @peter.repository.query([ RDF::URI.new('http://example.com/name'), :predicate, :object] ).to_a 54 | triples.length.should == 1 55 | triples.first.predicate.should == RDF::RDFS.label 56 | triples.first.object.to_s.should == "Name" 57 | end 58 | 59 | end 60 | 61 | context "with array of fields" do 62 | before do 63 | @peter.eager_load_predicate_triples!(:predicates => [RDF::SKOS.prefLabel, RDF::DC.title]) 64 | end 65 | 66 | it "should add triples to the repository all for the given fields of the predicate" do 67 | triples = @peter.repository.query([ RDF::URI.new('http://example.com/name'), :predicate, :object] ).to_a.sort{|a,b| a.to_s <=> b.to_s } 68 | triples.length.should == 2 69 | 70 | triples.first.predicate.should == RDF::DC.title 71 | triples.first.object.to_s.should == "Soubriquet" 72 | 73 | triples.last.predicate.should == RDF::SKOS.prefLabel 74 | triples.last.object.to_s.should == "Nom" 75 | 76 | end 77 | 78 | end 79 | 80 | end 81 | 82 | describe "#eager_load_object_triples!" do 83 | 84 | context "with no options passed" do 85 | before do 86 | @john.eager_load_object_triples! 87 | end 88 | 89 | it "should add triples to the repository for the all the objects' predicates" do 90 | triples = @john.repository.query([ @peter.uri, :predicate, :object] ) 91 | triples.to_a.length.should == 3 92 | 93 | triples.to_a.sort{|a,b| a.to_s <=> b.to_s }[0].predicate.should == RDF::URI('http://example.com/age') 94 | triples.to_a.sort{|a,b| a.to_s <=> b.to_s }[0].object.to_s.should == "30" 95 | 96 | triples.to_a.sort{|a,b| a.to_s <=> b.to_s }[1].predicate.should == RDF::URI('http://example.com/name') 97 | triples.to_a.sort{|a,b| a.to_s <=> b.to_s }[1].object.to_s.should == "Peter" 98 | 99 | triples.to_a.sort{|a,b| a.to_s <=> b.to_s }[2].predicate.should == RDF.type 100 | triples.to_a.sort{|a,b| a.to_s <=> b.to_s }[2].object.to_s.should == RDF::URI('http://example.com/person') 101 | 102 | end 103 | end 104 | 105 | context "with labels_only option" do 106 | before do 107 | @john.eager_load_object_triples!(:labels_only => true) 108 | end 109 | 110 | it "should add triples to the repository all for the object labels only" do 111 | triples = @john.repository.query([ @peter.uri, :predicate, :object] ).to_a 112 | triples.length.should == 0 # people don't have labels 113 | end 114 | 115 | end 116 | 117 | context "with array of fields" do 118 | before do 119 | @john.eager_load_object_triples!(:predicates => ['http://example.com/name']) 120 | end 121 | 122 | it "should add triples to the repository all for the given fields of the object" do 123 | triples = @john.repository.query([ @peter.uri, :predicate, :object] ).to_a.sort{|a,b| a.to_s <=> b.to_s } 124 | 125 | triples.length.should == 1 126 | 127 | triples.first.predicate.should == 'http://example.com/name' 128 | triples.first.object.to_s.should == "Peter" 129 | 130 | end 131 | end 132 | 133 | end 134 | 135 | describe "#get_related_resource" do 136 | 137 | context "when eager load not called" do 138 | 139 | context "and related resource exists" do 140 | it "should return nil" do 141 | res = @john.get_related_resource(@peter.uri, Person) 142 | res.should == nil 143 | end 144 | end 145 | 146 | context "and related resource doesn't exist" do 147 | 148 | it "should return nil" do 149 | res = @john.get_related_resource(RDF::URI.new('http://example.com/nonexistent/person'), Person) 150 | res.should be_nil 151 | end 152 | end 153 | end 154 | 155 | context "when eager_load_object_triples has been called" do 156 | before do 157 | @john.eager_load_object_triples! 158 | end 159 | 160 | it "should not call find" do 161 | Person.should_not_receive(:find) 162 | @john.get_related_resource(@peter.uri, Person) 163 | end 164 | 165 | it "should get the right instance of the resource class passed in" do 166 | res = @john.get_related_resource(@peter.uri, Person) 167 | res.should == @peter 168 | end 169 | 170 | end 171 | 172 | context "when eager_load_predicate_triples has been called" do 173 | before do 174 | @john.eager_load_predicate_triples! 175 | end 176 | 177 | it "should not call find" do 178 | Person.should_not_receive(:find) 179 | @john.get_related_resource(RDF::URI.new('http://example.com/name'), Resource) 180 | end 181 | 182 | it "should get the right instance of the resource class passed in" do 183 | res = @john.get_related_resource(RDF::URI.new('http://example.com/name'), Resource) 184 | res.should == @name 185 | end 186 | 187 | it "should be possible to call methods on the returned object" do 188 | @john.get_related_resource(RDF::URI.new('http://example.com/name'), Resource).label.should == @name.label 189 | end 190 | end 191 | 192 | 193 | end 194 | 195 | describe "#has_related_resource?" do 196 | 197 | context "when eager load not called" do 198 | context "and related resource exists" do 199 | it "should return false" do 200 | @john.has_related_resource?(RDF::URI.new('http://example.com/name'), Resource).should be false 201 | end 202 | end 203 | 204 | context "and related resource doesn't exist" do 205 | it "should return false" do 206 | @john.has_related_resource?(RDF::URI.new('http://example.com/nonexistent/person'), Person).should be false 207 | end 208 | end 209 | end 210 | 211 | context "when eager load called" do 212 | before do 213 | @john.eager_load_predicate_triples! 214 | end 215 | 216 | context "and related resource exists" do 217 | it "should return true" do 218 | @john.has_related_resource?(RDF::URI.new('http://example.com/name'), Resource).should be true 219 | end 220 | end 221 | 222 | context "and related resource doesn't exist" do 223 | it "should return false" do 224 | @john.has_related_resource?(RDF::URI.new('http://example.com/nonexistent/person'), Person).should be false 225 | end 226 | end 227 | end 228 | 229 | end 230 | 231 | end -------------------------------------------------------------------------------- /spec/tripod/finders_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe Tripod::Finders do 4 | 5 | let!(:ric) do 6 | r = Person.new('http://example.com/id/ric') 7 | r.name = "ric" 8 | r.knows = RDF::URI.new("http://example.com/id/bill") 9 | r 10 | end 11 | 12 | let!(:bill) do 13 | b = Person.new('http://example.com/id/bill') 14 | b.name = "bill" 15 | b 16 | end 17 | 18 | let!(:john) do 19 | j = Person.new('http://example.com/id/john', 'http://example.com/another_graph') 20 | j.name = "john" 21 | j 22 | end 23 | 24 | 25 | describe '.find' do 26 | 27 | before do 28 | ric.save! 29 | bill.save! 30 | end 31 | 32 | context 'when record exists' do 33 | let(:person) { Person.find(ric.uri) } 34 | 35 | it 'hydrates and return an object' do 36 | person.name.should == "ric" 37 | person.knows.should == [RDF::URI('http://example.com/id/bill')] 38 | end 39 | 40 | it 'sets the graph on the instantiated object' do 41 | person.graph_uri.should_not be_nil 42 | person.graph_uri.should == RDF::URI("http://example.com/graph") 43 | end 44 | 45 | it "returns a non-new record" do 46 | person.new_record?.should be false 47 | end 48 | 49 | end 50 | 51 | context 'when record does not exist' do 52 | it 'raises not found' do 53 | lambda { Person.find('http://example.com/nonexistent') }.should raise_error(Tripod::Errors::ResourceNotFound) 54 | end 55 | end 56 | 57 | context 'with graph_uri supplied' do 58 | let!(:another_person) do 59 | p = Person.new('http://example.com/anotherperson', :graph_uri => 'http://example.com/graphx') 60 | p.name = 'a.n.other' 61 | p.save! 62 | p 63 | end 64 | 65 | context 'when there are triples about the resource in that graph' do 66 | it 'should use that graph to call new' do 67 | Person.should_receive(:new).with(another_person.uri, :graph_uri => 'http://example.com/graphx').and_call_original 68 | Person.find(another_person.uri, :graph_uri => 'http://example.com/graphx') 69 | end 70 | 71 | end 72 | 73 | context 'when there are no triples about the resource in that graph' do 74 | it 'should raise not found' do 75 | expect { 76 | Person.find(another_person.uri, :graph_uri => "http://example.com/graphy") 77 | }.to raise_error(Tripod::Errors::ResourceNotFound) 78 | end 79 | end 80 | end 81 | 82 | context 'with graph_uri supplied (deprecated)' do 83 | let!(:another_person) do 84 | p = Person.new('http://example.com/anotherperson', 'http://example.com/graphx') 85 | p.name = 'a.n.other' 86 | p.save! 87 | p 88 | end 89 | 90 | context 'when there are triples about the resource in that graph' do 91 | it 'should use that graph to call new' do 92 | Person.should_receive(:new).with(another_person.uri, :graph_uri => 'http://example.com/graphx').and_call_original 93 | Person.find(another_person.uri, 'http://example.com/graphx') 94 | end 95 | 96 | end 97 | 98 | context 'when there are no triples about the resource in that graph' do 99 | it 'should raise not found' do 100 | expect { 101 | Person.find(another_person.uri, "http://example.com/graphy") 102 | }.to raise_error(Tripod::Errors::ResourceNotFound) 103 | end 104 | end 105 | end 106 | 107 | context 'with no graph_uri supplied' do 108 | it 'should look up the graph to call new' do 109 | ric # trigger the lazy load 110 | Person.should_receive(:new).with(ric.uri, :graph_uri => Person.get_graph_uri).and_call_original 111 | Person.find(ric.uri) 112 | end 113 | end 114 | 115 | context "looking in any graph" do 116 | context 'model has no default graph URI' do 117 | let!(:resource) do 118 | r = Resource.new('http://example.com/foo', :graph_uri => 'http://example/graph/foo') 119 | r.label = 'Foo' 120 | r.save! 121 | r 122 | end 123 | 124 | it 'should find a resource regardless of which graph it is in' do 125 | Resource.find(resource.uri, :ignore_graph => true).should_not be_nil 126 | end 127 | end 128 | 129 | context 'model has a default graph URI' do 130 | let!(:another_person) do 131 | p = Person.new('http://example.com/anotherperson', :graph_uri => 'http://example.com/graphx') 132 | p.name = 'a.n.other' 133 | p.save! 134 | p 135 | end 136 | 137 | it 'should override the default graph URI and find the resource regardless' do 138 | Person.find(another_person.uri, :ignore_graph => true).should_not be_nil 139 | end 140 | 141 | it 'should return the resource without a graph URI' do 142 | Person.find(another_person.uri, :ignore_graph => true).graph_uri.should be_nil 143 | end 144 | end 145 | end 146 | end 147 | 148 | describe ".all" do 149 | it "should make and return a new criteria for the current class" do 150 | Person.all.should == Tripod::Criteria.new(Person) 151 | end 152 | end 153 | 154 | describe ".where" do 155 | 156 | let(:criteria) { Person.where("[pattern]") } 157 | 158 | it "should make and return a criteria for the current class" do 159 | criteria.class.should == Tripod::Criteria 160 | end 161 | 162 | it "should apply the where clause" do 163 | criteria.where_clauses.should include("[pattern]") 164 | end 165 | 166 | end 167 | 168 | describe "count" do 169 | before do 170 | ric.save! 171 | bill.save! 172 | end 173 | 174 | it "should just call count on the all criteria" do 175 | all_crit = Tripod::Criteria.new(Person) 176 | Person.should_receive(:all).and_return(all_crit) 177 | all_crit.should_receive(:count).and_call_original 178 | Person.count 179 | end 180 | 181 | it 'should return the count of all resources of this type' do 182 | Person.count.should == 2 183 | end 184 | end 185 | 186 | describe "first" do 187 | before do 188 | ric.save! 189 | bill.save! 190 | end 191 | 192 | it "should just call count on the all criteria" do 193 | all_crit = Tripod::Criteria.new(Person) 194 | Person.should_receive(:all).and_return(all_crit) 195 | all_crit.should_receive(:first).and_call_original 196 | Person.first 197 | end 198 | 199 | it 'should return the first resources of this type' do 200 | Person.first.class.should == Person 201 | end 202 | end 203 | 204 | describe '.find_by_sparql' do 205 | 206 | before { [bill, ric, john].each(&:save!) } 207 | 208 | context 'given a simple SELECT query, returning uri and graph' do 209 | let(:query) { 'SELECT DISTINCT ?uri ?graph WHERE { GRAPH ?graph { ?uri ?p ?o } }' } 210 | 211 | it 'returns an array of matching resources' do 212 | res = Person.find_by_sparql(query) 213 | res.should =~ [bill, ric, john] 214 | end 215 | end 216 | 217 | context 'given a simple SELECT query, returning uri only' do 218 | let(:query) { 'SELECT DISTINCT ?uri WHERE { ?uri ?p ?o }' } 219 | 220 | it 'returns an array of matching resources' do 221 | res = Person.find_by_sparql(query) 222 | res.should =~ [bill, ric, john] 223 | end 224 | end 225 | 226 | context 'given a SELECT query and named uri and graph variables' do 227 | let(:query) { 'SELECT DISTINCT ?x ?y WHERE { GRAPH ?y { ?x ?p ?o } }' } 228 | let(:opts) { {:uri_variable => 'x', :graph_variable => 'y'} } 229 | 230 | it 'returns an array of matching resources' do 231 | res = Person.find_by_sparql(query, opts) 232 | res.should =~ [bill, ric, john] 233 | end 234 | end 235 | 236 | context 'given a SELECT query containing a graph restriction' do 237 | let(:query) { 'SELECT DISTINCT ?uri ?graph WHERE { GRAPH { ?uri ?p ?o } }' } 238 | 239 | it 'only returns matching resources' do 240 | res = Person.find_by_sparql(query) 241 | res.should =~ [bill, ric] 242 | end 243 | end 244 | 245 | context 'given a SELECT query containing multiple graph restrictions' do 246 | let(:query) { 'SELECT DISTINCT ?uri ?graph WHERE { 247 | { GRAPH { ?uri ?p ?o } } 248 | UNION 249 | { GRAPH { ?uri ?p ?o } } 250 | }' } 251 | 252 | it "should return matching resources" do 253 | res = Person.find_by_sparql(query) 254 | res.should =~ [bill, ric, john] 255 | end 256 | end 257 | 258 | context 'given a SELECT query which returns no resources' do 259 | let(:query) { 'SELECT ?uri WHERE { ?uri ?p ?o . ?p ?o . }' } 260 | 261 | it 'returns an empty array' do 262 | res = Person.find_by_sparql(query) 263 | res.empty?.should == true 264 | end 265 | end 266 | end 267 | end 268 | -------------------------------------------------------------------------------- /spec/tripod/persistence_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe Tripod::Persistence do 4 | 5 | let(:unsaved_person) do 6 | @unsaved_uri = @uri = 'http://example.com/uri' 7 | @graph1 = RDF::Graph.new 8 | stmt = RDF::Statement.new 9 | stmt.subject = RDF::URI.new(@uri) 10 | stmt.predicate = RDF::URI.new('http://example.com/pred') 11 | stmt.object = RDF::URI.new('http://example.com/obj') 12 | @graph1 << stmt 13 | p = Person.new(@uri, 'http://example.com/graph') 14 | p.hydrate!(:graph => @graph1) 15 | p 16 | end 17 | 18 | let(:saved_person) do 19 | @saved_uri = @uri2 = 'http://example.com/uri2' 20 | @graph2 = RDF::Graph.new 21 | stmt = RDF::Statement.new 22 | stmt.subject = RDF::URI.new(@uri2) 23 | stmt.predicate = RDF::URI.new('http://example.com/pred2') 24 | stmt.object = RDF::URI.new('http://example.com/obj2') 25 | @graph2 << stmt 26 | p = Person.new(@uri2, 'http://example.com/graph') 27 | p.hydrate!(:graph => @graph2) 28 | p.save 29 | p 30 | end 31 | 32 | 33 | describe ".save" do 34 | 35 | context "with no graph_uri set" do 36 | it 'should raise a GraphUriNotSet error' do 37 | p = Resource.new('http://example.com/arbitrary/resource') 38 | lambda { p.save }.should raise_error(Tripod::Errors::GraphUriNotSet) 39 | end 40 | end 41 | 42 | it 'saves the contents to the db' do 43 | unsaved_person.save.should be true 44 | 45 | # try reading the data back out. 46 | p2 = Person.new(@uri) 47 | p2.hydrate! 48 | repo_statements = p2.repository.statements 49 | repo_statements.count.should == 1 50 | repo_statements.first.subject.should == RDF::URI.new(@uri) 51 | repo_statements.first.predicate.should == RDF::URI.new('http://example.com/pred') 52 | repo_statements.first.object.should == RDF::URI.new('http://example.com/obj') 53 | end 54 | 55 | 56 | it 'should leave other people untouched' do 57 | # save the unsaved person 58 | unsaved_person.save.should be true 59 | 60 | # read the saved person back out the db, and check he's untouched. 61 | p2 = Person.new(saved_person.uri) 62 | p2.hydrate! 63 | p2.repository.dump(:ntriples).should == saved_person.repository.dump(:ntriples) 64 | end 65 | 66 | context 'given triples about this resource in another graph' do 67 | let(:graph_uri) { 'http://example.com/my_other_life' } 68 | let(:father) { RDF::URI.new('http://example.com/vader') } 69 | 70 | before do 71 | p = Person.new(saved_person.uri, graph_uri: graph_uri) 72 | p.father = father 73 | p.save! 74 | end 75 | 76 | it 'should leave those triples untouched' do 77 | saved_person.name = 'Luke' 78 | saved_person.save! 79 | p = Person.find(saved_person.uri, graph_uri: graph_uri) 80 | p.father.should == father 81 | end 82 | end 83 | 84 | it 'runs the callbacks' do 85 | unsaved_person.should_receive(:pre_save) 86 | unsaved_person.save 87 | end 88 | end 89 | 90 | describe ".destroy" do 91 | 92 | it 'removes all triples from the db' do 93 | saved_person.destroy.should be true 94 | 95 | # re-load it back into memory 96 | p2 = Person.new(@saved_uri) 97 | p2.hydrate! 98 | p2.repository.should be_empty # nothing there any more! 99 | end 100 | 101 | it 'should run the callbacks' do 102 | saved_person.should_receive(:pre_destroy) 103 | saved_person.destroy 104 | end 105 | end 106 | 107 | describe ".save!" do 108 | it 'throws an exception if save fails' do 109 | unsaved_person.stub(:graph_uri).and_return(nil) # force a failure 110 | lambda {unsaved_person.save!}.should raise_error(Tripod::Errors::Validations) 111 | end 112 | end 113 | 114 | describe '.update_attribute' do 115 | let (:person) { Person.new('http://example.com/newperson') } 116 | 117 | context 'without transactions' do 118 | before { person.stub(:save) } 119 | 120 | it 'should write the attribute' do 121 | person.update_attribute(:name, 'Bob') 122 | person.name.should == 'Bob' 123 | end 124 | 125 | it 'should save the record' do 126 | person.should_receive(:save) 127 | person.update_attribute(:name, 'Bob') 128 | end 129 | end 130 | 131 | context 'with transactions' do 132 | it 'should create a new resource' do 133 | transaction = Tripod::Persistence::Transaction.new 134 | 135 | person.update_attribute(:name, 'George', transaction: transaction) 136 | 137 | lambda { Person.find(person.uri) }.should raise_error(Tripod::Errors::ResourceNotFound) 138 | transaction.commit 139 | lambda { Person.find(person.uri) }.should_not raise_error() 140 | end 141 | 142 | it 'should assign the attributes of an existing' do 143 | transaction = Tripod::Persistence::Transaction.new 144 | person.save 145 | 146 | person.update_attribute(:name, 'George', transaction: transaction) 147 | 148 | Person.find(person.uri).name.should_not == 'George' 149 | transaction.commit 150 | Person.find(person.uri).name.should == 'George' 151 | end 152 | end 153 | end 154 | 155 | describe '.update_attributes' do 156 | let (:person) { Person.new('http://example.com/newperson') } 157 | 158 | context "without transactions" do 159 | before { person.stub(:save) } 160 | 161 | it 'should assign the attributes' do 162 | person.update_attributes(:name => 'Bob') 163 | person.name.should == 'Bob' 164 | end 165 | 166 | it 'should save the record' do 167 | person.should_receive(:save) 168 | person.update_attributes(:name => 'Bob') 169 | end 170 | end 171 | 172 | context 'with transactions' do 173 | 174 | it 'should create a new resource' do 175 | transaction = Tripod::Persistence::Transaction.new 176 | attributes = { name: 'Fred' } 177 | 178 | person.update_attributes(attributes, transaction: transaction) 179 | 180 | lambda { Person.find(person.uri) }.should raise_error(Tripod::Errors::ResourceNotFound) 181 | transaction.commit 182 | lambda { Person.find(person.uri) }.should_not raise_error() 183 | end 184 | 185 | it 'should assign the attributes of an existing' do 186 | transaction = Tripod::Persistence::Transaction.new 187 | attributes = { name: 'Fred' } 188 | person.save 189 | 190 | person.update_attributes(attributes, transaction: transaction) 191 | 192 | Person.find(person.uri).name.should_not == 'Fred' 193 | transaction.commit 194 | Person.find(person.uri).name.should == 'Fred' 195 | end 196 | end 197 | end 198 | 199 | describe "transactions" do 200 | 201 | it "only saves on commit" do 202 | 203 | transaction = Tripod::Persistence::Transaction.new 204 | 205 | unsaved_person.save(transaction: transaction) 206 | saved_person.write_predicate('http://example.com/pred2', 'blah') 207 | saved_person.save(transaction: transaction) 208 | 209 | # nothing should have changed yet. 210 | lambda {Person.find(unsaved_person.uri)}.should raise_error(Tripod::Errors::ResourceNotFound) 211 | Person.find(saved_person.uri).read_predicate('http://example.com/pred2').first.to_s.should == RDF::URI.new('http://example.com/obj2').to_s 212 | 213 | transaction.commit 214 | 215 | # things should have changed now. 216 | lambda {Person.find(unsaved_person.uri)}.should_not raise_error() 217 | Person.find(saved_person.uri).read_predicate('http://example.com/pred2').first.should == 'blah' 218 | 219 | end 220 | 221 | it "silently ignore invalid saves" do 222 | transaction = Tripod::Persistence::Transaction.new 223 | 224 | unsaved_person.stub(:graph_uri).and_return(nil) # force a failure 225 | unsaved_person.save(transaction: transaction).should be false 226 | 227 | saved_person.write_predicate('http://example.com/pred2', 'blah') 228 | saved_person.save(transaction: transaction).should be true 229 | 230 | transaction.commit 231 | 232 | # transaction should be gone 233 | Tripod::Persistence.transactions[transaction.transaction_id].should be_nil 234 | 235 | # unsaved person still not there 236 | lambda {Person.find(unsaved_person.uri)}.should raise_error(Tripod::Errors::ResourceNotFound) 237 | 238 | # saved person SHOULD be updated 239 | Person.find(saved_person.uri).read_predicate('http://example.com/pred2').first.should == 'blah' 240 | end 241 | 242 | it "can be aborted" do 243 | transaction = Tripod::Persistence::Transaction.new 244 | unsaved_person.save(transaction: transaction) 245 | transaction.abort() 246 | 247 | # unsaved person still not there 248 | lambda {Person.find(unsaved_person.uri)}.should raise_error(Tripod::Errors::ResourceNotFound) 249 | 250 | #transaction gone. 251 | transaction.query.should be_blank 252 | Tripod::Persistence.transactions[transaction.transaction_id].should be_nil 253 | end 254 | 255 | it "should be removed once committed" do 256 | transaction = Tripod::Persistence::Transaction.new 257 | unsaved_person.save(transaction: transaction) 258 | transaction.commit() 259 | 260 | #transaction gone. 261 | transaction.query.should be_blank 262 | Tripod::Persistence.transactions[transaction.transaction_id].should be_nil 263 | end 264 | 265 | end 266 | 267 | end 268 | -------------------------------------------------------------------------------- /spec/tripod/criteria_execution_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe Tripod::Criteria do 4 | 5 | let(:person_criteria) do 6 | c = Person.all #Tripod::Criteria.new(Person) 7 | end 8 | 9 | let(:resource_criteria) do 10 | c = Resource.all #Tripod::Criteria.new(Resource) 11 | end 12 | 13 | let!(:john) do 14 | p = Person.new('http://example.com/id/john') 15 | p.name = "John" 16 | p.save! 17 | p 18 | end 19 | 20 | let!(:barry) do 21 | p = Person.new('http://example.com/id/barry') 22 | p.name = "Barry" 23 | p.save! 24 | p 25 | end 26 | 27 | describe "#as_query" do 28 | 29 | context "when graph_lambdas exist" do 30 | it "should return the contents of the block inside a graph statement with unbound ?g parameter" do 31 | resource_criteria.graph(nil) do 32 | "?uri ?p ?o" 33 | end 34 | resource_criteria.as_query.should == "SELECT DISTINCT ?uri WHERE { GRAPH ?g { ?uri ?p ?o } ?uri ?p ?o }" 35 | end 36 | 37 | it "should be possible to bind to the ?g paramter on the criteria after supplying a block" do 38 | resource_criteria.graph(nil) do 39 | "?s ?p ?o" 40 | end.where("?uri ?p ?g") 41 | resource_criteria.as_query.should == "SELECT DISTINCT ?uri WHERE { GRAPH ?g { ?s ?p ?o } ?uri ?p ?g }" 42 | end 43 | end 44 | 45 | context "for a class with an rdf_type and graph" do 46 | it "should return a SELECT query based with an rdf type restriction" do 47 | person_criteria.as_query.should == "SELECT DISTINCT ?uri ( as ?graph) WHERE { GRAPH { ?uri a } }" 48 | end 49 | 50 | context "with include_graph option set to false" do 51 | it "should not select graphs, but restrict to graph" do 52 | person_criteria.as_query(:return_graph => false).should == "SELECT DISTINCT ?uri WHERE { GRAPH { ?uri a } }" 53 | end 54 | end 55 | 56 | context "and extra restrictions" do 57 | before { person_criteria.where("[pattern]") } 58 | 59 | it "should return a SELECT query with the extra restriction" do 60 | person_criteria.as_query.should == "SELECT DISTINCT ?uri ( as ?graph) WHERE { GRAPH { ?uri a . [pattern] } }" 61 | end 62 | end 63 | 64 | context "with an overriden graph" do 65 | before { person_criteria.graph("http://example.com/anothergraph") } 66 | 67 | it "should override the graph in the query" do 68 | person_criteria.as_query.should == "SELECT DISTINCT ?uri ( as ?graph) WHERE { GRAPH { ?uri a } }" 69 | end 70 | end 71 | end 72 | 73 | context "for a class without an rdf_type and graph" do 74 | it "should return a SELECT query without an rdf_type restriction" do 75 | resource_criteria.as_query.should == "SELECT DISTINCT ?uri ?graph WHERE { GRAPH ?graph { ?uri ?p ?o } }" 76 | end 77 | 78 | context "with include_graph option set to false" do 79 | it "should not select graphs or restrict to graph" do 80 | resource_criteria.as_query(:return_graph => false).should == "SELECT DISTINCT ?uri WHERE { ?uri ?p ?o }" 81 | end 82 | end 83 | 84 | context "and extra restrictions" do 85 | before { resource_criteria.where("?uri a ") } 86 | 87 | it "should return a SELECT query with the extra restrictions" do 88 | resource_criteria.as_query.should == "SELECT DISTINCT ?uri ?graph WHERE { GRAPH ?graph { ?uri a } }" 89 | end 90 | end 91 | 92 | context "with a graph set" do 93 | before { resource_criteria.graph("http://example.com/graphy") } 94 | 95 | it "should override the graph in the query" do 96 | resource_criteria.as_query.should == "SELECT DISTINCT ?uri ( as ?graph) WHERE { GRAPH { ?uri ?p ?o } }" 97 | end 98 | end 99 | end 100 | 101 | context "with extras" do 102 | 103 | before { resource_criteria.where("?uri a ").extras("LIMIT 10").extras("OFFSET 20") } 104 | 105 | it "should add the extras on the end" do 106 | resource_criteria.as_query.should == "SELECT DISTINCT ?uri ?graph WHERE { GRAPH ?graph { ?uri a } } LIMIT 10 OFFSET 20" 107 | end 108 | end 109 | end 110 | 111 | describe "#resources" do 112 | 113 | context "with options passed" do 114 | it "should pass the options to as_query" do 115 | person_criteria.should_receive(:as_query).with(:return_graph => false).and_call_original 116 | person_criteria.resources(:return_graph => false) 117 | end 118 | end 119 | 120 | context "with no extra restrictions" do 121 | it "should return a set of hydrated objects for the type" do 122 | person_criteria.resources.to_a.should == [john, barry] 123 | end 124 | end 125 | 126 | context "with extra restrictions" do 127 | before { person_criteria.where("?uri 'John'") } 128 | 129 | it "should return a set of hydrated objects for the type and restrictions" do 130 | person_criteria.resources.to_a.should == [john] 131 | end 132 | end 133 | 134 | context "with return_graph option set to false" do 135 | 136 | context "where the class has a graph_uri set" do 137 | it "should set the graph_uri on the hydrated objects" do 138 | person_criteria.resources(:return_graph => false).first.graph_uri.should_not be_nil 139 | end 140 | end 141 | 142 | context "where the class does not have a graph_uri set" do 143 | it "should not set the graph_uri on the hydrated objects" do 144 | resource_criteria.resources(:return_graph => false).first.graph_uri.should be_nil 145 | end 146 | end 147 | 148 | end 149 | 150 | end 151 | 152 | describe "#first" do 153 | 154 | context "with options passed" do 155 | it "should pass the options to as_query" do 156 | person_criteria.should_receive(:as_query).with(:return_graph => false).and_call_original 157 | person_criteria.first(:return_graph => false) 158 | end 159 | end 160 | 161 | it "should return the first resource for the criteria" do 162 | person_criteria.first.should == john 163 | end 164 | 165 | it "should call Query.select with the 'first sparql'" do 166 | sparql = Tripod::SparqlQuery.new(person_criteria.as_query).as_first_query_str 167 | Tripod::SparqlClient::Query.should_receive(:select).with(sparql).and_call_original 168 | person_criteria.first 169 | end 170 | 171 | context "with return_graph option set to false" do 172 | 173 | context "where the class has a graph_uri set" do 174 | it "should set the graph_uri on the hydrated object" do 175 | person_criteria.first(:return_graph => false).graph_uri.should_not be_nil 176 | end 177 | end 178 | 179 | context "where the class does not have a graph_uri set" do 180 | it "should not set the graph_uri on the hydrated object" do 181 | resource_criteria.first(:return_graph => false).graph_uri.should be_nil 182 | end 183 | end 184 | 185 | end 186 | end 187 | 188 | describe "#count" do 189 | 190 | context "with options passed" do 191 | it "should pass the options to as_querys" do 192 | person_criteria.should_receive(:as_query).with(:return_graph => false).and_call_original 193 | person_criteria.count(:return_graph => false) 194 | end 195 | end 196 | 197 | it "should return a set of hydrated objects for the criteria" do 198 | person_criteria.count.should == 2 199 | person_criteria.where("?uri 'John'").count.should ==1 200 | end 201 | 202 | it "should call Query.select with the 'count sparql'" do 203 | sparql = Tripod::SparqlQuery.new(person_criteria.as_query).as_count_query_str 204 | Tripod::SparqlClient::Query.should_receive(:select).with(sparql).and_call_original 205 | person_criteria.count 206 | end 207 | 208 | it "should execute the right Sparql" do 209 | sparql = "SELECT (COUNT(*) as ?tripod_count_var) { 210 | SELECT DISTINCT ?uri ( as ?graph) WHERE { GRAPH { ?uri a } } LIMIT 10 OFFSET 20 211 | }" 212 | Tripod::SparqlClient::Query.should_receive(:select).with(sparql).and_call_original 213 | Person.all.limit(10).offset(20).count 214 | end 215 | 216 | end 217 | 218 | describe "exeuting a chained criteria" do 219 | 220 | let(:chained_criteria) { Person.where("?uri ?name").limit(1).offset(0).order("DESC(?name)") } 221 | 222 | it "should run the right Sparql" do 223 | sparql = "SELECT DISTINCT ?uri ( as ?graph) WHERE { GRAPH { ?uri a . ?uri ?name } } ORDER BY DESC(?name) LIMIT 1 OFFSET 0" 224 | Tripod::SparqlClient::Query.should_receive(:select).with(sparql).and_call_original 225 | chained_criteria.resources 226 | end 227 | 228 | it "should return the right resources" do 229 | chained_criteria.resources.to_a.should == [john] 230 | end 231 | 232 | it "should return the right number of resources" do 233 | chained_criteria.count.should == 1 234 | end 235 | end 236 | 237 | end -------------------------------------------------------------------------------- /lib/tripod/finders.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | # This module defines behaviour for finders. 4 | module Tripod::Finders 5 | extend ActiveSupport::Concern 6 | 7 | module ClassMethods 8 | 9 | # Find a +Resource+ by its uri (and, optionally, by its graph if there are more than one). 10 | # 11 | # @example Find a single resource by a uri. 12 | # Person.find('http://ric') 13 | # Person.find(RDF::URI('http://ric')) 14 | # @example Find a single resource by uri and graph 15 | # Person.find('http://ric', :graph_uri => 'http://example.com/people') 16 | # @example Find a single resource by uri, looking in any graph (i.e. the UNION graph) 17 | # Person.find('http://ric', :ignore_graph => true) 18 | # @example Find a single resource by uri and graph (DEPRECATED) 19 | # Person.find('http://ric', 'http://example.com/people') 20 | # Person.find(RDF::URI('http://ric'), Person.find(RDF::URI('http://example.com/people'))) 21 | # 22 | # @param [ String, RDF::URI ] uri The uri of the resource to find 23 | # @param [ Hash, String, RDF::URI ] opts Either an options hash (see above), or (for backwards compatibility) the uri of the graph from which to get the resource 24 | # 25 | # @raise [ Tripod::Errors::ResourceNotFound ] If no resource found. 26 | # 27 | # @return [ Resource ] A single resource 28 | def find(uri, opts={}) 29 | if opts.is_a?(String) # backward compatibility hack 30 | graph_uri = opts 31 | ignore_graph = false 32 | else 33 | graph_uri = opts.fetch(:graph_uri, nil) 34 | ignore_graph = opts.fetch(:ignore_graph, false) 35 | end 36 | 37 | resource = nil 38 | if ignore_graph 39 | resource = self.new(uri, :ignore_graph => true) 40 | else 41 | graph_uri ||= self.get_graph_uri 42 | unless graph_uri 43 | # do a quick select to see what graph to use. 44 | select_query = "SELECT * WHERE { GRAPH ?g {<#{uri.to_s}> ?p ?o } } LIMIT 1" 45 | result = Tripod::SparqlClient::Query.select(select_query) 46 | if result.length > 0 47 | graph_uri = result[0]["g"]["value"] 48 | else 49 | raise Tripod::Errors::ResourceNotFound.new(uri) 50 | end 51 | end 52 | resource = self.new(uri, :graph_uri => graph_uri.to_s) 53 | end 54 | 55 | resource.hydrate! 56 | resource.new_record = false 57 | 58 | # check that there are triples for the resource (catches case when someone has deleted data 59 | # between our original check for the graph and hydrating the object. 60 | raise Tripod::Errors::ResourceNotFound.new(uri) if resource.repository.empty? 61 | 62 | # return the instantiated, hydrated resource 63 | resource 64 | end 65 | 66 | # Find a collection of +Resource+s by a SPARQL select statement which returns their uris. 67 | # Under the hood, this only executes two queries: a select, then a describe. 68 | # 69 | # @example 70 | # Person.find_by_sparql('SELECT ?uri ?graph WHERE { GRAPH ?graph { ?uri ?p ?o } } LIMIT 3') 71 | # 72 | # @param [ String ] sparql_query. A sparql query which returns a list of uris of the objects. 73 | # @param [ Hash ] opts. A hash of options. 74 | # 75 | # @option options [ String ] uri_variable The name of the uri variable in thh query, if not 'uri' 76 | # @option options [ String ] graph_variable The name of the uri variable in thh query, if not 'graph' 77 | # 78 | # @return [ Array ] An array of hydrated resources of this class's type. 79 | def find_by_sparql(sparql_query, opts={}) 80 | _create_and_hydrate_resources_from_sparql(sparql_query, opts) 81 | end 82 | 83 | # execute a where clause on this resource. 84 | # returns a criteria object 85 | def where(sparql_snippet) 86 | criteria = Tripod::Criteria.new(self) 87 | criteria.where(sparql_snippet) 88 | end 89 | 90 | # execute a query to return all objects (restricted by this class's rdf_type if specified) 91 | # returns a criteria object 92 | def all 93 | Tripod::Criteria.new(self) 94 | end 95 | 96 | def count 97 | self.all.count 98 | end 99 | 100 | def first 101 | self.all.first 102 | end 103 | 104 | # returns a graph of triples which describe the uris passed in. 105 | def describe_uris(uris) 106 | graph = RDF::Graph.new 107 | 108 | if uris.length > 0 109 | uris_sparql_str = uris.map{ |u| "<#{u.to_s}>" }.join(" ") 110 | 111 | # Do a big describe statement, and read the results into an in-memory repo 112 | ntriples_string = Tripod::SparqlClient::Query.query("CONSTRUCT { ?s ?p ?o } WHERE { VALUES ?s { #{uris_sparql_str} }. ?s ?p ?o . }", Tripod.ntriples_header_str) 113 | graph = _rdf_graph_from_ntriples_string(ntriples_string, graph) 114 | end 115 | 116 | graph 117 | end 118 | 119 | # returns a graph of triples which describe results of the sparql passed in. 120 | # 121 | # @option options [ String ] uri_variable The name of the uri variable in the query, if not 'uri' 122 | def describe_select_results(select_sparql, opts={}) 123 | ntriples_string = _raw_describe_select_results(select_sparql, opts) # this defaults to using n-triples 124 | _rdf_graph_from_ntriples_string(ntriples_string) 125 | end 126 | 127 | # PRIVATE utility methods (not intended to be used externally) 128 | ########################## 129 | 130 | # given a sparql select query, create and hydrate some resources 131 | # 132 | # @option options [ String ] uri_variable The name of the uri variable in the query, if not 'uri' 133 | # @option options [ String ] graph_variable The name of the uri variable in thh query, if not 'graph' 134 | def _resources_from_sparql(select_sparql, opts={}) 135 | _create_and_hydrate_resources_from_sparql(select_sparql, opts) 136 | end 137 | 138 | # given a string of ntriples data, populate an RDF graph. 139 | # If you pass a graph in, it will add to that one. 140 | def _rdf_graph_from_ntriples_string(ntriples_string, graph=nil) 141 | graph ||= RDF::Graph.new 142 | RDF::Reader.for(:ntriples).new(ntriples_string) do |reader| 143 | reader.each_statement do |statement| 144 | graph << statement 145 | end 146 | end 147 | graph 148 | end 149 | 150 | # given a construct or describe query, return a graph of triples. 151 | def _graph_of_triples_from_construct_or_describe(construct_query) 152 | ntriples_str = Tripod::SparqlClient::Query.query(construct_query, Tripod.ntriples_header_str) 153 | _rdf_graph_from_ntriples_string(ntriples_str, graph=nil) 154 | end 155 | 156 | # Given a select query, perform a DESCRIBE query to get a graph of data from which we 157 | # create and hydrate a collection of resources. 158 | # 159 | # @option options [ String ] uri_variable The name of the uri variable in the query, if not 'uri' 160 | # @option options [ String ] graph_variable The name of the uri variable in the query, if not 'graph' 161 | def _create_and_hydrate_resources_from_sparql(select_sparql, opts={}) 162 | # TODO: Optimization?: if return_graph option is false, then don't do this next line? 163 | uris_and_graphs = _select_uris_and_graphs(select_sparql, :uri_variable => opts[:uri_variable], :graph_variable => opts[:graph_variable]) 164 | 165 | #there are no resources if there are no uris and graphs 166 | if uris_and_graphs.empty? 167 | [] 168 | else 169 | construct_query = _construct_query_for_uris_and_graphs(uris_and_graphs) 170 | graph = _graph_of_triples_from_construct_or_describe(construct_query) 171 | _resources_from_graph(graph, uris_and_graphs) 172 | end 173 | end 174 | 175 | # For a select query, generate a query which DESCRIBES all the results 176 | # 177 | # @option options [ String ] uri_variable The name of the uri variable in the query, if not 'uri' 178 | def _describe_query_for_select(select_sparql, opts={}) 179 | uri_variable = opts[:uri_variable] || "uri" 180 | " 181 | CONSTRUCT { 182 | ?tripod_construct_s ?tripod_construct_p ?tripod_construct_o . 183 | #{ all_triples_construct('?tripod_construct_s') } 184 | } 185 | WHERE { 186 | { SELECT (?#{uri_variable} as ?tripod_construct_s) { 187 | #{select_sparql} 188 | } } 189 | ?tripod_construct_s ?tripod_construct_p ?tripod_construct_o . 190 | #{ all_triples_where('?tripod_construct_s') } 191 | } 192 | " 193 | end 194 | 195 | # Generate a CONSTRUCT query for the given uri and graph pairs. 196 | def _construct_query_for_uris_and_graphs(uris_and_graphs) 197 | value_pairs = uris_and_graphs.map do |(uri, graph)| 198 | u = RDF::URI.new(uri).to_base 199 | g = graph ? RDF::URI.new(graph).to_base : 'UNDEF' 200 | "(#{u} #{g})" 201 | end 202 | query = "CONSTRUCT { ?uri ?p ?o . #{ self.all_triples_construct("?uri") }} WHERE { GRAPH ?g { ?uri ?p ?o . #{ self.all_triples_where("?uri") } VALUES (?uri ?g) { #{ value_pairs.join(' ') } } } }" 203 | end 204 | 205 | # For a select query, get a raw serialisation of the DESCRIPTION of the resources from the database. 206 | # 207 | # @option options [ String ] uri_variable The name of the uri variable in the query, if not 'uri' 208 | # @option options [ String ] accept_header The http accept header (default application/n-triples) 209 | def _raw_describe_select_results(select_sparql, opts={}) 210 | accept_header = opts[:accept_header] || Tripod.ntriples_header_str 211 | query = _describe_query_for_select(select_sparql, :uri_variable => opts[:uri_variable]) 212 | Tripod::SparqlClient::Query.query(query, accept_header) 213 | end 214 | 215 | # given a graph of data, and a hash of uris=>graphs, create and hydrate some resources. 216 | # Note: if any of the graphs are not set in the hash, 217 | # those resources can still be constructed, but not persisted back to DB. 218 | def _resources_from_graph(graph, uris_and_graphs) 219 | repo = add_data_to_repository(graph) 220 | resources = [] 221 | 222 | # TODO: ? if uris_and_graphs not passed in, we could get the 223 | # uris from the graph, and just not create the resoruces with a graph 224 | # (but they won't be persistable). 225 | 226 | uris_and_graphs.each do |(u,g)| 227 | 228 | # instantiate a new resource 229 | g ||= {} 230 | r = self.new(u, g) 231 | 232 | # make a graph of data for this resource's uri 233 | data_graph = RDF::Graph.new 234 | repo.query( [RDF::URI.new(u), :predicate, :object] ) do |statement| 235 | data_graph << statement 236 | 237 | if statement.object.is_a? RDF::Node 238 | repo.query( [statement.object, :predicate, :object] ) {|s| data_graph << s} 239 | end 240 | end 241 | 242 | # use it to hydrate this resource 243 | r.hydrate!(:graph => data_graph) 244 | r.new_record = false 245 | resources << r 246 | end 247 | 248 | resources 249 | end 250 | 251 | # based on the query passed in, build an array of [uri, graph] pairs 252 | # @param [ String] sparql. The sparql query 253 | # @param [ Hash ] opts. A hash of options. 254 | # 255 | # @option options [ String ] uri_variable The name of the uri variable in the query, if not 'uri' 256 | # @option options [ String ] graph_variable The name of the uri variable in thh query, if not 'graph' 257 | def _select_uris_and_graphs(sparql, opts={}) 258 | select_results = Tripod::SparqlClient::Query.select(sparql) 259 | 260 | uri_variable = opts[:uri_variable] || 'uri' 261 | graph_variable = opts[:graph_variable] || 'graph' 262 | 263 | return [] unless select_results.select{|r| r.keys.length > 0 }.any? 264 | 265 | select_results.reduce([]) do |memo, result| 266 | u = result[uri_variable]['value'] 267 | g = result[graph_variable]['value'] if result[graph_variable] 268 | memo << [u, g] 269 | memo 270 | end 271 | end 272 | 273 | end 274 | end 275 | --------------------------------------------------------------------------------