├── .github └── workflows │ ├── ci.yml │ └── generate-docs.yml ├── .gitignore ├── .yardopts ├── AUTHORS ├── CHANGELOG.md ├── CONTRIBUTING.md ├── Gemfile ├── Guardfile ├── README.md ├── Rakefile ├── UNLICENSE ├── VERSION ├── lib ├── rdf │ └── ext │ │ └── uri.rb ├── spira.rb └── spira │ ├── association_reflection.rb │ ├── base.rb │ ├── exceptions.rb │ ├── persistence.rb │ ├── reflections.rb │ ├── resource.rb │ ├── serialization.rb │ ├── type.rb │ ├── types.rb │ ├── types │ ├── any.rb │ ├── anyURI.rb │ ├── boolean.rb │ ├── date.rb │ ├── dateTime.rb │ ├── decimal.rb │ ├── double.rb │ ├── float.rb │ ├── gYear.rb │ ├── int.rb │ ├── integer.rb │ ├── long.rb │ ├── native.rb │ ├── negativeInteger.rb │ ├── nonNegativeInteger.rb │ ├── nonPositiveInteger.rb │ ├── positiveInteger.rb │ ├── string.rb │ ├── time.rb │ └── uri.rb │ ├── utils.rb │ ├── validations.rb │ ├── validations │ └── uniqueness.rb │ └── version.rb ├── spec ├── attributes_spec.rb ├── base_uri_spec.rb ├── basic_spec.rb ├── dirty_spec.rb ├── enumerable_spec.rb ├── fixtures │ ├── bob.nt │ ├── has_many.nt │ ├── localized.nt │ ├── non_model_data.nt │ ├── relations.nt │ └── types.nt ├── has_many_spec.rb ├── hooks_spec.rb ├── inheritance_spec.rb ├── instantiation_spec.rb ├── localized_attributes_spec.rb ├── nodes_spec.rb ├── non_model_data_spec.rb ├── property_types_spec.rb ├── psych_spec.rb ├── querying_spec.rb ├── rdf_types_spec.rb ├── relations_spec.rb ├── repository_spec.rb ├── serialization_spec.rb ├── spec_helper.rb ├── type_classes_spec.rb ├── types │ ├── anyURI_spec.rb │ ├── any_spec.rb │ ├── boolean_spec.rb │ ├── dateTime_spec.rb │ ├── date_spec.rb │ ├── decimal_spec.rb │ ├── double_spec.rb │ ├── float_spec.rb │ ├── gYear_spec.rb │ ├── int_spec.rb │ ├── integer_spec.rb │ ├── long_spec.rb │ ├── negativeInteger_spec.rb │ ├── nonNegativeInteger_spec.rb │ ├── nonPositiveInteger_spec.rb │ ├── positiveInteger_spec.rb │ ├── string_spec.rb │ ├── time_spec.rb │ └── uri_spec.rb ├── update_spec.rb ├── utils_spec.rb ├── validations_spec.rb └── vocabulary_spec.rb └── spira.gemspec /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | # This workflow runs continuous CI across different versions of ruby on all branches and pull requests to develop. 2 | 3 | name: CI 4 | on: 5 | push: 6 | branches: [ '**' ] 7 | pull_request: 8 | branches: [ develop ] 9 | workflow_dispatch: 10 | 11 | jobs: 12 | tests: 13 | name: Ruby ${{ matrix.ruby }} 14 | if: "contains(github.event.commits[0].message, '[ci skip]') == false" 15 | runs-on: ubuntu-latest 16 | env: 17 | CI: true 18 | ALLOW_FAILURES: ${{ endsWith(matrix.ruby, 'head') }} 19 | strategy: 20 | fail-fast: false 21 | matrix: 22 | ruby: ['3.0', 3.1, 3.2, 3.3, ruby-head, jruby] 23 | steps: 24 | - name: Clone repository 25 | uses: actions/checkout@v3 26 | - name: Set up Ruby 27 | uses: ruby/setup-ruby@v1 28 | with: 29 | ruby-version: ${{ matrix.ruby }} 30 | - name: Install dependencies 31 | run: bundle install --jobs 4 --retry 3 32 | - name: Run tests 33 | run: ruby --version; bundle exec rspec spec || $ALLOW_FAILURES 34 | - name: Coveralls GitHub Action 35 | uses: coverallsapp/github-action@v2 36 | if: "matrix.ruby == '3.3'" 37 | with: 38 | github-token: ${{ secrets.GITHUB_TOKEN }} 39 | -------------------------------------------------------------------------------- /.github/workflows/generate-docs.yml: -------------------------------------------------------------------------------- 1 | name: Build & deploy documentation 2 | on: 3 | push: 4 | branches: 5 | - master 6 | workflow_dispatch: 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | name: Update gh-pages with docs 11 | steps: 12 | - name: Clone repository 13 | uses: actions/checkout@v3 14 | - name: Set up Ruby 15 | uses: ruby/setup-ruby@v1 16 | with: 17 | ruby-version: "3.1" 18 | - name: Install required gem dependencies 19 | run: gem install yard --no-document 20 | - name: Build YARD Ruby Documentation 21 | run: yardoc 22 | - name: Deploy 23 | uses: peaceiris/actions-gh-pages@v3 24 | with: 25 | github_token: ${{ secrets.GITHUB_TOKEN }} 26 | publish_dir: ./doc/yard 27 | publish_branch: gh-pages 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | pkg 3 | coverage 4 | .yardoc 5 | doc 6 | TAGS 7 | tags 8 | gems.tags 9 | *~ 10 | .*~ 11 | .bundle 12 | Gemfile.lock 13 | bin/ 14 | /.bundle/ 15 | /releases 16 | /.byebug_history 17 | -------------------------------------------------------------------------------- /.yardopts: -------------------------------------------------------------------------------- 1 | --title "Spira: Breathing life into linked data" 2 | --output-dir doc/yard 3 | --protected 4 | --no-private 5 | --hide-void-return 6 | --markup markdown 7 | --readme README.md 8 | - 9 | AUTHORS 10 | UNLICENSE 11 | VERSION 12 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | * Ben Lavender 2 | * Arto Bendiken 3 | * Nicholas J Humfrey 4 | * Slava Kravchenko 5 | * Aymeric Brisse 6 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog for Spira 2 | 3 | ## [Unreleased] 4 | ### Changed 5 | 6 | * Spira#using_repository now returns the given repository 7 | 8 | ## 0.3.0 9 | * General updates to bring up to date. 10 | 11 | ## 0.0.12 12 | * Implemented #validate, #validate! (refactored from #save!) 13 | * Force to_a on query results when constructing to force the promise-like 14 | semantics of SPARQL::Client 15 | 16 | ## 0.0.11 17 | * Bumped the version dependency on rdf-isomorphic to 0.3.0 18 | * Added support for before_create, after_create, before_save, after_save, 19 | after_update, before_destroy and after_destroy hooks. 20 | * Switch RDF.rb dependency to >= instead of ~> 21 | 22 | ## 0.0.10 23 | * Use RDF::URI.intern on URIs generated via base URIs 24 | * Added a Spira::Types::Native, which will return the RDF::Value for a given 25 | predicate directly without any serialization or dserialization. 26 | 27 | ## 0.0.9 28 | * Fix a bug with Spira::Types::Any that prevented blank node objects 29 | * Added Spira::Resource#copy, #copy!, #copy_resource!, and #rename! 30 | * Fix a bug with fragment RDF::URI arguments that prevented correct URI 31 | construction 32 | * Added Spira::Resource.project(subject, attributes, &block), which creates a 33 | new instance without attempting to perform any base_uri logic on the given 34 | subject. This provides a supported API entry point for implementors to 35 | create their own domain-specific URI construction. 36 | * Updating a value to nil will now remove it from the repository on #save! 37 | * Tweaks to dirty tracking to correctly catch both changed and updated values. 38 | All tests pass for the first time with this change. 39 | * Change gemspec name to work with bundler 40 | 41 | ## 0.0.8 42 | * Remove type checking for repository addition. More power in return for 43 | slightly more difficult error messages. 44 | * Repositories added via klass, \*arg are now only instantiated on first use 45 | instead of immediately. 46 | * RDF::URI#as, RDF::Node#as, Resource.for, and Resource#new can now all accept 47 | a block, which yields the new instance and saves it after the block. 48 | * Clarify error message when default repository is not setup 49 | * Added a weak-reference identity map for each instance. Any circular references in 50 | relations will now return the original object instead of querying for a new 51 | one. 52 | * Use a weak-reference identity map when iterating by class. 53 | * When serializing/unserializing, duck typing (:serialize, :unserialize) is now 54 | permitted. 55 | 56 | ## 0.0.7 57 | * Added Resource.\[\], an alias for Resource.for 58 | * Resource.each now correctly raises an exception when a repository isn't found 59 | 60 | ## 0.0.6 61 | * Added #exists?, which returns a boolean if an instance exists in 62 | the backing store. 63 | * Added #data, which returns an enumerator of all RDF data for a subject, not 64 | just model data. 65 | * #save! and #update! now return self for chaining 66 | * Implemented #update and #update!, which allow setting multiple properties 67 | at once 68 | * Existing values not matching a model's defined type will now be deleted on 69 | #save! 70 | * Saving resources will now only update dirty fields 71 | * Saving resources now removes all existing triples for a given predicate 72 | if the field was updated instead of only removing one. 73 | * Implemented and documented #destroy!, #destroy!(:subject), 74 | #destroy!(:object), and #destroy!(:completely). Removed #destroy_resource! 75 | * has_many collections are now Sets and not Arrays, more accurately reflecting 76 | RDF semantics. 77 | * The Any (default) property type will now work fine with URIs 78 | * Added ResourceDeclarationError to replace various errors that occur during 79 | invalid class declarations via the DSL. 80 | * Raise an error if a non-URI predicate is given in the DSL 81 | * Small updates for RDF.rb 0.2.0 82 | * Implemented dirty field tracking. Resource#dirty?(:name) will now report if 83 | a field has not been saved. 84 | 85 | ## 0.0.5 86 | * Relations can now find related classes in modules, either by absolute 87 | reference, or by class name if they are in the same namespace. 88 | * Fix a bug with default_vocabulary in which a '/' was appended to 89 | vocabularies ending in '#' 90 | * Fix a bug with the Decimal type where round-tripping was incorrect 91 | * Fix some error messages that were missing closing parentheses 92 | 93 | ## 0.0.4 94 | * Added a Decimal type 95 | * Small updates for RDF.rb 0.2.0 compatibility 96 | * Add a Spira::Base class that can be inherited from for users who prefer to 97 | inherit rather than include. 98 | * Resource#new returns to the public API as a way to create a resource with a 99 | new blank node subject. 100 | 101 | ## 0.0.3 102 | * Bumped promise dependency to 0.1.1 to fix a Ruby 1.9 warning 103 | * Rework error handling when a repository is not configured; this should 104 | always now raise a Spira::NoRepositoryError regardless of what operation 105 | was attempted, and the error message was improved as well. 106 | * A '/' is no longer appended to base URIs ending with a '#' 107 | * Resources can now take a BNode as a subject. Implemented #node?, #uri, 108 | #to_uri, #to_node, and #to_subject in support of this; see the yardocs for 109 | exact semantics. RDF::Node is monkey patched with #as, just like RDF::URI, 110 | for instantiation. Old code should not break, but if you want to add 111 | BNodes, you may be using #uri where you want to now be using #subject. 112 | 113 | ## 0.0.2 114 | * Implemented #each on resource classes, allowing classes with a defined RDF 115 | type to be enumerated 116 | * Fragment URIs are now used as strings, allowing i.e. Integers to be used as 117 | the final portion of a URI for classes with a base_uri defined. 118 | * Added an RDF::URI property type 119 | * Implemented #to_rdf and #to_uri for increased compatibility with the RDF.rb 120 | ecosystem 121 | 122 | ## 0.0.1 123 | * Initial release 124 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to contribute 2 | 3 | Community contributions are essential for keeping Ruby RDF great. We want to keep it as easy as possible to contribute changes that get things working in your environment. There are a few guidelines that we need contributors to follow so that we can have a chance of keeping on top of things. 4 | 5 | ## Development 6 | 7 | This repository uses [Git Flow](https://github.com/nvie/gitflow) to manage development and release activity. All submissions _must_ be on a feature branch based on the _develop_ branch to ease staging and integration. 8 | 9 | * create or respond to an issue on the [Github Repository](https://github.com/ruby-rdf/spira/issues) 10 | * Fork and clone the repo: 11 | `git clone git@github.com:your-username/spira.git` 12 | * Install bundle: 13 | `bundle install` 14 | * Create tests in RSpec and make sure you achieve at least 90% code coverage for the feature your adding or behavior being modified. 15 | * Push to your fork and [submit a pull request][pr]. 16 | 17 | ## Do's and Dont's 18 | * Do your best to adhere to the existing coding conventions and idioms. 19 | * Don't use hard tabs, and don't leave trailing whitespace on any line. 20 | Before committing, run `git diff --check` to make sure of this. 21 | * Do document every method you add using [YARD][] annotations. Read the 22 | [tutorial][YARD-GS] or just look at the existing code for examples. 23 | * Don't touch the `.gemspec` or `VERSION` files. If you need to change them, 24 | do so on your private branch only. 25 | * Do feel free to add yourself to the `CREDITS` file and the 26 | corresponding list in the the `README`. Alphabetical order applies. 27 | * Don't touch the `AUTHORS` file. If your contributions are significant 28 | enough, be assured we will eventually add you in there. 29 | * Do note that in order for us to merge any non-trivial changes (as a rule 30 | of thumb, additions larger than about 15 lines of code), we need an 31 | explicit [public domain dedication][PDD] on record from you, 32 | which you will be asked to agree to on the first commit to a repo within the organization. 33 | Note that the agreement applies to all repos in the [Ruby RDF](https://github.com/ruby-rdf/) organization. 34 | 35 | [YARD]: https://yardoc.org/ 36 | [YARD-GS]: https://rubydoc.info/docs/yard/file/docs/GettingStarted.md 37 | [PDD]: https://unlicense.org/#unlicensing-contributions 38 | [pr]: https://github.com/ruby-rdf/rdf/compare/ 39 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | gemspec 3 | 4 | gem 'rdf', github: "ruby-rdf/rdf", branch: "develop" 5 | 6 | group :development, :test do 7 | gem 'ebnf', github: "dryruby/ebnf", branch: "develop" 8 | gem 'json-ld', github: "ruby-rdf/json-ld", branch: "develop" 9 | gem 'rdf-isomorphic', github: "ruby-rdf/rdf-isomorphic", branch: "develop" 10 | gem 'rdf-spec', github: "ruby-rdf/rdf-spec", branch: "develop" 11 | gem 'rdf-turtle', github: "ruby-rdf/rdf-turtle", branch: "develop" 12 | gem 'rdf-vocab', github: "ruby-rdf/rdf-vocab", branch: "develop" 13 | gem 'sxp', github: "dryruby/sxp.rb", branch: "develop" 14 | gem 'rake', '~> 13.0' 15 | gem 'redcarpet', '~> 3.2.2' unless RUBY_ENGINE == 'jruby' 16 | gem 'byebug', platform: :mri 17 | gem 'psych', platforms: [:mri, :rbx] 18 | end 19 | 20 | group :test do 21 | gem 'simplecov', '~> 0.22', platforms: :mri 22 | gem 'simplecov-lcov', '~> 0.8', platforms: :mri 23 | gem 'guard' #, '~> 2.13.0' 24 | gem 'guard-rspec' #, '~> 3.1.0' 25 | gem 'guard-ctags-bundler' #, '~> 1.4.0' 26 | end 27 | -------------------------------------------------------------------------------- /Guardfile: -------------------------------------------------------------------------------- 1 | guard "ctags-bundler", emacs: true do 2 | watch(/^(lib|spec\/support)\/.*\.rb$/) 3 | watch("Gemfile.lock") 4 | end 5 | 6 | guard "rspec" do 7 | watch(/^lib\/.*\.rb$/) { "spec" } 8 | watch(/^spec\/.*_spec\.rb$/) 9 | end 10 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'rspec' 3 | require 'rspec/core/rake_task' 4 | require 'bundler/gem_tasks' 5 | require 'yard' 6 | 7 | namespace :gem do 8 | desc "Build the spira-#{File.read('VERSION').chomp}.gem file" 9 | task :build do 10 | sh "gem build spira.gemspec && mv spira-#{File.read('VERSION').chomp}.gem pkg/" 11 | end 12 | 13 | desc "Release the spira-#{File.read('VERSION').chomp}.gem file" 14 | task :release do 15 | sh "gem push pkg/spira-#{File.read('VERSION').chomp}.gem" 16 | end 17 | end 18 | 19 | YARD::Rake::YardocTask.new 20 | 21 | desc 'Run specs' 22 | task 'spec' do 23 | RSpec::Core::RakeTask.new("spec") do |t| 24 | t.pattern = 'spec/**/*.{spec,rb}' 25 | t.rspec_opts = ["-c --order rand"] 26 | end 27 | end 28 | 29 | desc 'Run specs with backtrace' 30 | task 'tracespec' do 31 | RSpec::Core::RakeTask.new("tracespec") do |t| 32 | t.pattern = 'spec/**/*.{spec,rb}' 33 | t.rspec_opts = ["-bcf documentation"] 34 | end 35 | end 36 | 37 | desc 'Run coverage' 38 | task 'coverage' do 39 | RSpec::Core::RakeTask.new("coverage") do |t| 40 | t.pattern = 'spec/**/*.{spec,rb}' 41 | t.rspec_opts = ["-c"] 42 | end 43 | end 44 | 45 | desc "Open an irb session with everything loaded, including test fixtures" 46 | task :console do 47 | sh "irb -rubygems -I lib -r spira -I spec/fixtures -r person -r event -r cds -r cars -r posts -I spec -r spec_helper -r loading" 48 | end 49 | 50 | task default: [:spec] 51 | 52 | desc "Add analytics tracking information to yardocs" 53 | task :addanalytics do 54 | tracking_code = < 56 | 57 | var _gaq = _gaq || []; 58 | _gaq.push(['_setAccount', 'UA-3784741-3']); 59 | _gaq.push(['_trackPageview']); 60 | 61 | (function() { 62 | var ga = document.createElement('script'); ga.type = 'text\/javascript'; ga.async = true; 63 | ga.src = ('https:' == document.location.protocol ? 'https:\/\/ssl' : 'http:\/\/www') + '.google-analytics.com\/ga.js'; 64 | var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s); 65 | })(); 66 | 67 | 68 | EOC 69 | files = Dir.glob('./doc/yard/**/*.html').reject { |file| %w{class_list file_list frames.html _index.html method_list}.any? { |skipfile| file.include?(skipfile) }} 70 | files.each do |file| 71 | contents = File.read(file) 72 | writer = File.open(file, 'w') 73 | writer.write(contents.gsub(/\<\/body\>/, tracking_code + "")) 74 | writer.flush 75 | end 76 | end 77 | -------------------------------------------------------------------------------- /UNLICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 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 NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | 3.3.0 2 | -------------------------------------------------------------------------------- /lib/rdf/ext/uri.rb: -------------------------------------------------------------------------------- 1 | module RDF 2 | class URI 3 | ## 4 | # Create a projection of this URI as the given Spira::Resource class. 5 | # Equivalent to `klass.for(self, *args)` 6 | # 7 | # @example Instantiating a URI as a Spira Resource 8 | # RDF::URI('http://example.org/person/bob').as(Person) 9 | # @param [Class] klass 10 | # @param [*Any] args Any arguments to pass to klass.for 11 | # @yield [self] Executes a given block and calls `#save!` 12 | # @yieldparam [self] self The newly created instance 13 | # @return [Klass] An instance of klass 14 | def as(klass, *args, &block) 15 | raise ArgumentError, "#{klass} is not a Spira resource" unless klass.is_a?(Class) && klass.ancestors.include?(Spira::Base) 16 | klass.for(self, *args, &block) 17 | end 18 | end 19 | 20 | class Node 21 | ## 22 | # Create a projection of this Node as the given Spira::Resource class. 23 | # Equivalent to `klass.for(self, *args)` 24 | # 25 | # @example Instantiating a blank node as a Spira Resource 26 | # RDF::Node.new.as(Person) 27 | # @param [Class] klass 28 | # @param [*Any] args Any arguments to pass to klass.for 29 | # @yield [self] Executes a given block and calls `#save!` 30 | # @yieldparam [self] self The newly created instance 31 | # @return [Klass] An instance of klass 32 | def as(klass, *args) 33 | raise ArgumentError, "#{klass} is not a Spira resource" unless klass.is_a?(Class) && klass.ancestors.include?(Spira::Base) 34 | klass.for(self, *args) 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /lib/spira.rb: -------------------------------------------------------------------------------- 1 | require "rdf" 2 | require "rdf/ext/uri" 3 | require "promise" 4 | require "spira/exceptions" 5 | require "spira/utils" 6 | require "rdf/vocab" 7 | 8 | ## 9 | # Spira is a framework for building projections of RDF data into Ruby classes. 10 | # It is built on top of RDF.rb. 11 | # 12 | # @see https://rubygems.org/gems/rdf 13 | # @see https://github.com/bhuga/spira 14 | # @see Spira::Resource 15 | 16 | module Spira 17 | 18 | autoload :Base, 'spira/base' 19 | autoload :Type, 'spira/type' 20 | autoload :Types, 'spira/types' 21 | autoload :VERSION, 'spira/version' 22 | 23 | ## 24 | # The list of all property types available for Spira resources 25 | # 26 | # @see Spira::Types 27 | # @return [Hash{Symbol => Spira::Type}] 28 | def types 29 | @types ||= {} 30 | end 31 | module_function :types 32 | 33 | ## 34 | # The RDF::Repository used (reader) 35 | # 36 | # @return [RDF::Repository] 37 | # @see RDF::Repository 38 | def repository 39 | Thread.current[:repository] 40 | end 41 | module_function :repository 42 | 43 | ## 44 | # The RDF::Repository used (reader) 45 | # 46 | # @return [RDF::Repository] 47 | # @see RDF::Repository 48 | def repository=(repository) 49 | Thread.current[:repository] = repository 50 | end 51 | module_function :repository= 52 | 53 | ## 54 | # Clear the repository from Spira's knowledge. Use it if you want, but 55 | # it's really here for testing. 56 | # 57 | # @return [Void] 58 | # @private 59 | def clear_repository! 60 | Spira.repository = nil 61 | end 62 | module_function :clear_repository! 63 | 64 | # Execute a block on a specific repository 65 | # 66 | # @param [RDF::Repository] repo the repository to work on 67 | # @yield the block with the instructions while using the repository 68 | # @return [RDF::Repository] the given repository 69 | def using_repository(repo) 70 | old_repository = Spira.repository 71 | Spira.repository = repo 72 | yield if block_given? 73 | repo 74 | ensure 75 | Spira.repository = old_repository 76 | end 77 | module_function :using_repository 78 | 79 | private 80 | 81 | ## 82 | # Alias a property type to another. This allows a range of options to be 83 | # specified for a property type which all reference one Spira::Type 84 | # 85 | # @param [Any] new The new symbol or reference 86 | # @param [Any] original The type the new symbol should refer to 87 | # @return [Void] 88 | def type_alias(new, original) 89 | types[new] = original 90 | end 91 | module_function :type_alias 92 | end 93 | -------------------------------------------------------------------------------- /lib/spira/association_reflection.rb: -------------------------------------------------------------------------------- 1 | class AssociationReflection 2 | attr_reader :macro 3 | attr_reader :name 4 | attr_reader :options 5 | 6 | def initialize(macro, name, options = {}) 7 | @macro = macro 8 | @name = name 9 | @options = options 10 | end 11 | 12 | def class_name 13 | @class_name ||= (options[:type] || derive_class_name).to_s 14 | end 15 | 16 | def klass 17 | @klass ||= class_name.constantize 18 | end 19 | 20 | private 21 | 22 | def derive_class_name 23 | name.to_s.camelize 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/spira/base.rb: -------------------------------------------------------------------------------- 1 | require "set" 2 | require "active_model" 3 | require "rdf/isomorphic" 4 | require "active_support/core_ext/hash/indifferent_access" 5 | 6 | require "spira/resource" 7 | require "spira/persistence" 8 | require "spira/validations" 9 | require "spira/reflections" 10 | require "spira/serialization" 11 | 12 | module Spira 13 | 14 | ## 15 | # Spira::Base aims to perform similar to ActiveRecord::Base 16 | # You should inherit your models from it. 17 | # 18 | class Base 19 | extend ActiveModel::Callbacks 20 | extend ActiveModel::Naming 21 | include ActiveModel::Conversion 22 | include ActiveModel::Dirty 23 | include ActiveModel::Serialization 24 | 25 | include ::RDF, ::RDF::Enumerable, ::RDF::Queryable, Utils 26 | 27 | define_model_callbacks :save, :destroy, :create, :update 28 | 29 | ## 30 | # This instance's URI. 31 | # 32 | # @return [RDF::URI] 33 | attr_reader :subject 34 | 35 | class << self 36 | attr_reader :reflections, :properties 37 | 38 | def types 39 | Set.new 40 | end 41 | 42 | ## 43 | # The base URI for this class. Attempts to create instances for non-URI 44 | # objects will be appended to this base URI. 45 | # 46 | # @return [Void] 47 | def base_uri 48 | # should be redefined in children, if required 49 | # see also Spira::Resource.configure :base_uri option 50 | nil 51 | end 52 | 53 | ## 54 | # The default vocabulary for this class. Setting a default vocabulary 55 | # will allow properties to be defined without a `:predicate` option. 56 | # Predicates will instead be created by appending the property name to 57 | # the given string. 58 | # 59 | # @return [Void] 60 | def default_vocabulary 61 | # should be redefined in children, if required 62 | # see also Spira::Resource.configure :default_vocabulary option 63 | nil 64 | end 65 | 66 | def serialize(node, options = {}) 67 | if node.respond_to?(:subject) 68 | node.subject 69 | elsif node.respond_to?(:blank?) && node.blank? 70 | nil 71 | else 72 | raise TypeError, "cannot serialize #{node.inspect} as a Spira resource" 73 | end 74 | end 75 | 76 | def unserialize(value, options = {}) 77 | if value.respond_to?(:blank?) && value.blank? 78 | nil 79 | else 80 | # Spira resources are instantiated as "promised" 81 | # to avoid instantiation loops in case of resource-to-resource relations. 82 | promise { instantiate_record(value) } 83 | end 84 | end 85 | 86 | 87 | private 88 | 89 | def inherited(child) 90 | child.instance_variable_set :@properties, @properties.dup 91 | child.instance_variable_set :@reflections, @reflections.dup 92 | super 93 | end 94 | 95 | def instantiate_record(subj) 96 | new(_subject: id_for(subj)) 97 | end 98 | 99 | end # class methods 100 | 101 | 102 | def id 103 | new_record? ? nil : subject.path.split(/\//).last 104 | end 105 | 106 | ## 107 | # Initialize a new Spira::Base instance of this resource class using 108 | # a new blank node subject. Accepts a hash of arguments for initial 109 | # attributes. To use a URI or existing blank node as a subject, use 110 | # the `.for` method on the subclass instead. 111 | # 112 | # @example 113 | # class Person < Spira::Base; end 114 | # bob = Person.for("bob") 115 | # 116 | # @param [Hash{Symbol => Any}] props Default attributes for this instance 117 | # @yield [self] Executes a given block 118 | # @yieldparam [self] self The newly created instance 119 | # @see Spira::Persistence::ClassMethods#for 120 | # @see RDF::URI#as 121 | # @see RDF::Node#as 122 | def initialize(props = {}, options = {}) 123 | @subject = props.delete(:_subject) || RDF::Node.new 124 | @attrs = {} 125 | 126 | reload props 127 | 128 | yield self if block_given? 129 | end 130 | 131 | # Returns the attributes 132 | def attributes 133 | @attrs 134 | end 135 | 136 | # Freeze the attributes hash such that associations are still accessible, even on destroyed records. 137 | def freeze 138 | @attrs.freeze; self 139 | end 140 | 141 | # Returns +true+ if the attributes hash has been frozen. 142 | def frozen? 143 | @attrs.frozen? 144 | end 145 | 146 | ## 147 | # The `RDF.type` associated with this class. 148 | # 149 | # This just takes a first type from "types" list, 150 | # so make sure you know what you're doing if you use it. 151 | # 152 | # @return [nil,RDF::URI] The RDF type associated with this instance's class. 153 | def type 154 | self.class.type 155 | end 156 | 157 | ## 158 | # All `RDF.type` nodes associated with this class. 159 | # 160 | # @return [nil,RDF::URI] The RDF type associated with this instance's class. 161 | def types 162 | self.class.types 163 | end 164 | 165 | ## 166 | # Assign all attributes from the given hash. 167 | # 168 | def reload(props = {}) 169 | reset_changes 170 | super 171 | assign_attributes(props) 172 | self 173 | end 174 | 175 | ## 176 | # Returns the RDF representation of this resource. 177 | # 178 | # @return [RDF::Enumerable] 179 | def to_rdf 180 | self 181 | end 182 | 183 | ## 184 | # A developer-friendly view of this projection 185 | # 186 | def inspect 187 | "<#{self.class}:#{self.object_id} @subject: #{@subject}>" 188 | end 189 | 190 | ## 191 | # Compare this instance with another instance. The comparison is done on 192 | # an RDF level, and will work across subclasses as long as the attributes 193 | # are the same. 194 | # 195 | # @see https://rubygems.org/gems/rdf-isomorphic/ 196 | def ==(other) 197 | # TODO: define behavior for equality on subclasses. 198 | # TODO: should we compare attributes here? 199 | if self.class == other.class 200 | subject == other.uri 201 | elsif other.is_a?(RDF::Enumerable) 202 | self.isomorphic_with?(other) 203 | else 204 | false 205 | end 206 | end 207 | 208 | ## 209 | # Returns true for :to_uri if this instance's subject is a URI, and false if it is not. 210 | # Returns true for :to_node if this instance's subject is a Node, and false if it is not. 211 | # Calls super otherwise. 212 | # 213 | def respond_to?(*args) 214 | case args[0] 215 | when :to_uri 216 | subject.respond_to?(:to_uri) 217 | when :to_node 218 | subject.node? 219 | else 220 | super(*args) 221 | end 222 | end 223 | 224 | ## 225 | # Returns the RDF::URI associated with this instance if this instance's 226 | # subject is an RDF::URI, and nil otherwise. 227 | # 228 | # @return [RDF::URI,nil] 229 | def uri 230 | subject.respond_to?(:to_uri) ? subject : nil 231 | end 232 | 233 | ## 234 | # Returns the URI representation of this resource, if available. If this 235 | # resource's subject is a BNode, raises a NoMethodError. 236 | # 237 | # @return [RDF::URI] 238 | # @raise [NoMethodError] 239 | def to_uri 240 | uri || (raise NoMethodError, "No such method: :to_uri (this instance's subject is not a URI)") 241 | end 242 | 243 | ## 244 | # Returns true if the subject associated with this instance is a blank node. 245 | # 246 | # @return [true, false] 247 | def node? 248 | subject.node? 249 | end 250 | 251 | ## 252 | # Returns the Node subject of this resource, if available. If this 253 | # resource's subject is a URI, raises a NoMethodError. 254 | # 255 | # @return [RDF::Node] 256 | # @raise [NoMethodError] 257 | def to_node 258 | subject.node? ? subject : (raise NoMethodError, "No such method: :to_uri (this instance's subject is not a URI)") 259 | end 260 | 261 | ## 262 | # Returns a new instance of this class with the new subject instead of self.subject 263 | # 264 | # @param [RDF::Resource] new_subject 265 | # @return [Spira::Base] copy 266 | def copy(new_subject) 267 | self.class.new(@attrs.merge(_subject: new_subject)) 268 | end 269 | 270 | ## 271 | # Returns a new instance of this class with the new subject instead of 272 | # self.subject after saving the new copy to the repository. 273 | # 274 | # @param [RDF::Resource] new_subject 275 | # @return [Spira::Base, String] copy 276 | def copy!(new_subject) 277 | copy(new_subject).save! 278 | end 279 | 280 | ## 281 | # Assign attributes to the resource 282 | # without persisting it. 283 | def assign_attributes(attrs) 284 | attrs.each do |name, value| 285 | attribute_will_change!(name.to_s) 286 | send "#{name}=", value 287 | end 288 | end 289 | 290 | 291 | private 292 | 293 | def reset_changes 294 | clear_changes_information 295 | end 296 | 297 | def write_attribute(name, value) 298 | name = name.to_s 299 | if self.class.properties[name] 300 | if @attrs[name].is_a?(Promise) 301 | changed_attributes[name] = @attrs[name] unless changed_attributes.include?(name) 302 | @attrs[name] = value 303 | else 304 | if value != read_attribute(name) 305 | attribute_will_change!(name) 306 | @attrs[name] = value 307 | end 308 | end 309 | else 310 | raise Spira::PropertyMissingError, "attempt to assign a value to a non-existing property '#{name}'" 311 | end 312 | end 313 | 314 | ## 315 | # Get the current value for the given attribute 316 | # 317 | def read_attribute(name) 318 | value = @attrs[name.to_s] 319 | 320 | refl = self.class.reflections[name] 321 | if refl && !value 322 | # yield default values for empty reflections 323 | case refl.macro 324 | when :has_many 325 | # TODO: this should be actually handled by the reflection class 326 | [] 327 | end 328 | else 329 | value 330 | end 331 | end 332 | 333 | ## Localized properties functions 334 | 335 | def merge_localized_property(name, arg) 336 | values = read_attribute("#{name}_native") 337 | values.delete_if { |s| s.language == I18n.locale } 338 | values << serialize_localized_property(arg, I18n.locale) if arg 339 | values 340 | end 341 | 342 | def serialize_localized_property(value, locale) 343 | RDF::Literal.new(value, language: locale) 344 | end 345 | 346 | def unserialize_localized_properties(values, locale) 347 | v = values.detect { |s| s.language == locale || s.simple? } 348 | v && v.object 349 | end 350 | 351 | def hash_localized_properties(values) 352 | values.inject({}) do |out, v| 353 | out[v.language] = v.object 354 | out 355 | end 356 | end 357 | 358 | def serialize_hash_localized_properties(values) 359 | values.map { |lang, property| RDF::Literal.new(property, language: lang) } 360 | end 361 | 362 | # Build a Ruby value from an RDF value. 363 | def build_value(node, type) 364 | klass = classize_resource(type) 365 | if klass.respond_to?(:unserialize) 366 | klass.unserialize(node) 367 | else 368 | raise TypeError, "Unable to unserialize #{node} as #{type}" 369 | end 370 | end 371 | 372 | # Build an RDF value from a Ruby value for a property 373 | def build_rdf_value(value, type) 374 | klass = classize_resource(type) 375 | if klass.respond_to?(:serialize) 376 | klass.serialize(value) 377 | else 378 | raise TypeError, "Unable to serialize #{value} as #{type}" 379 | end 380 | end 381 | 382 | def valid_object?(node) 383 | node && (!node.literal? || node.valid?) 384 | end 385 | 386 | extend Resource 387 | extend Reflections 388 | include Types 389 | include Persistence 390 | include Validations 391 | include Serialization 392 | 393 | @reflections = HashWithIndifferentAccess.new 394 | @properties = HashWithIndifferentAccess.new 395 | end 396 | end 397 | -------------------------------------------------------------------------------- /lib/spira/exceptions.rb: -------------------------------------------------------------------------------- 1 | module Spira 2 | 3 | class SpiraError < StandardError ; end 4 | 5 | ## 6 | # For cases when a method is called which requires a `type` method be 7 | # declared on a Spira class. 8 | class NoTypeError < SpiraError ; end 9 | 10 | ## 11 | # For cases in which a repository is required but none has been given 12 | class NoRepositoryError < SpiraError ; end 13 | 14 | ## 15 | # For errors in the DSL, such as invalid predicates 16 | class ResourceDeclarationError < SpiraError ; end 17 | 18 | ## 19 | # Raised when user tries to assign a non-existing property 20 | class PropertyMissingError < SpiraError ; end 21 | 22 | ## 23 | # Raised when record cannot be persisted 24 | class RecordNotSaved < SpiraError ; end 25 | end 26 | -------------------------------------------------------------------------------- /lib/spira/reflections.rb: -------------------------------------------------------------------------------- 1 | require "spira/association_reflection" 2 | 3 | module Spira 4 | module Reflections 5 | # Returns a hash containing all AssociationReflection objects for the current class 6 | # Example: 7 | # 8 | # Invoice.reflections 9 | # Account.reflections 10 | # 11 | def reflections 12 | read_inheritable_attribute(:reflections) || write_inheritable_attribute(:reflections, {}) 13 | end 14 | 15 | def reflect_on_association(association) 16 | reflections[association].is_a?(AssociationReflection) ? reflections[association] : nil 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/spira/resource.rb: -------------------------------------------------------------------------------- 1 | require "active_support/core_ext/class" 2 | require "spira/association_reflection" 3 | 4 | module Spira 5 | module Resource 6 | ## 7 | # Configuration options for the Spira::Resource: 8 | # 9 | # @params[Hash] options 10 | # :base_uri :: base URI to be used for the resource 11 | # :default_vocabulary :: default vocabulary to use for the properties 12 | # defined for this resource 13 | # All these configuration options are readable via 14 | # their respectively named Spira resource methods. 15 | # 16 | def configure(options = {}) 17 | singleton_class.class_eval do 18 | { base_uri: options[:base_uri], 19 | default_vocabulary: options[:default_vocabulary] 20 | }.each do |name, value| 21 | # redefine reader methods only when required, 22 | # otherwise, use the ancestor methods 23 | if value 24 | define_method name do 25 | value 26 | end 27 | end 28 | end 29 | end 30 | end 31 | 32 | ## 33 | # Declare a type for the Spira::Resource. 34 | # You can declare multiple types for a resource 35 | # with multiple "type" assignments. 36 | # If no types are declared for a resource, 37 | # they are inherited from the parent resource. 38 | # 39 | # @params[RDF::URI] uri 40 | # 41 | def type(uri = nil) 42 | if uri 43 | if uri.is_a?(RDF::URI) 44 | ts = @types ? types : Set.new 45 | singleton_class.class_eval do 46 | define_method :types do 47 | ts 48 | end 49 | end 50 | @types = ts << uri 51 | else 52 | raise TypeError, "Type must be a RDF::URI" 53 | end 54 | else 55 | types.first 56 | end 57 | end 58 | 59 | ## 60 | # Add a property to this class. A property is an accessor field that 61 | # represents an RDF predicate. 62 | # 63 | # @example A simple string property 64 | # property :name, predicate: RDF::Vocab::FOAF.name, type: String 65 | # @example A property which defaults to {Spira::Types::Any} 66 | # property :name, predicate: RDF::Vocab::FOAF.name 67 | # @example An integer property 68 | # property :age, predicate: RDF::Vocab::FOAF.age, type: Integer 69 | # @param [Symbol] name The name of this property 70 | # @param [Hash{Symbol => Any}] opts property options 71 | # @option opts [RDF::URI] :predicate The RDF predicate which will refer to this property 72 | # @option opts [Spira::Type, String] :type (Spira::Types::Any) The 73 | # type for this property. If a Spira::Type is given, that class will be 74 | # used to serialize and unserialize values. If a String is given, it 75 | # should be the String form of a Spira::Base class name (Strings are 76 | # used to prevent issues with load order). 77 | # @see Spira::Types 78 | # @see Spira::Type 79 | # @return [Void] 80 | def property(name, opts = {}) 81 | if opts.delete(:localized) 82 | raise 'Only Spira::Types::Any properties can accept the :localized option' unless type_for(opts[:type]) == Spira::Types::Any 83 | define_localized_property_methods(name, opts) 84 | has_many "#{name}_native", opts.merge(type: Spira::Types::Native) 85 | else 86 | unset_has_many(name) 87 | predicate = predicate_for(opts[:predicate], name) 88 | type = type_for(opts[:type]) 89 | properties[name] = HashWithIndifferentAccess.new(predicate: predicate, type: type) 90 | 91 | define_attribute_method name 92 | define_method "#{name}=" do |arg| 93 | write_attribute name, arg 94 | end 95 | define_method name do 96 | read_attribute name 97 | end 98 | end 99 | end 100 | 101 | ## 102 | # The plural form of `property`. `Has_many` has the same options as 103 | # `property`, but instead of a single value, a Ruby Array of objects will 104 | # be created instead. 105 | # 106 | # has_many corresponds to an RDF subject with several triples of the same 107 | # predicate. This corresponds to a Ruby Array, which will be returned when 108 | # the property is accessed. Arrays will be accepted for new values, but 109 | # ordering and duplicate values will be lost on save. 110 | # 111 | # @see Spira::Base::DSL#property 112 | def has_many(name, opts = {}) 113 | property(name, opts) 114 | 115 | reflections[name] = AssociationReflection.new(:has_many, name, opts) 116 | 117 | define_method "#{name.to_s.singularize}_ids" do 118 | records = send(name) || [] 119 | records.map(&:id).compact 120 | end 121 | define_method "#{name.to_s.singularize}_ids=" do |ids| 122 | records = ids.map {|id| self.class.reflect_on_association(name).klass.unserialize(id) }.compact 123 | send "#{name}=", records 124 | end 125 | end 126 | 127 | private 128 | 129 | # Unset a has_many relation if it exists. Allow to redefine the cardinality of a relation in a subClass 130 | # 131 | # @private 132 | def unset_has_many(name) 133 | if reflections[name] 134 | reflections.delete(name) 135 | undef_method "#{name.to_s.singularize}_ids" 136 | undef_method "#{name.to_s.singularize}_ids=" 137 | end 138 | end 139 | 140 | ## 141 | # Create the localized specific getter/setter for a given property 142 | # 143 | # @private 144 | def define_localized_property_methods(name, opts) 145 | define_method "#{name}=" do |arg| 146 | new_value = merge_localized_property(name, arg) 147 | write_attribute "#{name}_native", new_value 148 | end 149 | 150 | define_method name do 151 | value = read_attribute("#{name}_native") 152 | unserialize_localized_properties(value, I18n.locale) 153 | end 154 | 155 | define_method "#{name}_with_locales" do 156 | value = read_attribute("#{name}_native") 157 | hash_localized_properties(value) 158 | end 159 | 160 | define_method "#{name}_with_locales=" do |arg| 161 | value = serialize_hash_localized_properties(arg) 162 | write_attribute "#{name}_native", value 163 | end 164 | end 165 | 166 | ## 167 | # Determine the predicate for a property based on the given predicate, name, and default vocabulary 168 | # 169 | # @param [#to_s, #to_uri] predicate 170 | # @param [Symbol] name 171 | # @return [RDF::URI] 172 | # @private 173 | def predicate_for(predicate, name) 174 | case 175 | when predicate.respond_to?(:to_uri) && predicate.to_uri.absolute? 176 | predicate 177 | when default_vocabulary.nil? 178 | raise ResourceDeclarationError, "A :predicate option is required for types without a default vocabulary" 179 | else 180 | # FIXME: use rdf.rb smart separator after 0.3.0 release 181 | separator = default_vocabulary.to_s[-1,1] =~ /(\/|#)/ ? '' : '/' 182 | RDF::URI.intern(default_vocabulary.to_s + separator + name.to_s) 183 | end 184 | end 185 | 186 | ## 187 | # Determine the type for a property based on the given type option 188 | # 189 | # @param [nil, Spira::Type, Constant] type 190 | # @return Spira::Type 191 | # @private 192 | def type_for(type) 193 | case 194 | when type.nil? 195 | Spira::Types::Any 196 | when type.is_a?(Symbol) || type.is_a?(String) 197 | type 198 | when Spira.types[type] 199 | Spira.types[type] 200 | else 201 | raise TypeError, "Unrecognized type: #{type}" 202 | end 203 | end 204 | 205 | end 206 | end 207 | -------------------------------------------------------------------------------- /lib/spira/serialization.rb: -------------------------------------------------------------------------------- 1 | module Spira 2 | module Serialization 3 | ## 4 | # Support for Psych (YAML) custom serializer. 5 | # 6 | # This causes the subject and all attributes to be saved to a YAML or JSON serialization 7 | # in such a way that they can be restored in the future. 8 | # 9 | # @param [Psych::Coder] coder 10 | def encode_with(coder) 11 | coder["subject"] = subject 12 | attributes.each {|p,v| coder[p.to_s] = v if v} 13 | end 14 | 15 | ## 16 | # Support for Psych (YAML) custom de-serializer. 17 | # 18 | # Updates a previously allocated Spira::Base instance to that of a previously 19 | # serialized instance. 20 | # 21 | # @param [Psych::Coder] coder 22 | def init_with(coder) 23 | instance_variable_set(:"@subject", coder["subject"]) 24 | assign_attributes coder.map.except("subject") 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /lib/spira/type.rb: -------------------------------------------------------------------------------- 1 | module Spira 2 | 3 | ## 4 | # Spira::Type can be included by classes to create new property types for 5 | # Spira. These types are responsible for serialization a Ruby value into an 6 | # `RDF::Value`, and deserialization of an `RDF::Value` into a Ruby value. 7 | # 8 | # A simple example: 9 | # 10 | # class Integer 11 | # 12 | # include Spira::Type 13 | # 14 | # def self.unserialize(value) 15 | # value.object 16 | # end 17 | # 18 | # def self.serialize(value) 19 | # RDF::Literal.new(value) 20 | # end 21 | # 22 | # register_alias XSD.integer 23 | # end 24 | # 25 | # This example will serialize and deserialize integers. It's included with 26 | # Spira by default. It allows either of the following forms to declare an 27 | # integer property on a Spira resource: 28 | # 29 | # property :age, predicate: RDF::Vocab::FOAF.age, type: Integer 30 | # property :age, predicate: RDF::Vocab::FOAF.age, type: RDF::XSD.integer 31 | # 32 | # `Spira::Type`s include the RDF namespace and thus have all of the base RDF 33 | # vocabularies available to them without the `RDF::` prefix. 34 | # 35 | # @see https://ruby-rdf.github.io/rdf/RDF/Value.html 36 | # @see Spira::Resource 37 | module Type 38 | 39 | ## 40 | # Make the DSL available to a child class. 41 | # 42 | # @private 43 | def self.included(child) 44 | child.extend(ClassMethods) 45 | Spira.type_alias(child,child) 46 | end 47 | 48 | include RDF 49 | 50 | module ClassMethods 51 | 52 | ## 53 | # Register an alias that this type can be referred to as, such as an RDF 54 | # URI. The alias can be any object, symbol, or constant. 55 | # 56 | # @param [Any] identifier The new alias in property declarations for this class 57 | # @return [Void] 58 | def register_alias(identifier) 59 | Spira.type_alias(identifier, self) 60 | end 61 | 62 | ## 63 | # Serialize a given value to RDF. 64 | # 65 | # @param [Any] value The Ruby value to be serialized 66 | # @return [RDF::Value] The RDF form of this value 67 | def serialize(value) 68 | value 69 | end 70 | 71 | ## 72 | # Unserialize a given RDF value to Ruby 73 | # 74 | # @param [RDF::Value] value The RDF form of this value 75 | # @return [Any] The Ruby form of this value 76 | def unserialize(value) 77 | value 78 | end 79 | end 80 | 81 | end 82 | end 83 | -------------------------------------------------------------------------------- /lib/spira/types.rb: -------------------------------------------------------------------------------- 1 | module Spira 2 | 3 | ## 4 | # Spira::Types is a set of default Spira::Type classes. 5 | # 6 | # @see Spira::Type 7 | # @see Spira::Types::Int 8 | # @see Spira::Types::Integer 9 | # @see Spira::Types::Boolean 10 | # @see Spira::Types::String 11 | # @see Spira::Types::GYear 12 | # @see Spira::Types::Date 13 | # @see Spira::Types::DateTime 14 | # @see Spira::Types::Float 15 | # @see Spira::Types::Any 16 | module Types 17 | 18 | # No autoloading here--the associations to XSD types are made by the 19 | # classes themselves, so we need to require them or XSD types 20 | # will show up as not found. 21 | 22 | Dir.glob(File.join(File.dirname(__FILE__), 'types', '*.rb')).each { |file| require file } 23 | 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/spira/types/any.rb: -------------------------------------------------------------------------------- 1 | module Spira::Types 2 | 3 | ## 4 | # This class does its best to serialize or unserialize RDF values into Ruby 5 | # values and vice versa using RDF.rb's built-in helpers for `RDF::Literal`s. 6 | # Its behavior is defined as 'What `RDF::Literal` does' for a given value. 7 | # 8 | # @see Spira::Type 9 | # @see https://ruby-rdf.github.io/rdf/RDF/Literal.html 10 | class Any 11 | 12 | include Spira::Type 13 | 14 | def self.unserialize(value) 15 | value.respond_to?(:object) ? value.object : value 16 | end 17 | 18 | def self.serialize(value) 19 | raise TypeError, "Spira::Types::Any cannot serialize collections" if value.is_a?(Array) 20 | value.is_a?(RDF::Value) ? value : RDF::Literal.new(value) 21 | end 22 | 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /lib/spira/types/anyURI.rb: -------------------------------------------------------------------------------- 1 | module Spira::Types 2 | 3 | ## 4 | # A {Spira::Type} for URI values. Values will be associated with the 5 | # `XSD.anyURI` type with no language code. 6 | # 7 | # A {Spira::Resource} property can reference this type as 8 | # `Spira::Types::AnyURI`, `AnyURI`, or `XSD.anyURI`. 9 | # 10 | # @see Spira::Type 11 | # @see https://ruby-rdf.github.io/rdf/RDF/Literal.html 12 | class AnyURI 13 | 14 | include Spira::Type 15 | 16 | def self.unserialize(value) 17 | value.object 18 | end 19 | 20 | def self.serialize(value) 21 | RDF::Literal.new(value, datatype: XSD.anyURI) 22 | end 23 | 24 | register_alias RDF::XSD.anyURI 25 | 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /lib/spira/types/boolean.rb: -------------------------------------------------------------------------------- 1 | module Spira::Types 2 | 3 | ## 4 | # A {Spira::Type} for Boolean values. Values will be expressed as booleans 5 | # and packaged as `XSD.boolean` `RDF::Literal`s. 6 | # 7 | # A {Spira::Resource} property can reference this type as 8 | # `Spira::Types::Boolean`, `Boolean`, or `XSD.boolean`. 9 | # 10 | # @see Spira::Type 11 | # @see https://ruby-rdf.github.io/rdf/RDF/Literal.html 12 | class Boolean 13 | 14 | include Spira::Type 15 | 16 | def self.unserialize(value) 17 | value.object == true 18 | end 19 | 20 | def self.serialize(value) 21 | if value 22 | RDF::Literal.new(true, datatype: RDF::XSD.boolean) 23 | else 24 | RDF::Literal.new(false, datatype: RDF::XSD.boolean) 25 | end 26 | end 27 | 28 | register_alias RDF::XSD.boolean 29 | 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /lib/spira/types/date.rb: -------------------------------------------------------------------------------- 1 | module Spira::Types 2 | 3 | ## 4 | # A {Spira::Type} for dates. Values will be associated with the 5 | # `XSD.date` type. 6 | # 7 | # A {Spira::Resource} property can reference this type as 8 | # `Spira::Types::Date`, `Date`, or `XSD.date`. 9 | # 10 | # @see Spira::Type 11 | # @see https://ruby-rdf.github.io/rdf/RDF/Literal.html 12 | class Date 13 | include Spira::Type 14 | 15 | def self.unserialize(value) 16 | object = value.object 17 | end 18 | 19 | def self.serialize(value) 20 | RDF::Literal.new(value, datatype: XSD.date) 21 | end 22 | 23 | register_alias RDF::XSD.date 24 | 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /lib/spira/types/dateTime.rb: -------------------------------------------------------------------------------- 1 | module Spira::Types 2 | 3 | ## 4 | # A {Spira::Type} for dateTimes. Values will be associated with the 5 | # `XSD.dateTime` type. 6 | # 7 | # A {Spira::Resource} property can reference this type as 8 | # `Spira::Types::DateTime`, `DateTime`, or `XSD.dateTime`. 9 | # 10 | # @see Spira::Type 11 | # @see https://ruby-rdf.github.io/rdf/RDF/Literal.html 12 | class DateTime 13 | include Spira::Type 14 | 15 | def self.unserialize(value) 16 | object = value.object 17 | end 18 | 19 | def self.serialize(value) 20 | RDF::Literal.new(value, datatype: XSD.dateTime) 21 | end 22 | 23 | register_alias RDF::XSD.dateTime 24 | 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /lib/spira/types/decimal.rb: -------------------------------------------------------------------------------- 1 | require 'bigdecimal' 2 | 3 | module Spira::Types 4 | 5 | ## 6 | # A {Spira::Type} for integer values. Values will be associated with the 7 | # `XSD.integer` type. 8 | # 9 | # A {Spira::Resource} property can reference this type as 10 | # `Spira::Types::Integer`, `Integer`, or `XSD.integer`. 11 | # 12 | # @see Spira::Type 13 | # @see https://ruby-rdf.github.io/rdf/RDF/Literal.html 14 | class Decimal 15 | include Spira::Type 16 | 17 | def self.unserialize(value) 18 | object = value.object 19 | object.is_a?(BigDecimal) ? object : BigDecimal.new(object.to_s) 20 | end 21 | 22 | def self.serialize(value) 23 | RDF::Literal.new(value.is_a?(BigDecimal) ? value.to_s('F') : value.to_s, datatype: RDF::XSD.decimal) 24 | end 25 | 26 | register_alias RDF::XSD.decimal 27 | 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /lib/spira/types/double.rb: -------------------------------------------------------------------------------- 1 | module Spira::Types 2 | 3 | ## 4 | # A {Spira::Type} for Double values. Values will be associated with the 5 | # `XSD.double` type. 6 | # 7 | # A {Spira::Resource} property can reference this type as 8 | # `Spira::Types::Double`, `Double`, or `XSD.double`. 9 | # 10 | # @see Spira::Type 11 | # @see https://ruby-rdf.github.io/rdf/RDF/Literal.html 12 | class Double 13 | 14 | include Spira::Type 15 | 16 | def self.unserialize(value) 17 | value.object.to_f 18 | end 19 | 20 | def self.serialize(value) 21 | RDF::Literal.new(value.to_f, datatype: RDF::XSD.double) 22 | end 23 | 24 | register_alias RDF::XSD.double 25 | 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /lib/spira/types/float.rb: -------------------------------------------------------------------------------- 1 | module Spira::Types 2 | 3 | ## 4 | # A {Spira::Type} for Float values. Values will be associated with the 5 | # `XSD.float` type. 6 | # 7 | # A {Spira::Resource} property can reference this type as 8 | # `Spira::Types::Float`, `Float`, or `XSD.float`. 9 | # 10 | # @see Spira::Type 11 | # @see https://ruby-rdf.github.io/rdf/RDF/Literal.html 12 | class Float 13 | 14 | include Spira::Type 15 | 16 | def self.unserialize(value) 17 | value.object.to_f 18 | end 19 | 20 | def self.serialize(value) 21 | RDF::Literal.new(value.to_f, datatype: RDF::XSD.float) 22 | end 23 | 24 | register_alias RDF::XSD.float 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /lib/spira/types/gYear.rb: -------------------------------------------------------------------------------- 1 | module Spira::Types 2 | 3 | ## 4 | # A {Spira::Type} for gYear. Values will be associated with the 5 | # `XSD.gYear` type. 6 | # 7 | # A {Spira::Resource} property can reference this type as 8 | # `Spira::Types::GYear`, `GYear`, or `XSD.gYear`. 9 | # 10 | # @see Spira::Type 11 | # @see https://ruby-rdf.github.io/rdf/RDF/Literal.html 12 | class GYear 13 | include Spira::Type 14 | 15 | def self.unserialize(value) 16 | object = value.object.to_i 17 | end 18 | 19 | def self.serialize(value) 20 | RDF::Literal.new(value, datatype: XSD.gYear) 21 | end 22 | 23 | register_alias RDF::XSD.gYear 24 | 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /lib/spira/types/int.rb: -------------------------------------------------------------------------------- 1 | module Spira::Types 2 | 3 | ## 4 | # A {Spira::Type} for integer values. Values will be associated with the 5 | # `XSD.int` type. 6 | # 7 | # A {Spira::Resource} property can reference this type as 8 | # `Spira::Types::Int`, `Int`, or `XSD.int`. 9 | # 10 | # @see Spira::Type 11 | # @see https://ruby-rdf.github.io/rdf/RDF/Literal.html 12 | class Int 13 | 14 | include Spira::Type 15 | 16 | def self.unserialize(value) 17 | value.object.to_i 18 | end 19 | 20 | def self.serialize(value) 21 | RDF::Literal.new(value, datatype: XSD.int) 22 | end 23 | 24 | register_alias RDF::XSD.int 25 | 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /lib/spira/types/integer.rb: -------------------------------------------------------------------------------- 1 | module Spira::Types 2 | 3 | ## 4 | # A {Spira::Type} for integer values. Values will be associated with the 5 | # `XSD.integer` type. 6 | # 7 | # A {Spira::Resource} property can reference this type as 8 | # `Spira::Types::Integer`, `Integer`, or `XSD.integer`. 9 | # 10 | # @see Spira::Type 11 | # @see https://ruby-rdf.github.io/rdf/RDF/Literal.html 12 | class Integer 13 | 14 | include Spira::Type 15 | 16 | def self.unserialize(value) 17 | value.object.to_i 18 | end 19 | 20 | def self.serialize(value) 21 | RDF::Literal.new(value, datatype: XSD.integer) 22 | end 23 | 24 | register_alias RDF::XSD.integer 25 | 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /lib/spira/types/long.rb: -------------------------------------------------------------------------------- 1 | module Spira::Types 2 | 3 | ## 4 | # A {Spira::Type} for integer values. Values will be associated with the 5 | # `XSD.long` type. 6 | # 7 | # A {Spira::Resource} property can reference this type as 8 | # `Spira::Types::Long`, `Long`, or `XSD.long`. 9 | # 10 | # @see Spira::Type 11 | # @see https://ruby-rdf.github.io/rdf/RDF/Literal.html 12 | class Long 13 | 14 | include Spira::Type 15 | 16 | def self.unserialize(value) 17 | value.object.to_i 18 | end 19 | 20 | def self.serialize(value) 21 | RDF::Literal.new(value, datatype: XSD.long) 22 | end 23 | 24 | register_alias RDF::XSD.long 25 | 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /lib/spira/types/native.rb: -------------------------------------------------------------------------------- 1 | module Spira::Types 2 | 3 | ## 4 | # This type is a native type, doing no conversion to Ruby types. The naked 5 | # RDF::Value (URI, Node, Literal, etc) will be used, and no deserialization 6 | # is done. 7 | # 8 | # @see Spira::Type 9 | class Native 10 | 11 | include Spira::Type 12 | 13 | def self.unserialize(value) 14 | value 15 | end 16 | 17 | def self.serialize(value) 18 | value 19 | end 20 | 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /lib/spira/types/negativeInteger.rb: -------------------------------------------------------------------------------- 1 | module Spira::Types 2 | 3 | ## 4 | # A {Spira::Type} for negative integer values. Values will be associated with the 5 | # `XSD.negativeInteger` type. 6 | # 7 | # A {Spira::Resource} property can reference this type as 8 | # `Spira::Types::NegativeInteger`, `NegativeInteger`, or `XSD.negativeInteger`. 9 | # 10 | # @see Spira::Type 11 | # @see https://ruby-rdf.github.io/rdf/RDF/Literal.html 12 | class NegativeInteger 13 | 14 | include Spira::Type 15 | 16 | def self.unserialize(value) 17 | value.object.to_i 18 | end 19 | 20 | def self.serialize(value) 21 | RDF::Literal.new(value, datatype: XSD.negativeInteger) 22 | end 23 | 24 | register_alias RDF::XSD.negativeInteger 25 | 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /lib/spira/types/nonNegativeInteger.rb: -------------------------------------------------------------------------------- 1 | module Spira::Types 2 | 3 | ## 4 | # A {Spira::Type} for nonnegative integer values. Values will be associated with the 5 | # `XSD.nonNegativeInteger` type. 6 | # 7 | # A {Spira::Resource} property can reference this type as 8 | # `Spira::Types::NonNegativeInteger`, `NonNegativeInteger`, or `XSD.nonNegativeInteger`. 9 | # 10 | # @see Spira::Type 11 | # @see https://ruby-rdf.github.io/rdf/RDF/Literal.html 12 | class NonNegativeInteger 13 | 14 | include Spira::Type 15 | 16 | def self.unserialize(value) 17 | value.object.to_i 18 | end 19 | 20 | def self.serialize(value) 21 | RDF::Literal.new(value, datatype: XSD.nonNegativeInteger) 22 | end 23 | 24 | register_alias RDF::XSD.nonNegativeInteger 25 | 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /lib/spira/types/nonPositiveInteger.rb: -------------------------------------------------------------------------------- 1 | module Spira::Types 2 | 3 | ## 4 | # A {Spira::Type} for nonpositive integer values. Values will be associated with the 5 | # `XSD.nonPositiveInteger` type. 6 | # 7 | # A {Spira::Resource} property can reference this type as 8 | # `Spira::Types::NonPositiveInteger`, `NonPositiveInteger`, or `XSD.nonPositiveInteger`. 9 | # 10 | # @see Spira::Type 11 | # @see https://ruby-rdf.github.io/rdf/RDF/Literal.html 12 | class NonPositiveInteger 13 | 14 | include Spira::Type 15 | 16 | def self.unserialize(value) 17 | value.object.to_i 18 | end 19 | 20 | def self.serialize(value) 21 | RDF::Literal.new(value, datatype: XSD.nonPositiveInteger) 22 | end 23 | 24 | register_alias RDF::XSD.nonPositiveInteger 25 | 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /lib/spira/types/positiveInteger.rb: -------------------------------------------------------------------------------- 1 | module Spira::Types 2 | 3 | ## 4 | # A {Spira::Type} for positive integer values. Values will be associated with the 5 | # `XSD.positiveInteger` type. 6 | # 7 | # A {Spira::Resource} property can reference this type as 8 | # `Spira::Types::PositiveInteger`, `PositiveInteger`, or `XSD.positiveInteger`. 9 | # 10 | # @see Spira::Type 11 | # @see https://ruby-rdf.github.io/rdf/RDF/Literal.html 12 | class PositiveInteger 13 | 14 | include Spira::Type 15 | 16 | def self.unserialize(value) 17 | value.object.to_i 18 | end 19 | 20 | def self.serialize(value) 21 | RDF::Literal.new(value, datatype: XSD.positiveInteger) 22 | end 23 | 24 | register_alias RDF::XSD.positiveInteger 25 | 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /lib/spira/types/string.rb: -------------------------------------------------------------------------------- 1 | module Spira::Types 2 | 3 | ## 4 | # A {Spira::Type} for string values. Values will be associated with the 5 | # `XSD.string` type with no language code. 6 | # 7 | # A {Spira::Resource} property can reference this type as 8 | # `Spira::Types::String`, `String`, or `XSD.string`. 9 | # 10 | # @see Spira::Type 11 | # @see https://ruby-rdf.github.io/rdf/RDF/Literal.html 12 | class String 13 | 14 | include Spira::Type 15 | 16 | def self.unserialize(value) 17 | value.object.to_s 18 | end 19 | 20 | def self.serialize(value) 21 | RDF::Literal.new(value.to_s) 22 | end 23 | 24 | register_alias RDF::XSD.string 25 | 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /lib/spira/types/time.rb: -------------------------------------------------------------------------------- 1 | module Spira::Types 2 | 3 | ## 4 | # A {Spira::Type} for times. Values will be associated with the 5 | # `XSD.time` type. 6 | # 7 | # A {Spira::Resource} property can reference this type as 8 | # `Spira::Types::Time`, `Time`, or `XSD.time`. 9 | # 10 | # @see Spira::Type 11 | # @see https://ruby-rdf.github.io/rdf/RDF/Literal.html 12 | class Time 13 | include Spira::Type 14 | 15 | def self.unserialize(value) 16 | object = value.object.to_time 17 | end 18 | 19 | def self.serialize(value) 20 | RDF::Literal.new(value, datatype: XSD.time) 21 | end 22 | 23 | register_alias RDF::XSD.time 24 | 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /lib/spira/types/uri.rb: -------------------------------------------------------------------------------- 1 | module Spira::Types 2 | 3 | ## 4 | # This type takes RDF Resource objects and provides RDF::URI objects for the 5 | # ruby representation. 6 | # 7 | # @see Spira::Type 8 | # @see https://ruby-rdf.github.io/rdf/RDF/URI.html 9 | class URI 10 | 11 | include Spira::Type 12 | 13 | def self.unserialize(value) 14 | RDF::URI(value) 15 | end 16 | 17 | def self.serialize(value) 18 | RDF::URI(value) 19 | end 20 | 21 | register_alias :uri 22 | register_alias RDF::URI 23 | 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/spira/utils.rb: -------------------------------------------------------------------------------- 1 | module Spira 2 | module Utils 3 | ## 4 | # Rename the subject of a Spira object to something else 5 | # @param [RDF::Resource] new_subject 6 | # @param [RDF::Repository] repository 7 | # @return [Spira::Base] A new instance using this subject 8 | def rename!(new_subject, repository = nil) 9 | repository ||= Spira.repository 10 | repository.rename!(subject, new_subject) 11 | self.class.for(new_subject) 12 | end 13 | end 14 | end 15 | 16 | module RDF 17 | class Repository 18 | ## 19 | # Rename a resource in the Repository to the new given subject. 20 | # 21 | # @param [RDF::Resource] old_subject 22 | # @param [RDF::Resource] new_subject 23 | # @return [self] 24 | def rename!(old_subject, new_subject) 25 | transaction(mutable: true) do |tx| 26 | query({subject: old_subject}) do |statement| 27 | tx.insert RDF::Statement.new(new_subject, statement.predicate, statement.object) 28 | tx.delete(statement) 29 | end 30 | query({object: old_subject}) do |statement| 31 | tx.insert RDF::Statement.new(statement.subject, statement.predicate, new_subject) 32 | tx.delete(statement) 33 | end 34 | end 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /lib/spira/validations.rb: -------------------------------------------------------------------------------- 1 | module Spira 2 | # = Spira RecordInvalid 3 | # 4 | # Raised by save! and create! when the record is invalid. Use the 5 | # +record+ method to retrieve the record which did not validate. 6 | # 7 | # begin 8 | # complex_operation_that_calls_save!_internally 9 | # rescue Spira:RecordInvalid: invalid 10 | # puts invalid.record.errors 11 | # end 12 | class RecordInvalid < SpiraError 13 | attr_reader :record 14 | def initialize(record) 15 | @record = record 16 | errors = @record.errors.full_messages.join(", ") 17 | # TODO: use I18n later 18 | # super(I18n.t("activerecord.errors.messages.record_invalid", errors: errors)) 19 | super "invalid record" 20 | end 21 | end 22 | 23 | module Validations 24 | extend ActiveSupport::Concern 25 | include ActiveModel::Validations 26 | 27 | module ClassMethods 28 | # Creates an object just like Base.create but calls save! instead of +save+ 29 | # so an exception is raised if the record is invalid. 30 | def create!(properties = {}, options = {}, &block) 31 | if properties.is_a?(Array) 32 | properties.collect { |attr| create!(attr, options, &block) } 33 | else 34 | object = new(properties, options) 35 | yield(object) if block_given? 36 | object.save! 37 | object 38 | end 39 | end 40 | end 41 | 42 | # The validation process on save can be skipped by passing validate: false. The regular Base#save method is 43 | # replaced with this when the validations module is mixed in, which it is by default. 44 | def save(options={}) 45 | perform_validations(options) ? super : false 46 | end 47 | 48 | # Attempts to save the record just like Base#save but will raise a +RecordInvalid+ exception instead of returning false 49 | # if the record is not valid. 50 | def save!(options={}) 51 | perform_validations(options) ? super : raise(RecordInvalid.new(self)) 52 | end 53 | 54 | # Runs all the validations within the specified context. Returns true if no errors are found, 55 | # false otherwise. 56 | # 57 | # If the argument is false (default is +nil+), the context is set to :create if 58 | # new_record? is true, and to :update if it is not. 59 | # 60 | # Validations with no :on option will run no matter the context. Validations with 61 | # some :on option will only run in the specified context. 62 | def valid?(context = nil) 63 | context ||= (new_record? ? :create : :update) 64 | output = super(context) 65 | errors.empty? && output 66 | end 67 | 68 | protected 69 | 70 | def perform_validations(options={}) 71 | perform_validation = options[:validate] != false 72 | perform_validation ? valid?(options[:context]) : true 73 | end 74 | end 75 | end 76 | 77 | require "spira/validations/uniqueness" 78 | -------------------------------------------------------------------------------- /lib/spira/validations/uniqueness.rb: -------------------------------------------------------------------------------- 1 | module Spira 2 | module Validations 3 | class UniquenessValidator < ActiveModel::EachValidator 4 | # Unfortunately, we have to tie Uniqueness validators to a class. 5 | # Note: the `setup` hook has been deprecated in rails 4.1 and completely 6 | # removed in rails 4.2; the klass is now found in #{options[:class]}. 7 | if ActiveModel::VERSION::MAJOR <= 3 || 8 | (ActiveModel::VERSION::MAJOR == 4 && ActiveModel::VERSION::MINOR < 1) 9 | def setup(klass) 10 | @klass = klass 11 | end 12 | else 13 | def initialize(options) 14 | super 15 | @klass = options.fetch(:class) 16 | end 17 | end 18 | 19 | def validate_each(record, attribute, value) 20 | @klass.find_each(conditions: {attribute => value}) do |other_record| 21 | if other_record.subject != record.subject 22 | record.errors.add(attribute, "is already taken") 23 | break 24 | end 25 | end 26 | end 27 | end 28 | 29 | module ClassMethods 30 | # Validates whether the value of the specified attributes are unique across the system. 31 | # Useful for making sure that only one user 32 | # can be named "davidhh". 33 | # 34 | # class Person < Spira::Base 35 | # validates_uniqueness_of :user_name 36 | # end 37 | # 38 | def validates_uniqueness_of(*attr_names) 39 | validates_with UniquenessValidator, _merge_attributes(attr_names) 40 | end 41 | end 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /lib/spira/version.rb: -------------------------------------------------------------------------------- 1 | module Spira 2 | module VERSION 3 | VERSION_FILE = File.join(File.expand_path(File.dirname(__FILE__)), "..", "..", "VERSION") 4 | MAJOR, MINOR, TINY, EXTRA = File.read(VERSION_FILE).split(".") 5 | 6 | STRING = [MAJOR, MINOR, TINY, EXTRA].compact.join('.') 7 | 8 | ## 9 | # @return [String] 10 | def self.to_s() STRING end 11 | 12 | ## 13 | # @return [String] 14 | def self.to_str() STRING end 15 | 16 | ## 17 | # @return [Array(Integer, Integer, Integer)] 18 | def self.to_a() [MAJOR, MINOR, TINY] end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /spec/attributes_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe "RDF::Resource attributes" do 4 | let(:person) do 5 | Spira.repository = RDF::Repository.new 6 | 7 | class Person < Spira::Base 8 | property :name, predicate: RDF::RDFS.label 9 | has_many :friends, predicate: RDF::Vocab::FOAF.knows, type: :Person 10 | end 11 | 12 | friend = Person.new(name: "Dick") 13 | friend.save! 14 | 15 | p = Person.new(name: "Charlie") 16 | p.friends << friend 17 | p.save! 18 | end 19 | 20 | describe "#reload" do 21 | it "should reload single-value attributes from the repository" do 22 | person.name = "Jennifer" 23 | 24 | expect { 25 | person.reload 26 | }.to change(person, :name).from("Jennifer").to("Charlie") 27 | end 28 | 29 | it "should reload list attributes from the repository" do 30 | friend = Person.new(name: "Bob") 31 | friend.save! 32 | person.friends << friend 33 | 34 | expect(person.friends.length).to eql 2 35 | 36 | person.reload 37 | expect(person.friends.length).to eql 1 38 | end 39 | end 40 | 41 | context "when assigning a value to a non-existing property" do 42 | context "via #update_attributes" do 43 | it "should raise a NoMethodError" do 44 | expect { 45 | person.update_attributes({nonexisting_attribute: 0}) 46 | }.to raise_error NoMethodError 47 | end 48 | end 49 | 50 | context "via #write_attribute" do 51 | it "should raise a Spira::PropertyMissingError" do 52 | expect { 53 | person.send :write_attribute, :nonexisting_attribute, 0 54 | }.to raise_error Spira::PropertyMissingError 55 | end 56 | end 57 | end 58 | 59 | end 60 | -------------------------------------------------------------------------------- /spec/base_uri_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe 'Default URIs' do 4 | 5 | before(:each) {Spira.repository = RDF::Repository.new} 6 | 7 | context "classes with a base URI" do 8 | 9 | before :all do 10 | class ::BaseURITest < Spira::Base 11 | configure base_uri: "http://example.org/example" 12 | property :name, predicate: RDF::RDFS.label 13 | end 14 | 15 | class ::HashBaseURITest < Spira::Base 16 | configure base_uri: "http://example.org/example#" 17 | property :name, predicate: RDF::RDFS.label 18 | end 19 | end 20 | 21 | it "have a base URI method" do 22 | expect(BaseURITest).to respond_to :base_uri 23 | end 24 | 25 | it "have a correct base URI" do 26 | expect(BaseURITest.base_uri).to eql "http://example.org/example" 27 | end 28 | 29 | it "provide an id_for method" do 30 | expect(BaseURITest).to respond_to :id_for 31 | end 32 | 33 | it "provide a uri based on the base URI for string arguments" do 34 | expect(BaseURITest.id_for('bob')).to eql RDF::URI.new('http://example.org/example/bob') 35 | end 36 | 37 | it "use the string form of an absolute URI as an absolute URI" do 38 | uri = 'http://example.org/example/bob' 39 | expect(BaseURITest.id_for(uri)).to eql RDF::URI.new(uri) 40 | end 41 | 42 | it "allow any type to be used as a URI fragment, via to_s" do 43 | uri = 'http://example.org/example/5' 44 | expect(BaseURITest.id_for(5)).to eql RDF::URI.new(uri) 45 | end 46 | 47 | it "allow appending fragment RDF::URIs to base_uris" do 48 | expect(BaseURITest.for(RDF::URI('test')).subject.to_s).to eql 'http://example.org/example/test' 49 | end 50 | 51 | it "do not raise an exception to project with a relative URI" do 52 | expect {x = BaseURITest.for 'bob'}.not_to raise_error 53 | end 54 | 55 | it "return an absolute, correct RDF::URI from #uri when created with a relative uri" do 56 | test = BaseURITest.for('bob') 57 | expect(test.uri).to be_a RDF::URI 58 | expect(test.uri.to_s).to eql "http://example.org/example/bob" 59 | end 60 | 61 | it "save objects created with a relative URI as absolute URIs" do 62 | test = BaseURITest.for('bob') 63 | test.name = 'test' 64 | test.save! 65 | saved = BaseURITest.for('bob') 66 | expect(saved.name).to eql 'test' 67 | end 68 | 69 | it "do not append a / if the base URI ends with a #" do 70 | expect(HashBaseURITest.id_for('bob')).to eql RDF::URI.new('http://example.org/example#bob') 71 | end 72 | end 73 | 74 | context "classes without a base URI" do 75 | before :all do 76 | class ::NoBaseURITest < Spira::Base 77 | property :name, predicate: RDF::RDFS.label 78 | end 79 | end 80 | 81 | it "have a base URI method" do 82 | expect(NoBaseURITest).to respond_to :base_uri 83 | end 84 | 85 | it "provide a id_for method" do 86 | expect(NoBaseURITest).to respond_to :id_for 87 | end 88 | 89 | it "have a nil base_uri" do 90 | expect(NoBaseURITest.base_uri).to be_nil 91 | end 92 | 93 | it "raise an ArgumentError when projected with a relative URI" do 94 | expect { x = NoBaseURITest.id_for('bob')}.to raise_error ArgumentError 95 | end 96 | end 97 | 98 | end 99 | -------------------------------------------------------------------------------- /spec/basic_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | # Tests of basic functionality--getting, setting, creating, saving, when no 4 | # relations or anything fancy are involved. 5 | 6 | describe Spira do 7 | 8 | before :all do 9 | class ::Person < Spira::Base 10 | configure base_uri: "http://example.org/example/people" 11 | property :name, predicate: RDF::RDFS.label 12 | property :age, predicate: RDF::Vocab::FOAF.age, type: Integer 13 | end 14 | 15 | class Employee < Spira::Base 16 | property :name, predicate: RDF::RDFS.label 17 | property :age, predicate: RDF::Vocab::FOAF.age, type: Integer 18 | end 19 | end 20 | 21 | context "with repository given" do 22 | let(:person_repository) {RDF::Repository.load(fixture('bob.nt'))} 23 | before(:each) {Spira.repository = person_repository} 24 | 25 | context "The person fixture" do 26 | 27 | it "should know its source" do 28 | expect(Person.repository).to be_a RDF::Repository 29 | expect(Person.repository).to equal person_repository 30 | end 31 | 32 | context "when instantiating new URIs" do 33 | 34 | it "should offer a for method" do 35 | expect(Person).to respond_to :for 36 | end 37 | 38 | it "should be able to create new instances for non-existing resources" do 39 | expect { Person.for(RDF::URI.new('http://example.org/newperson')) }.not_to raise_error 40 | end 41 | 42 | it "should create Person instances" do 43 | expect(Person.for(RDF::URI.new('http://example.org/newperson'))).to be_a Person 44 | end 45 | 46 | context "with attributes given" do 47 | let(:alice) {Person.for 'alice', age: 30, name: 'Alice'} 48 | 49 | it "should have properties if it had them as attributes on creation" do 50 | expect(alice.age).to eql 30 51 | expect(alice.name).to eql 'Alice' 52 | end 53 | 54 | it "should save updated properties" do 55 | alice.age = 16 56 | expect(alice.age).to eql 16 57 | end 58 | 59 | end 60 | end 61 | 62 | context "when instantiating existing URIs" do 63 | 64 | it "should return a Person for a non-existent URI" do 65 | expect(Person.for('nobody')).to be_a Person 66 | end 67 | 68 | it "should return an empty Person for a non-existent URI" do 69 | person = Person.for('nobody') 70 | expect(person.age).to be_nil 71 | expect(person.name).to be_nil 72 | end 73 | 74 | end 75 | 76 | context "with attributes given" do 77 | let(:bob) {Person.for 'bob', name: 'Bob Smith II'} 78 | 79 | it "should overwrite existing properties with given attributes" do 80 | expect(bob.name).to eql "Bob Smith II" 81 | end 82 | 83 | it "should not overwrite existing properties which are not given" do 84 | expect(bob.age).to eql 15 85 | end 86 | 87 | it "should allow property updating" do 88 | bob.age = 16 89 | expect(bob.age).to eql 16 90 | end 91 | end 92 | 93 | context "A newly-created person" do 94 | let(:person) {Person.for 'http://example.org/example/people/alice'} 95 | 96 | context "in respect to some general methods" do 97 | it "should #uri" do 98 | expect(person).to respond_to :uri 99 | end 100 | 101 | it "should return a RDF::URI from #uri" do 102 | expect(person.uri).to be_a RDF::URI 103 | end 104 | 105 | it "should return the correct URI from #uri" do 106 | expect(person.uri.to_s).to eql 'http://example.org/example/people/alice' 107 | end 108 | 109 | it "should support #to_uri" do 110 | expect(person).to respond_to :to_uri 111 | end 112 | 113 | it "should return the correct URI from #to_uri" do 114 | expect(person.to_uri.to_s).to eql 'http://example.org/example/people/alice' 115 | end 116 | 117 | it "should support #to_rdf" do 118 | expect(person).to respond_to :to_rdf 119 | end 120 | 121 | it "should return an RDF::Enumerable for #to_rdf" do 122 | expect(person.to_rdf).to be_a RDF::Enumerable 123 | end 124 | end 125 | 126 | context "using getters and setters" do 127 | it "should have a name method" do 128 | expect(person).to respond_to :name 129 | end 130 | 131 | it "should have an age method" do 132 | expect(person).to respond_to :age 133 | end 134 | 135 | it "should return nil for unset properties" do 136 | expect(person.name).to eql nil 137 | end 138 | 139 | it "should allow setting a name" do 140 | expect { person.name = "Bob Smith" }.not_to raise_error 141 | end 142 | 143 | it "should allow getting a name" do 144 | person.name = "Bob Smith" 145 | expect(person.name).to eql "Bob Smith" 146 | end 147 | 148 | it "should allow setting an age" do 149 | expect { person.age = 15 }.not_to raise_error 150 | end 151 | 152 | it "should allow getting an age" do 153 | person.age = 15 154 | expect(person.age).to eql 15 155 | end 156 | 157 | it "should correctly set more than one property" do 158 | person.age = 15 159 | person.name = "Bob Smith" 160 | expect(person.age).to eql 15 161 | expect(person.name).to eql "Bob Smith" 162 | end 163 | end 164 | end 165 | end 166 | end 167 | 168 | context "without a repository" do 169 | before { Spira.clear_repository! } 170 | 171 | let(:bob) { RDF::URI("http://example.org/example/people/bob").as(Person) } 172 | 173 | context "when saved" do 174 | it "should raise NoRepositoryError exception" do 175 | expect { bob.save }.to raise_exception Spira::NoRepositoryError 176 | end 177 | end 178 | end 179 | end 180 | -------------------------------------------------------------------------------- /spec/dirty_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe Spira do 4 | 5 | before :all do 6 | class ::DirtyTest < Spira::Base 7 | property :name, predicate: RDF::RDFS.label 8 | property :age, predicate: RDF::Vocab::FOAF.age, type: Integer 9 | has_many :items, predicate: RDF::RDFS.seeAlso 10 | end 11 | end 12 | 13 | let(:uri) {RDF::URI("http://example.org/example/people/alice")} 14 | before :each do 15 | Spira.repository = RDF::Repository.new do |repo| 16 | repo << RDF::Statement.new(uri, RDF::RDFS.label, "Alice") 17 | repo << RDF::Statement.new(uri, RDF::Vocab::FOAF.age, 15) 18 | repo << RDF::Statement.new(uri, RDF::RDFS.seeAlso, "A Literal") 19 | repo << RDF::Statement.new(uri, RDF::RDFS.seeAlso, "Another Literal") 20 | end 21 | end 22 | 23 | context "when tracking dirty attributes" do 24 | subject {DirtyTest.for(uri)} 25 | 26 | it {is_expected.not_to be_changed} 27 | 28 | context "that are properties" do 29 | 30 | it "should not mark attributes as dirty when loading" do 31 | expect(subject.changed_attributes).not_to include("name") 32 | expect(subject.changed_attributes).not_to include("age") 33 | end 34 | 35 | it "should mark the projection as dirty if an attribute is dirty" do 36 | subject.name = "Steve" 37 | is_expected.to be_changed 38 | end 39 | 40 | it "should mark attributes as dirty when changed" do 41 | subject.name = "Steve" 42 | expect(subject.changed_attributes).to include("name") 43 | expect(subject.changed_attributes).not_to include("age") 44 | end 45 | 46 | it "should mark attributes as dirty when providing them as arguments" do 47 | expect(subject.changed_attributes).not_to include("name") 48 | expect(subject.changed_attributes).not_to include("age") 49 | end 50 | end 51 | 52 | context "that are lists" do 53 | its(:changed_attributes) {is_expected.not_to include("items")} 54 | 55 | it "should mark the projection as dirty if an attribute is dirty" do 56 | subject.items = ["Steve"] 57 | expect(subject.changed_attributes).to include("items") 58 | end 59 | 60 | it "should mark attributes as dirty when changed" do 61 | subject.items = ["Steve"] 62 | expect(subject.changed_attributes).to include("items") 63 | expect(subject.changed_attributes).not_to include("age") 64 | end 65 | 66 | it "should not mark attributes as dirty when providing them as arguments" do 67 | expect(subject.changed_attributes).not_to include("items") 68 | expect(subject.changed_attributes).not_to include("age") 69 | end 70 | 71 | it "should mark attributes as dirty when updated" do 72 | # TODO: a fix is pending for this, read comments on #persist! method 73 | pending "ActiveModel::Dirty cannot track that - read its docs" 74 | subject.items << "Steve" 75 | expect(subject.changed_attributes).to include(:items) 76 | end 77 | 78 | end 79 | end 80 | 81 | end 82 | -------------------------------------------------------------------------------- /spec/enumerable_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | # Tests in terms of RDF::Enumerable, and interaction with other enumerables 4 | 5 | describe Spira::Base do 6 | 7 | before :all do 8 | Spira.repository = ::RDF::Repository.new 9 | 10 | class ::EnumerableSpec < Spira::Base 11 | configure base_uri: "http://example.org/example/people" 12 | 13 | property :name, predicate: RDF::RDFS.label 14 | property :age, predicate: RDF::Vocab::FOAF.age, type: Integer 15 | end 16 | 17 | class ::EnumerableWithAssociationsSpec < Spira::Base 18 | configure base_uri: "http://example.org/example/people" 19 | 20 | property :name, predicate: RDF::RDFS.label 21 | has_many :friends, predicate: RDF::Vocab::FOAF.knows, type: :EnumerableWithAssociationsSpec 22 | end 23 | end 24 | 25 | context "as an RDF::Enumerable" do 26 | let(:uri) {RDF::URI('http://example.org/example/people/bob')} 27 | let(:person) do 28 | p = EnumerableSpec.for uri 29 | p.name = "Bob Smith" 30 | p.age = 15 31 | p 32 | end 33 | let(:enumerable_repository) do 34 | RDF::Repository.new do |repo| 35 | repo << RDF::Statement.new(uri, RDF::Vocab::FOAF.age, 15) 36 | repo << RDF::Statement.new(uri, RDF::Vocab::RDFS.label, "Bob Smith") 37 | end 38 | end 39 | 40 | context "when just created" do 41 | subject {EnumerableWithAssociationsSpec.new} 42 | its(:statements) {is_expected.to be_empty} 43 | end 44 | 45 | context "when has has_many association" do 46 | subject { EnumerableWithAssociationsSpec.for RDF::URI('http://example.org/example/people/charlie') } 47 | before do 48 | 3.times { subject.friends << EnumerableWithAssociationsSpec.new } 49 | end 50 | 51 | it "should list associated statements individually" do 52 | expect(subject.statements.size).to eql subject.friends.size 53 | end 54 | end 55 | 56 | # @see lib/rdf/spec/enumerable.rb in rdf-spec 57 | it_behaves_like 'an RDF::Enumerable' do 58 | before(:each) do 59 | @rdf_enumerable_iv_statements = enumerable_repository 60 | end 61 | let(:enumerable) { person } 62 | end 63 | 64 | context "when comparing with other RDF::Enumerables" do 65 | 66 | it "should be equal if they are completely the same" do 67 | expect(person).to eq enumerable_repository 68 | end 69 | 70 | # This one is a tough call. Are two resources really equal if one is a 71 | # subset of the other? No. But Spira is supposed to let you access 72 | # existing data, and that means you might have data which has properties 73 | # a model class doesn't know about. 74 | # 75 | # Spira will default, then, to calling itself equal to an enumerable 76 | # which has the same uri and all the same properties, and then some. 77 | it "should be equal if the resource is a subgraph of the repository" do 78 | pending "Awaiting subgraph implementation in rdf_isomorphic" 79 | fail 80 | end 81 | 82 | it "should allow other enumerables to be isomorphic to a resource" do 83 | expect(enumerable_repository.statements).to be_isomorphic_with person 84 | end 85 | end 86 | end 87 | end 88 | -------------------------------------------------------------------------------- /spec/fixtures/bob.nt: -------------------------------------------------------------------------------- 1 | "15"^^ . 2 | "Bob Smith" . 3 | 4 | "15.00"^^ . 5 | "Bad Age" . 6 | -------------------------------------------------------------------------------- /spec/fixtures/has_many.nt: -------------------------------------------------------------------------------- 1 | "Test post 1" . 2 | "some content" . 3 | 4 | 5 | 6 | 7 | . 8 | "test comment 1" . 9 | "some comment content" . 10 | "1"^^ . 11 | "3"^^ . 12 | "5"^^ . 13 | . 14 | 15 | 16 | . 17 | "test comment 2" . 18 | "some comment content" . 19 | -------------------------------------------------------------------------------- /spec/fixtures/localized.nt: -------------------------------------------------------------------------------- 1 | "COMPANY" . 2 | "société"@fr . 3 | "company"@en . -------------------------------------------------------------------------------- /spec/fixtures/non_model_data.nt: -------------------------------------------------------------------------------- 1 | "a name" . 2 | "another name" . 3 | "15"^^ . 4 | "Not in the model" . 5 | 6 | "a name" . 7 | "another name" . 8 | "15"^^ . 9 | "20"^^ . 10 | -------------------------------------------------------------------------------- /spec/fixtures/relations.nt: -------------------------------------------------------------------------------- 1 | "Nirvana" . 2 | 3 | 4 | 5 | "Nevermind" . 6 | . 7 | 8 | "In Utero" . 9 | . 10 | -------------------------------------------------------------------------------- /spec/fixtures/types.nt: -------------------------------------------------------------------------------- 1 | . 2 | . 3 | "sedan" . 4 | 5 | "sports car" . 6 | 7 | . 8 | "minivan" . 9 | 10 | . 11 | "full-sized van" . 12 | 13 | . 14 | "some other van" . 15 | -------------------------------------------------------------------------------- /spec/has_many_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | class Posts < RDF::Vocabulary('http://example.org/posts/predicates/') 4 | property :rating 5 | end 6 | 7 | describe "has_many" do 8 | 9 | before :all do 10 | class ::Post < Spira::Base 11 | type RDF::URI.new('http://rdfs.org/sioc/types#Post') 12 | 13 | has_many :comments, predicate: RDF::Vocab::SIOC.has_reply, type: :Comment 14 | property :title, predicate: RDF::Vocab::DC.title 15 | property :body, predicate: RDF::Vocab::SIOC.content 16 | end 17 | 18 | class ::Comment < Spira::Base 19 | type RDF::URI.new('http://rdfs.org/sioc/types#Comment') 20 | 21 | property :post, predicate: RDF::Vocab::SIOC.reply_of, type: :Post 22 | property :title, predicate: RDF::Vocab::DC.title 23 | property :body, predicate: RDF::Vocab::SIOC.content 24 | has_many :ratings, predicate: Posts.rating, type: Integer 25 | end 26 | end 27 | 28 | context "Comment class basics" do 29 | let(:url) {RDF::URI.new('http://example.org/comments/comment1')} 30 | let(:comment) {Comment.for url} 31 | let(:empty_comment) {Comment.for RDF::URI.new('http://example.org/comments/comment0')} 32 | before {Spira.repository = RDF::Repository.load(fixture('has_many.nt'))} 33 | 34 | it "should have a ratings method" do 35 | expect(comment).to respond_to :ratings 36 | end 37 | 38 | it "should having a ratings= method" do 39 | expect(comment).to respond_to :ratings= 40 | end 41 | 42 | it "should report that ratings is an association" do 43 | expect(Comment.reflect_on_association(:ratings)).to be_a AssociationReflection 44 | end 45 | 46 | it "should report that bodies are not a list" do 47 | expect(Comment.reflect_on_association(:body)).to be_nil 48 | end 49 | 50 | it "should return an empty array of ratings for comments with none" do 51 | expect(empty_comment.ratings).to eql [] 52 | end 53 | 54 | it "should return a set of ratings for comments with some" do 55 | expect(comment.ratings).to be_a Array 56 | expect(comment.ratings.size).to eql 3 57 | expect(comment.ratings.sort).to eql [1,3,5] 58 | end 59 | 60 | it "should allow setting and saving non-array elements" do 61 | comment.title = 'test' 62 | expect(comment.title).to eql 'test' 63 | comment.save! 64 | expect(comment.title).to eql 'test' 65 | end 66 | 67 | it "should allow setting on array elements" do 68 | comment.ratings = [1,2,4] 69 | comment.save! 70 | expect(comment.ratings.sort).to eql [1,2,4] 71 | end 72 | 73 | it "should allow saving array elements" do 74 | comment.ratings = [1,2,4] 75 | expect(comment.ratings.sort).to eql [1,2,4] 76 | comment.save! 77 | expect(comment.ratings.sort).to eql [1,2,4] 78 | comment = Comment.for url 79 | expect(comment.ratings.sort).to eql [1,2,4] 80 | end 81 | 82 | it "should allow appending to array elements" do 83 | comment.ratings << 6 84 | expect(comment.ratings.sort).to eql [1,3,5,6] 85 | comment.save! 86 | expect(comment.ratings.sort).to eql [1,3,5,6] 87 | end 88 | 89 | it "should allow saving of appended elements" do 90 | comment.ratings << 6 91 | comment.save! 92 | comment = Comment.for url 93 | expect(comment.ratings.sort).to eql [1,3,5,6] 94 | end 95 | end 96 | 97 | context "Post class basics" do 98 | before :each do 99 | Spira.repository = RDF::Repository.load(fixture('has_many.nt')) 100 | end 101 | 102 | let(:post) {Post.for RDF::URI.new('http://example.org/posts/post1')} 103 | let(:empty_post) {Post.for RDF::URI.new('http://example.org/posts/post0')} 104 | let(:empty_comment) {Comment.for RDF::URI.new('http://example.org/comments/comment0')} 105 | 106 | it "should have a comments method" do 107 | expect(post).to respond_to :comments 108 | end 109 | 110 | it "should have a comments= method" do 111 | expect(post).to respond_to :comments= 112 | end 113 | 114 | it "should return an empty array from comments for an object with none" do 115 | expect(empty_post.comments).to eql [] 116 | end 117 | 118 | it "should return an array of comments for an object with some" do 119 | expect(post.comments.size).to eql 2 120 | post.comments.each do |comment| 121 | expect(comment).to be_a Comment 122 | end 123 | end 124 | 125 | it "should allow setting and saving non-array elements" do 126 | post.title = "test post title" 127 | post.save! 128 | expect(post.title).to eql 'test post title' 129 | end 130 | 131 | it "should allow setting array elements" do 132 | post.comments = (post.comments + [empty_comment]) 133 | expect(post.comments.size).to eql 3 134 | expect(post.comments).to include empty_comment 135 | end 136 | 137 | it "should allow saving array elements" do 138 | comments = post.comments + [empty_comment] 139 | post.comments = (post.comments + [empty_comment]) 140 | expect(post.comments.size).to eql 3 141 | post.save! 142 | expect(post.comments.size).to eql 3 143 | post.comments.each do |comment| 144 | expect(comments).to include comment 145 | end 146 | end 147 | 148 | context "given all associations have a base_uri" do 149 | before do 150 | Post.class_eval { 151 | configure base_uri: "http://example.org/posts" 152 | } 153 | 154 | Comment.class_eval { 155 | configure base_uri: "http://example.org/comments" 156 | } 157 | end 158 | 159 | it "should assign comments by their IDs" do 160 | cids = post.comment_ids.first 161 | post.comment_ids = [cids, ""] 162 | 163 | expect(post.comment_ids).to eql [cids] 164 | end 165 | end 166 | end 167 | end 168 | -------------------------------------------------------------------------------- /spec/hooks_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe 'Spira resources' do 4 | 5 | before :all do 6 | class ::HookTest < ::Spira::Base 7 | property :name, predicate: RDF::Vocab::FOAF.name 8 | property :age, predicate: RDF::Vocab::FOAF.age 9 | end 10 | end 11 | subject {RDF::URI.intern('http://example.org/test')} 12 | let(:repository) do 13 | RDF::Repository.new do |repo| 14 | repo << RDF::Statement.new(subject, RDF::Vocab::FOAF.name, "A name") 15 | end 16 | end 17 | 18 | before {Spira.repository = repository} 19 | 20 | context "with a before_create method" do 21 | before :all do 22 | class ::BeforeCreateTest < ::HookTest 23 | type RDF::Vocab::FOAF.Person 24 | before_create :update_name 25 | 26 | def update_name 27 | self.name = "Everyone has this name" 28 | end 29 | end 30 | 31 | class ::BeforeCreateWithoutTypeTest < ::HookTest 32 | def before_create 33 | self.name = "Everyone has this name" 34 | end 35 | end 36 | end 37 | 38 | before {repository << RDF::Statement.new(subject, RDF.type, RDF::Vocab::FOAF.Person)} 39 | 40 | it "calls the before_create method before saving a resouce for the first time" do 41 | test = RDF::URI('http://example.org/new').as(::BeforeCreateTest) 42 | test.save 43 | expect(test.name).to eql "Everyone has this name" 44 | expect(repository).to have_statement RDF::Statement.new(test.subject, RDF::Vocab::FOAF.name, "Everyone has this name") 45 | end 46 | 47 | it "does not call the before_create method if the resource previously existed" do 48 | test = subject.as(::BeforeCreateTest) 49 | test.save 50 | expect(test.name).to eql "A name" 51 | expect(repository).to have_statement RDF::Statement.new(test.subject, RDF::Vocab::FOAF.name, "A name") 52 | expect(repository).not_to have_statement RDF::Statement.new(test.subject, RDF::Vocab::FOAF.name, "Everyone has this name") 53 | end 54 | 55 | it "does not call the before_create method without a type declaration" do 56 | test = RDF::URI('http://example.org/new').as(::BeforeCreateWithoutTypeTest) 57 | test.save 58 | expect(repository).not_to have_statement RDF::Statement.new(test.subject, RDF::Vocab::FOAF.name, "Everyone has this name") 59 | end 60 | end 61 | 62 | context "with an after_create method" do 63 | before :all do 64 | class ::AfterCreateTest < ::HookTest 65 | type RDF::Vocab::FOAF.Person 66 | after_create :update_name 67 | 68 | def update_name 69 | self.name = "Everyone has this unsaved name" 70 | end 71 | end 72 | 73 | class ::AfterCreateWithoutTypeTest < ::HookTest 74 | after_create :update_name 75 | 76 | def update_name 77 | self.name = "Everyone has this unsaved name" 78 | end 79 | end 80 | end 81 | 82 | before {repository << RDF::Statement.new(subject, RDF.type, RDF::Vocab::FOAF.Person)} 83 | 84 | it "calls the after_create method after saving a resource for the first time" do 85 | test = RDF::URI('http://example.org/new').as(::AfterCreateTest) 86 | test.save 87 | expect(test.name).to eql "Everyone has this unsaved name" 88 | expect(repository).not_to have_statement RDF::Statement.new(test.subject, RDF::Vocab::FOAF.name, "Everyone has this name") 89 | end 90 | 91 | it "does not call after_create if the resource previously existed" do 92 | test = subject.as(::AfterCreateTest) 93 | test.save 94 | expect(test.name).to eql "A name" 95 | expect(repository).to have_statement RDF::Statement.new(test.subject, RDF::Vocab::FOAF.name, "A name") 96 | expect(repository).not_to have_statement RDF::Statement.new(test.subject, RDF::Vocab::FOAF.name, "Everyone has this name") 97 | end 98 | 99 | it "does not call the after_create method without a type declaration" do 100 | test = RDF::URI('http://example.org/new').as(::AfterCreateWithoutTypeTest) 101 | test.save 102 | expect(test.name).to be_nil 103 | end 104 | end 105 | 106 | context "with an after_update method" do 107 | 108 | before :all do 109 | class ::AfterUpdateTest < ::HookTest 110 | after_update :update_age 111 | 112 | def update_age 113 | self.age = 15 114 | end 115 | end 116 | end 117 | 118 | it "calls the after_update method after updating a field" do 119 | test = subject.as(AfterUpdateTest) 120 | expect(test.age).to be_nil 121 | test.update_attributes(name: "A new name") 122 | expect(test.age).to eql 15 123 | end 124 | 125 | it "does not call the after_update method after simply setting a field" do 126 | test = subject.as(AfterUpdateTest) 127 | expect(test.age).to be_nil 128 | test.name = "a new name" 129 | expect(test.age).to be_nil 130 | end 131 | end 132 | 133 | context "with a before_save method" do 134 | before :all do 135 | class ::BeforeSaveTest < ::HookTest 136 | before_save :update_age 137 | 138 | def update_age 139 | self.age = 15 140 | end 141 | end 142 | end 143 | 144 | it "calls the before_save method before saving" do 145 | test = subject.as(::BeforeSaveTest) 146 | expect(test.age).to be_nil 147 | test.save 148 | expect(test.age).to eql 15 149 | expect(repository).to have_statement RDF::Statement(subject, RDF::Vocab::FOAF.age, 15) 150 | end 151 | end 152 | 153 | context "with an after_save method" do 154 | 155 | before :all do 156 | class ::AfterSaveTest < ::HookTest 157 | after_save :update_age 158 | 159 | def update_age 160 | self.age = 15 161 | end 162 | end 163 | end 164 | 165 | it "calls the after_save method after saving" do 166 | test = subject.as(::AfterSaveTest) 167 | expect(test.age).to be_nil 168 | test.save 169 | expect(test.age).to eql 15 170 | expect(repository).not_to have_statement RDF::Statement(subject, RDF::Vocab::FOAF.age, 15) 171 | end 172 | 173 | end 174 | 175 | context "with a before_destroy method" do 176 | before :all do 177 | class ::BeforeDestroyTest < ::HookTest 178 | before_destroy :cleanup 179 | 180 | def cleanup 181 | self.class.repository.delete(RDF::Statement.new(nil, RDF::Vocab::FOAF.nick,nil)) 182 | end 183 | end 184 | end 185 | 186 | before {repository << RDF::Statement.new(RDF::URI('http://example.org/new'), RDF::Vocab::FOAF.nick, "test")} 187 | 188 | it "calls the before_destroy method before destroying" do 189 | subject.as(::BeforeDestroyTest).destroy(:completely) 190 | expect(repository).to be_empty 191 | end 192 | end 193 | 194 | context "with an after_destroy method" do 195 | before :all do 196 | class ::AfterDestroyTest < ::HookTest 197 | after_destroy :cleanup 198 | 199 | def cleanup 200 | self.class.repository.delete(RDF::Statement.new(nil, RDF::Vocab::FOAF.nick,nil)) 201 | raise Exception if self.class.repository.has_subject?(self.subject) 202 | end 203 | end 204 | end 205 | 206 | before {repository << RDF::Statement.new(RDF::URI('http://example.org/new'), RDF::Vocab::FOAF.nick, "test")} 207 | 208 | it "calls the after_destroy method after destroying" do 209 | # This would raise an exception if after_destroy were called before deleting is confirmed 210 | expect { subject.as(::AfterDestroyTest).destroy(:completely) }.not_to raise_error 211 | # This one makes sure that after_destory got called at all 212 | expect(repository).not_to have_predicate RDF::Vocab::FOAF.nick 213 | end 214 | end 215 | 216 | context "when the hook methods are private" do 217 | before :all do 218 | class Counter 219 | class << self 220 | attr_accessor :called_methods 221 | end 222 | self.called_methods = Set.new 223 | end 224 | 225 | class ::PrivateHookTest < ::HookTest 226 | type RDF::Vocab::FOAF.Person 227 | 228 | before_create :add_bc_counter 229 | after_create :add_ac_counter 230 | 231 | before_save :add_bs_counter 232 | after_save :add_as_counter 233 | 234 | before_destroy :add_bd_counter 235 | after_destroy :add_ad_counter 236 | 237 | before_update :add_bu_counter 238 | after_update :add_au_counter 239 | 240 | private 241 | 242 | def add_counter(name) 243 | Counter.called_methods << name.to_s 244 | end 245 | def add_bc_counter 246 | add_counter(__method__) 247 | end 248 | def add_ac_counter 249 | add_counter(__method__) 250 | end 251 | def add_bs_counter 252 | add_counter(__method__) 253 | end 254 | def add_as_counter 255 | add_counter(__method__) 256 | end 257 | def add_bd_counter 258 | add_counter(__method__) 259 | end 260 | def add_ad_counter 261 | add_counter(__method__) 262 | end 263 | def add_bu_counter 264 | add_counter(__method__) 265 | end 266 | def add_au_counter 267 | add_counter(__method__) 268 | end 269 | end 270 | end 271 | 272 | before {repository << RDF::Statement.new(subject, RDF.type, RDF::Vocab::FOAF.Person)} 273 | 274 | it "should call the hook methods" do 275 | subject = RDF::URI.new('http://example.org/test1').as(::PrivateHookTest) 276 | 277 | subject.save 278 | subject.update_attributes(name: "Jay") 279 | subject.destroy 280 | 281 | expect(Counter.called_methods).to include "add_bc_counter" 282 | expect(Counter.called_methods).to include "add_ac_counter" 283 | expect(Counter.called_methods).to include "add_bu_counter" 284 | expect(Counter.called_methods).to include "add_au_counter" 285 | expect(Counter.called_methods).to include "add_bs_counter" 286 | expect(Counter.called_methods).to include "add_as_counter" 287 | expect(Counter.called_methods).to include "add_bd_counter" 288 | expect(Counter.called_methods).to include "add_ad_counter" 289 | end 290 | end 291 | end 292 | -------------------------------------------------------------------------------- /spec/inheritance_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe Spira do 4 | 5 | context "inheritance" do 6 | 7 | before :all do 8 | class ::InheritanceItem < Spira::Base 9 | property :title, predicate: RDF::Vocab::DC.title, type: String 10 | has_many :subtitle, predicate: RDF::Vocab::DC.description, type: String 11 | type RDF::Vocab::SIOC.Item 12 | end 13 | 14 | class ::InheritancePost < ::InheritanceItem 15 | type RDF::Vocab::SIOC.Post 16 | property :creator, predicate: RDF::Vocab::DC.creator 17 | property :subtitle, predicate: RDF::Vocab::DC.description, type: String 18 | end 19 | 20 | class ::InheritedType < ::InheritanceItem 21 | end 22 | 23 | class ::InheritanceForumPost < ::InheritancePost 24 | end 25 | 26 | class ::InheritanceContainer < Spira::Base 27 | type RDF::Vocab::SIOC.Container 28 | has_many :items, type: 'InheritanceItem', predicate: RDF::Vocab::SIOC.container_of 29 | end 30 | 31 | class ::InheritanceForum < ::InheritanceContainer 32 | type RDF::Vocab::SIOC.Forum 33 | #property :moderator, predicate: RDF::Vocab::SIOC.has_moderator 34 | end 35 | end 36 | 37 | context "when redeclaring a property in a child" do 38 | let(:item) {RDF::URI('http://example.org/item').as(InheritanceItem)} 39 | let(:post) {RDF::URI('http://example.org/post').as(InheritancePost)} 40 | 41 | before {Spira.repository = RDF::Repository.new} 42 | 43 | it "should override the property on the child" do 44 | expect(post.subtitle).to be_nil 45 | expect(post).not_to respond_to(:subtitle_ids) 46 | end 47 | 48 | it "should not override the parent property" do 49 | expect(item.subtitle).to be_empty 50 | expect(item).to respond_to(:subtitle_ids) 51 | end 52 | end 53 | 54 | context "when passing properties to children" do 55 | let(:item) {RDF::URI('http://example.org/item').as(InheritanceItem)} 56 | let(:post) {RDF::URI('http://example.org/post').as(InheritancePost)} 57 | let(:type) {RDF::URI('http://example.org/type').as(InheritedType)} 58 | let(:forum) {RDF::URI('http://example.org/forum').as(InheritanceForumPost)} 59 | 60 | before {Spira.repository = RDF::Repository.new} 61 | 62 | it "should respond to a property getter" do 63 | expect(post).to respond_to :title 64 | end 65 | 66 | it "should respond to a property setter" do 67 | expect(post).to respond_to :title= 68 | end 69 | 70 | it "should respond to a propety getter on a grandchild class" do 71 | expect(forum).to respond_to :title 72 | end 73 | 74 | it "should respond to a propety setter on a grandchild class" do 75 | expect(forum).to respond_to :title= 76 | end 77 | 78 | it "should maintain property metadata" do 79 | expect(InheritancePost.properties).to have_key :title 80 | expect(InheritancePost.properties[:title][:type]).to eql Spira::Types::String 81 | end 82 | 83 | it "should add properties of child classes" do 84 | expect(post).to respond_to :creator 85 | expect(post).to respond_to :creator= 86 | expect(InheritancePost.properties).to have_key :creator 87 | end 88 | 89 | it "should allow setting a property" do 90 | post.title = "test title" 91 | expect(post.title).to eql "test title" 92 | end 93 | 94 | it "should inherit an RDFS type if one is not given" do 95 | expect(InheritedType.type).to eql RDF::Vocab::SIOC.Item 96 | end 97 | 98 | it "should overwrite the RDFS type if one is given" do 99 | expect(InheritancePost.type).to eql RDF::Vocab::SIOC.Post 100 | end 101 | 102 | it "should inherit an RDFS type from the most recent ancestor" do 103 | expect(InheritanceForumPost.type).to eql RDF::Vocab::SIOC.Post 104 | end 105 | 106 | it "should not define methods on parents" do 107 | expect(item).not_to respond_to :creator 108 | end 109 | 110 | it "should not modify the properties of the base class" do 111 | expect(Spira::Base.properties).to be_empty 112 | end 113 | 114 | context "when saving properties" do 115 | before :each do 116 | post.title = "test title" 117 | post.save! 118 | type.title = "type title" 119 | type.save! 120 | forum.title = "forum title" 121 | forum.save! 122 | end 123 | 124 | it "should save an edited property" do 125 | expect(InheritancePost.repository.query({subject: post.uri, predicate: RDF::Vocab::DC.title}).count).to eql 1 126 | end 127 | 128 | it "should save an edited property on a grandchild class" do 129 | expect(InheritanceForumPost.repository.query({subject: forum.uri, predicate: RDF::Vocab::DC.title}).count).to eql 1 130 | end 131 | 132 | it "should save the new type" do 133 | expect(InheritancePost.repository.query({subject: post.uri, predicate: RDF.type, object: RDF::Vocab::SIOC.Post}).count).to eql 1 134 | end 135 | 136 | it "should not save the supertype for a subclass which has specified one" do 137 | expect(InheritancePost.repository.query({subject: post.uri, predicate: RDF.type, object: RDF::Vocab::SIOC.Item}).to_a).to be_empty 138 | end 139 | 140 | it "should save the supertype for a subclass which has not specified one" do 141 | expect(InheritedType.repository.query({subject: type.uri, predicate: RDF.type, object: RDF::Vocab::SIOC.Item}).count).to eql 1 142 | end 143 | end 144 | end 145 | end 146 | 147 | describe "multitype classes" do 148 | before do 149 | class MultiTypeThing < Spira::Base 150 | type RDF::Vocab::SIOC.Item 151 | type RDF::Vocab::SIOC.Post 152 | end 153 | 154 | class InheritedMultiTypeThing < MultiTypeThing 155 | end 156 | 157 | class InheritedWithTypesMultiTypeThing < MultiTypeThing 158 | type RDF::Vocab::SIOC.Container 159 | end 160 | end 161 | 162 | it "should have multiple types" do 163 | types = Set.new [RDF::Vocab::SIOC.Item, RDF::Vocab::SIOC.Post] 164 | expect(MultiTypeThing.types).to eql types 165 | end 166 | 167 | it "should inherit multiple types" do 168 | expect(InheritedMultiTypeThing.types).to eql MultiTypeThing.types 169 | end 170 | 171 | it "should overwrite types" do 172 | types = Set.new << RDF::Vocab::SIOC.Container 173 | expect(InheritedWithTypesMultiTypeThing.types).to eql types 174 | end 175 | 176 | context "when saved" do 177 | before {Spira.repository = RDF::Repository.new} 178 | 179 | before do 180 | @thing = RDF::URI('http://example.org/thing').as(MultiTypeThing) 181 | @thing.save! 182 | end 183 | 184 | it "should store multiple classes" do 185 | expect(MultiTypeThing.repository.query({subject: @thing.uri, predicate: RDF.type, object: RDF::Vocab::SIOC.Item}).count).to eql 1 186 | expect(MultiTypeThing.repository.query({subject: @thing.uri, predicate: RDF.type, object: RDF::Vocab::SIOC.Post}).count).to eql 1 187 | end 188 | end 189 | end 190 | 191 | context "base classes" do 192 | before :all do 193 | class ::BaseChild < Spira::Base ; end 194 | end 195 | 196 | it "should have access to Spira DSL methods" do 197 | expect(BaseChild).to respond_to :property 198 | expect(BaseChild).to respond_to :base_uri 199 | expect(BaseChild).to respond_to :has_many 200 | expect(BaseChild).to respond_to :default_vocabulary 201 | end 202 | end 203 | end 204 | -------------------------------------------------------------------------------- /spec/instantiation_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe Spira do 4 | 5 | context "when instantiating" do 6 | 7 | before :all do 8 | class ::InstantiationTest < Spira::Base 9 | property :name, predicate: RDF::Vocab::FOAF.name 10 | end 11 | end 12 | 13 | context "when instantiating from a URI" do 14 | let(:uri) {RDF::URI('http://example.org/example')} 15 | before(:each) {Spira.repository = RDF::Repository.new} 16 | 17 | it "should add the 'as' method to RDF::URI" do 18 | expect(uri).to respond_to :as 19 | end 20 | 21 | it "should allow instantiation from a URI using RDF::URI#as" do 22 | expect(uri.as(InstantiationTest)).to be_a InstantiationTest 23 | end 24 | 25 | it "should yield the new instance to a block given to #as" do 26 | test = uri.as(InstantiationTest) do |test| 27 | test.name = "test name" 28 | end 29 | expect(test.name).to eql "test name" 30 | end 31 | 32 | it "should allow instantiation from a resource class using #for" do 33 | expect(InstantiationTest.for(uri)).to be_a InstantiationTest 34 | end 35 | 36 | it "should yield the new instance to a block given to #for" do 37 | test = InstantiationTest.for(uri) do |test| 38 | test.name = "test name" 39 | end 40 | expect(test.name).to eql "test name" 41 | end 42 | 43 | it "should allow instantiation from a URI with attributes given" do 44 | test = uri.as(InstantiationTest, name: "a name") 45 | expect(test.name).to eql "a name" 46 | end 47 | 48 | it "should know if a URI does not exist" do 49 | expect(InstantiationTest.for(uri)).not_to be_persisted 50 | end 51 | 52 | it "should know if a URI exists" do 53 | InstantiationTest.repository << RDF::Statement.new(uri, RDF::Vocab::FOAF.name, 'test') 54 | expect(InstantiationTest.for(uri)).to be_persisted 55 | end 56 | 57 | it "should allow the use of #[] as an alias to #for" do 58 | InstantiationTest.repository << RDF::Statement.new(uri, RDF::Vocab::FOAF.name, 'test') 59 | expect(InstantiationTest[uri]).to be_persisted 60 | end 61 | end 62 | 63 | context "when instantiating from a BNode" do 64 | let(:node) {RDF::Node.new} 65 | before {Spira.repository = RDF::Repository.new} 66 | 67 | it "should add the 'as' method to RDF::" do 68 | expect(node).to respond_to :as 69 | end 70 | 71 | it "should allow instantiation from a Node using RDF::Node#as" do 72 | expect(node.as(InstantiationTest)).to be_a InstantiationTest 73 | end 74 | 75 | it "should allow instantiation from a resource class using #for" do 76 | expect(InstantiationTest.for(node)).to be_a InstantiationTest 77 | end 78 | 79 | it "should allow instantiation from a Node with attributes given" do 80 | test = node.as(InstantiationTest, name: "a name") 81 | expect(test.name).to eql "a name" 82 | end 83 | 84 | it "should allow the use of #[] as an alias to #for" do 85 | expect(InstantiationTest[node]).to be_a InstantiationTest 86 | end 87 | end 88 | 89 | context "when creating without an identifier" do 90 | before {Spira.repository = RDF::Repository.new} 91 | 92 | it "should create an instance with a new Node identifier" do 93 | test = InstantiationTest.new 94 | expect(test.subject).to be_a RDF::Node 95 | expect(test.uri).to be_nil 96 | end 97 | 98 | it "should yield the new instance to a block given to #new" do 99 | test = InstantiationTest.new do |test| 100 | test.name = "test name" 101 | end 102 | expect(test.name).to eql "test name" 103 | end 104 | end 105 | 106 | end 107 | end 108 | -------------------------------------------------------------------------------- /spec/localized_attributes_spec.rb: -------------------------------------------------------------------------------- 1 | # encoding:utf-8 2 | require "spec_helper" 3 | 4 | # # Tests of basic functionality--getting, setting, creating, saving, when no 5 | # # relations or anything fancy are involved. 6 | 7 | describe Spira do 8 | 9 | class ::Concept < Spira::Base 10 | configure base_uri: "http://example.org/example/" 11 | property :label, predicate: RDF::RDFS.label, localized: true 12 | property :num_employees, predicate: RDF::URI.new('http://example.org/example/identifier') 13 | end 14 | 15 | let(:company) { 16 | ::Concept.for('http://example.org/example/company') 17 | } 18 | 19 | before {Spira.repository = RDF::Repository.load(fixture('localized.nt'))} 20 | 21 | context "with a localized property" do 22 | it "should have the default getter" do 23 | expect(company).to respond_to :label 24 | end 25 | 26 | it "should have a _native getter" do 27 | expect(company).to respond_to :label_native 28 | end 29 | 30 | it "should have a _with_locales getter" do 31 | expect(company).to respond_to :label_with_locales 32 | end 33 | 34 | describe "the default getter" do 35 | it "should take in account the current locale" do 36 | I18n.locale = :en 37 | expect(company.label).to eql "company" 38 | I18n.locale = :fr 39 | expect(company.label).to eql "société" 40 | end 41 | end 42 | 43 | describe "the _native getter" do 44 | it "should return all the labels" do 45 | expect(company.label_native.length).to eql 2 46 | end 47 | 48 | it "should return the labels as RDF Literals" do 49 | expect(company.label_native.first).to be_a(RDF::Literal) 50 | end 51 | end 52 | 53 | describe "the _with_locales getter" do 54 | it "should return a hash" do 55 | expect(company.label_with_locales).to be_a(Hash) 56 | end 57 | 58 | it "should contains all the locales" do 59 | expect(company.label_with_locales.keys).to include(:fr, :en) 60 | end 61 | 62 | it "should contains all the labels" do 63 | expect(company.label_with_locales[:fr]).to eql 'société' 64 | end 65 | end 66 | 67 | describe "the default setter" do 68 | it "should take in account the current locale" do 69 | I18n.locale = :en 70 | company.label = nil 71 | I18n.locale = :fr 72 | expect(company.label).to eql "société" 73 | end 74 | end 75 | 76 | describe "the _native setter" do 77 | it "should be locale independant" do 78 | company.label_native = [RDF::Literal.new('Company', language: :en)] 79 | expect(company.label_native.length).to eql 1 80 | end 81 | end 82 | 83 | describe "the _with_locales setter" do 84 | it "should be locale independant" do 85 | company.label_with_locales = { en: 'Company', fr: 'Société' } 86 | expect(company.label_native.length).to eql 2 87 | end 88 | end 89 | 90 | describe "#save" do 91 | it "only serializes the set values" do 92 | I18n.locale = :en 93 | company.label = nil 94 | company.save! 95 | expect(Spira.repository.size).to eq 2 96 | end 97 | end 98 | end 99 | end 100 | -------------------------------------------------------------------------------- /spec/nodes_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | # Behaviors relating to BNodes vs URIs 4 | 5 | describe 'Spira resources' do 6 | 7 | before :all do 8 | class ::NodeTest < Spira::Base 9 | property :name, predicate: RDF::Vocab::FOAF.name 10 | end 11 | end 12 | 13 | before :each do 14 | Spira.clear_repository! 15 | Spira.repository = RDF::Repository.new 16 | end 17 | 18 | context "when instatiated from URIs" do 19 | let(:uri) {RDF::URI('http://example.org/bob')} 20 | subject {uri.as(NodeTest)} 21 | 22 | it {is_expected.to respond_to :to_uri} 23 | 24 | it {is_expected.not_to respond_to :to_node} 25 | 26 | its(:node?) {is_expected.to be_falsey} 27 | its(:to_uri) {is_expected.to eql uri} 28 | 29 | specify {expect { subject.to_node }.to raise_error NoMethodError} 30 | end 31 | 32 | context "when instantiated from Nodes" do 33 | let(:node) {RDF::Node.new} 34 | subject {node.as(NodeTest)} 35 | 36 | it {is_expected.not_to respond_to :to_uri} 37 | 38 | it {is_expected.to respond_to :to_node} 39 | 40 | its(:node?) {is_expected.to be_truthy} 41 | its(:to_node) {is_expected.to eql node} 42 | 43 | specify {expect { subject.to_uri }.to raise_error NoMethodError} 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /spec/non_model_data_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe 'Resources with data not associated with a model' do 4 | 5 | before :all do 6 | class ::ExtraDataTest < Spira::Base 7 | configure base_uri: "http://example.org/example" 8 | 9 | property :property, predicate: RDF::Vocab::FOAF.age, type: Integer 10 | has_many :list, predicate: RDF::RDFS.label 11 | end 12 | end 13 | let(:extra_repo) {RDF::Repository.load(fixture('non_model_data.nt'))} 14 | 15 | before {Spira.repository = extra_repo} 16 | 17 | context "when multiple objects exist for a property" do 18 | subject {ExtraDataTest.for('example2')} 19 | 20 | it "should not raise an error to load a model with multiple instances of a property predicate" do 21 | expect { ExtraDataTest.for('example2') }.not_to raise_error 22 | end 23 | 24 | its(:property) {is_expected.to be_a Integer} 25 | 26 | it "should load one of the available property examples as the property" do 27 | expect([15,20]).to include subject.property 28 | end 29 | 30 | end 31 | 32 | context "when deleting" do 33 | subject {ExtraDataTest.for('example1')} 34 | 35 | it "should not delete non-model data on Resource#!destroy" do 36 | subject.destroy! 37 | expect(extra_repo.query({subject: subject.uri, predicate: RDF::Vocab::FOAF.name}).count).to eql 1 38 | end 39 | 40 | end 41 | 42 | context "when updating" do 43 | subject {ExtraDataTest.for('example1')} 44 | 45 | it "should save model data" do 46 | subject.property = 17 47 | subject.save! 48 | expect(extra_repo.query({subject: subject.uri, predicate: RDF::Vocab::FOAF.age}).count).to eql 1 49 | expect(extra_repo.first_value({subject: subject.uri, predicate: RDF::Vocab::FOAF.age}).to_i).to eql 17 50 | end 51 | 52 | it "should not affect non-model data" do 53 | subject.property = 17 54 | subject.save! 55 | expect(extra_repo.query({subject: subject.uri, predicate: RDF::Vocab::FOAF.name}).count).to eql 1 56 | expect(extra_repo.first_value({subject: subject.uri, predicate: RDF::Vocab::FOAF.name})).to eql "Not in the model" 57 | end 58 | end 59 | 60 | end 61 | -------------------------------------------------------------------------------- /spec/property_types_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe 'types for properties' do 4 | 5 | context "when declaring type classes" do 6 | context "in a separate thread" do 7 | it "should be available" do 8 | types = {} 9 | t = Thread.new { types = Spira.types } 10 | t.join 11 | 12 | expect(types).to satisfy do |ts| 13 | ts.any? && ts == Spira.types 14 | end 15 | end 16 | 17 | it "should be declared" do 18 | expect { 19 | t = Thread.new do 20 | class ::PropTypeA < Spira::Base 21 | configure default_vocabulary: RDF::URI.new('http://example.org/vocab'), 22 | base_uri: RDF::URI.new('http://example.org/props') 23 | 24 | property :test, type: XSD.string 25 | end 26 | end 27 | t.join 28 | }.not_to raise_error 29 | end 30 | end 31 | 32 | it "should raise a type error to use a type that has not been declared" do 33 | expect { 34 | class ::PropTypeA < Spira::Base 35 | configure default_vocabulary: RDF::URI.new('http://example.org/vocab'), 36 | base_uri: RDF::URI.new('http://example.org/props') 37 | 38 | property :test, type: XSD.non_existent_type 39 | end 40 | }.to raise_error TypeError 41 | end 42 | 43 | it "should not raise a type error to use a symbol type, even if the class has not been declared yet" do 44 | expect { 45 | class ::PropTypeB < Spira::Base 46 | configure default_vocabulary: RDF::URI.new('http://example.org/vocab'), 47 | base_uri: RDF::URI.new('http://example.org/props') 48 | 49 | property :test, type: :non_existent_type 50 | end 51 | }.not_to raise_error 52 | end 53 | 54 | it "should not raise an error to use an included XSD type aliased to a Spira type" do 55 | expect { 56 | class ::PropTypeD < Spira::Base 57 | configure default_vocabulary: RDF::URI.new('http://example.org/vocab'), 58 | base_uri: RDF::URI.new('http://example.org/props') 59 | 60 | property :test, type: XSD.string 61 | end 62 | }.not_to raise_error 63 | end 64 | 65 | it "should not raise an error to use an included Spira type" do 66 | expect { 67 | class ::PropTypeC < Spira::Base 68 | configure default_vocabulary: RDF::URI.new('http://example.org/vocab'), 69 | base_uri: RDF::URI.new('http://example.org/props') 70 | 71 | property :test, type: String 72 | end 73 | }.not_to raise_error 74 | end 75 | 76 | end 77 | 78 | # These tests are to make sure that type declarations and mappings work 79 | # correctly. For specific type boxing/unboxing, see the types/ folder. 80 | context 'when declaring types for properties' do 81 | 82 | before :all do 83 | 84 | Spira.repository = RDF::Repository.new 85 | 86 | class ::TestType 87 | include Spira::Type 88 | 89 | def self.serialize(value) 90 | RDF::Literal.new(value, datatype: XSD.test_type) 91 | end 92 | 93 | def self.unserialize(value) 94 | value.value 95 | end 96 | 97 | register_alias XSD.test_type 98 | end 99 | 100 | class ::PropTest < Spira::Base 101 | configure default_vocabulary: RDF::URI.new('http://example.org/vocab'), 102 | base_uri: RDF::URI.new('http://example.org/props') 103 | 104 | property :test, type: TestType 105 | property :xsd_test, type: XSD.test_type 106 | end 107 | end 108 | 109 | subject {PropTest.for 'test'} 110 | 111 | it "uses the given serialize function" do 112 | subject.test = "a string" 113 | expect(subject).to have_object RDF::Literal.new("a string", datatype: RDF::XSD.test_type) 114 | end 115 | 116 | it "uses the given unserialize function" do 117 | subject.test = "a string" 118 | subject.save! 119 | expect(subject.test).to eql "a string" 120 | expect(subject.test).to eql PropTest.for('test').test 121 | end 122 | 123 | it "correctly associates a URI datatype alias to the correct class" do 124 | expect(Spira.types[RDF::XSD.test_type]).to eql TestType 125 | expect(PropTest.properties[:xsd_test][:type]).to eql TestType 126 | end 127 | 128 | end 129 | 130 | end 131 | -------------------------------------------------------------------------------- /spec/psych_spec.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(File.expand_path(__FILE__)) + '/spec_helper' 2 | 3 | # Tests serializing and de-serializing with Psych (YAML). 4 | 5 | describe Spira, ruby: "1.9" do 6 | require 'psych' 7 | 8 | before :all do 9 | class PsychPerson < Spira::Base 10 | configure base_uri: "http://example.org/example/people" 11 | property :name, predicate: RDF::RDFS.label 12 | property :age, predicate: RDF::Vocab::FOAF.age, type: Integer 13 | end 14 | 15 | class PsychEmployee < Spira::Base 16 | property :name, predicate: RDF::RDFS.label 17 | property :age, predicate: RDF::Vocab::FOAF.age, type: Integer 18 | end 19 | 20 | Spira.repository = RDF::Repository.load(fixture('bob.nt')) 21 | end 22 | 23 | subject {PsychPerson.for(RDF::URI.new('http://example.org/newperson'))} 24 | 25 | it "serializes to YAML" do 26 | yaml = Psych.dump(subject) 27 | expect(yaml).to be_a(String) 28 | end 29 | 30 | it "de-serializes from YAML" do 31 | yaml = Psych.dump(subject) 32 | person = Psych.load(yaml) 33 | expect(person).to eq subject 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /spec/querying_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe Spira do 4 | let(:options) {{}} 5 | let(:conditions) {{}} 6 | 7 | before :all do 8 | class ::LoadTest < Spira::Base 9 | type RDF::Vocab::FOAF.Document 10 | configure base_uri: "http://example.com/loads" 11 | 12 | property :name, predicate: RDF::Vocab::FOAF.name 13 | property :label, predicate: RDF::RDFS.label 14 | property :child, predicate: RDF::Vocab::FOAF.currentProject, type: 'LoadTest' 15 | end 16 | end 17 | 18 | context "when querying repositories" do 19 | let(:repo) {RDF::Repository.new} 20 | let(:uri) {RDF::URI('http://example.org/example')} 21 | 22 | before {Spira.repository = repo} 23 | 24 | shared_examples_for "array that can be paginated" do 25 | let(:people) {[]} 26 | before do 27 | 3.times {|i| people << LoadTest.create(name: "person_#{i+1}")} 28 | end 29 | 30 | it "should yield all records" do 31 | subject.each {|person| expect(people).to include(person) } 32 | expect(subject.count).to eql people.size 33 | end 34 | 35 | context "given :offset option" do 36 | before { options.merge!(offset: 1) } 37 | 38 | it "should yield records within the given offset" do 39 | subject.each {|person| expect(person).not_to eql people[0] } 40 | end 41 | end 42 | 43 | context "given :limit option" do 44 | before { options.merge!(limit: 2) } 45 | 46 | it "should yield records within the given limit" do 47 | subject.each {|person| expect(person).not_to eql people[2] } 48 | end 49 | end 50 | 51 | context "given :offset and :limit options" do 52 | before { options.merge!(limit: 1, offset: 1) } 53 | 54 | it "should yield records withing the resulting range" do 55 | subject.each do |person| 56 | expect(person).not_to eql people[0] 57 | expect(person).not_to eql people[2] 58 | end 59 | end 60 | end 61 | end 62 | 63 | describe "find_each" do 64 | subject { LoadTest.find_each(conditions, **options) } 65 | 66 | it { is_expected.to be_a Enumerator } 67 | 68 | it { is_expected.not_to respond_to :to_ary } 69 | 70 | it_should_behave_like "array that can be paginated" 71 | end 72 | 73 | describe "all" do 74 | subject { LoadTest.all(options.merge(conditions: conditions)) } 75 | 76 | it { is_expected.to respond_to :to_ary } 77 | 78 | it_should_behave_like "array that can be paginated" 79 | end 80 | 81 | it "should attempt to query on instantiation" do 82 | expect(repo).to receive(:query).once.and_return([]) 83 | uri.as(LoadTest) 84 | end 85 | 86 | it "should attempt query once on property setting" do 87 | expect(repo).to receive(:query).once.and_return([]) 88 | test = uri.as(LoadTest) 89 | test.name = "test" 90 | test.name = "another test" 91 | end 92 | 93 | it "should not attempt to query on property getting" do 94 | expect(repo).to receive(:query).once.and_return([]) 95 | test = uri.as(LoadTest) 96 | test.name 97 | end 98 | 99 | it "should only query once for all properties" do 100 | expect(repo).to receive(:query).once.and_return([]) 101 | test = uri.as(LoadTest) 102 | test.name 103 | test.label 104 | end 105 | 106 | it "should support :reload" do 107 | test = uri.as(LoadTest) 108 | expect(test).to respond_to :reload 109 | end 110 | 111 | it "should touch the repository to reload" do 112 | expect(repo).to receive(:query).twice.and_return([]) 113 | test = uri.as(LoadTest) 114 | test.reload 115 | end 116 | 117 | it "should query the repository again after a reload" do 118 | expect(repo).to receive(:query).twice.and_return([]) 119 | test = uri.as(LoadTest) 120 | test.name 121 | test.reload 122 | test.name 123 | end 124 | 125 | context "for relations" do 126 | let(:child_uri) {RDF::URI("http://example.org/example2")} 127 | let(:parent_statements) {[]} 128 | let(:child_statements) {[]} 129 | before :each do 130 | st = RDF::Statement.new(subject: uri, predicate: RDF::Vocab::FOAF.currentProject, object: child_uri) 131 | # uri and child_uri now point at each other 132 | repo << st 133 | parent_statements << st 134 | st = RDF::Statement.new(subject: uri, predicate: RDF::Vocab::FOAF.name, object: RDF::Literal.new("a name")) 135 | repo << st 136 | parent_statements << st 137 | st = RDF::Statement.new(subject: uri, predicate: RDF::RDFS.label, object: RDF::Literal.new("a name")) 138 | repo << st 139 | parent_statements << st 140 | st = RDF::Statement.new(subject: uri, predicate: RDF.type, object: RDF::Vocab::FOAF.Document) 141 | repo << st 142 | parent_statements << st 143 | 144 | st = RDF::Statement.new(subject: child_uri, predicate: RDF::Vocab::FOAF.currentProject, object: uri) 145 | repo << st 146 | child_statements << st 147 | st = RDF::Statement.new(subject: child_uri, predicate: RDF::Vocab::FOAF.currentProject, object: uri) 148 | repo << st 149 | child_statements << st 150 | st = RDF::Statement.new(subject: child_uri, predicate: RDF.type, object: RDF::Vocab::FOAF.Document) 151 | repo << st 152 | child_statements << st 153 | # We need this copy to return from mocks, as the return value is itself queried inside spira, confusing the count 154 | end 155 | 156 | it "should not query the repository when loading a parent and not accessing a child" do 157 | name_statements = parent_statements.select {|st| st.predicate == RDF::Vocab::FOAF.name } 158 | expect(repo).to receive(:query).with({subject: uri}).once.and_return(name_statements) 159 | 160 | test = uri.as(LoadTest) 161 | test.name 162 | end 163 | 164 | it "should query the repository when loading a parent and accessing a field on a child" do 165 | name_statements = parent_statements.select {|st| st.predicate == RDF::Vocab::FOAF.name } 166 | expect(repo).to receive(:query).with({subject: uri}).once.and_return(parent_statements) 167 | expect(repo).to receive(:query).with({subject: child_uri}).once.and_return(name_statements) 168 | 169 | test = uri.as(LoadTest) 170 | test.child.name 171 | end 172 | 173 | it "should not re-query to access a child twice" do 174 | name_statements = parent_statements.select {|st| st.predicate == RDF::Vocab::FOAF.name } 175 | expect(repo).to receive(:query).with({subject: uri}).once.and_return(parent_statements) 176 | expect(repo).to receive(:query).with({subject: child_uri}).once.and_return(name_statements) 177 | 178 | test = uri.as(LoadTest) 179 | 2.times { test.child.name } 180 | end 181 | 182 | it "should re-query to access a child's parent from the child" do 183 | name_statements = parent_statements.select {|st| st.predicate == RDF::Vocab::FOAF.name } 184 | expect(repo).to receive(:query).with({subject: uri}).twice.and_return(parent_statements) 185 | expect(repo).to receive(:query).with({subject: child_uri}).once.and_return(child_statements) 186 | 187 | test = uri.as(LoadTest) 188 | 3.times do 189 | expect(test.child.child.name).to eql "a name" 190 | end 191 | end 192 | 193 | it "should re-query for children after a #reload" do 194 | parent_name_statements = parent_statements.select {|st| st.predicate == RDF::Vocab::FOAF.name } 195 | child_name_statements = child_statements.select {|st| st.predicate == RDF::Vocab::FOAF.name } 196 | expect(repo).to receive(:query).with({subject: uri}).exactly(4).times.and_return(parent_statements) 197 | expect(repo).to receive(:query).with({subject: child_uri}).twice.and_return(child_statements) 198 | 199 | test = uri.as(LoadTest) 200 | expect(test.child.child.name).to eql "a name" 201 | expect(test.child.name).to be_nil 202 | test.reload 203 | expect(test.child.child.name).to eql "a name" 204 | expect(test.child.name).to be_nil 205 | end 206 | 207 | it "should not re-query to iterate by type twice" do 208 | pending "no longer applies as the global cache is gone" 209 | 210 | # once to get the list of subjects, once for uri, once for child_uri, 211 | # and once for the list of subjects again 212 | parent_name_statements = parent_statements.select {|st| st.predicate == RDF::Vocab::FOAF.name } 213 | child_name_statements = child_statements.select {|st| st.predicate == RDF::Vocab::FOAF.name } 214 | expect(repo).to receive(:query).with(subject: uri, predicate: RDF::Vocab::FOAF.name).twice.and_return(parent_name_statements) 215 | expect(repo).to receive(:query).with(subject: child_uri, predicate: RDF::Vocab::FOAF.name).twice.and_return(child_name_statements) 216 | @types = RDF::Repository.new 217 | @types.insert *repo.statements.select{|s| s.predicate == RDF.type && s.object == RDF::Vocab::FOAF.Document} 218 | expect(repo).to receive(:query).with(predicate: RDF.type, object: RDF::Vocab::FOAF.Document).twice.and_return(@types.statements) 219 | 220 | # need to map to touch a property on each to make sure they actually 221 | # get loaded due to lazy evaluation 222 | 2.times do 223 | expect(LoadTest.each.map { |lt| lt.name }.size).to eql 2 224 | end 225 | end 226 | 227 | end 228 | end 229 | end 230 | -------------------------------------------------------------------------------- /spec/rdf_types_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | # These classes are to test finding based on RDF::RDFS.type 3 | 4 | 5 | class Cars < RDF::Vocabulary('http://example.org/cars/') 6 | property :car 7 | property :van 8 | property :car1 9 | property :van 10 | property :station_wagon 11 | property :unrelated_type 12 | end 13 | 14 | describe 'models with a defined rdf type' do 15 | subject {RDF::Repository.load(fixture('types.nt'))} 16 | let(:car1) {Car.for Cars.car1} 17 | let(:car2) {Car.for Cars.car2} 18 | 19 | before :all do 20 | class ::Car < Spira::Base 21 | type Cars.car 22 | property :name, predicate: RDF::RDFS.label 23 | end 24 | 25 | class ::Van < Spira::Base 26 | type Cars.van 27 | property :name, predicate: RDF::RDFS.label 28 | end 29 | 30 | class ::Wagon < Spira::Base 31 | property :name, predicate: RDF::RDFS.label 32 | end 33 | 34 | class ::MultiCar < Spira::Base 35 | type Cars.car 36 | type Cars.van 37 | end 38 | end 39 | 40 | before {Spira.repository = subject} 41 | 42 | context "when declaring types" do 43 | it "should raise an error when declaring a non-uri type" do 44 | expect { 45 | class ::XYZ < Spira::Base 46 | type 'a string, for example' 47 | end 48 | }.to raise_error TypeError 49 | end 50 | 51 | it "should provide a class method which returns the type" do 52 | expect(Car).to respond_to :type 53 | end 54 | 55 | it "should return the correct type" do 56 | expect(Car.type).to eql Cars.car 57 | end 58 | 59 | it "should return nil if no type is declared" do 60 | expect(Wagon.type).to eql nil 61 | end 62 | end 63 | 64 | context "When finding by types" do 65 | it "should find 1 car" do 66 | expect(Car.count).to eql 1 67 | end 68 | 69 | it "should find 3 vans" do 70 | expect(Van.count).to eql 3 71 | end 72 | end 73 | 74 | context "when creating" do 75 | subject {Car.for RDF::URI.new('http://example.org/cars/newcar')} 76 | 77 | its(:type) {is_expected.to eql Car.type} 78 | 79 | it "should not include a type statement on dump" do 80 | # NB: declaring an object with a type does not get the type statement in the DB 81 | # until the object is persisted! 82 | expect(subject).not_to have_statement(predicate: RDF.type, object: Car.type) 83 | end 84 | 85 | it "should not be able to assign type" do 86 | expect { 87 | Car.for(RDF::URI.new('http://example.org/cars/newcar2'), type: Cars.van) 88 | }.to raise_error NoMethodError 89 | end 90 | 91 | end 92 | 93 | context "when loading" do 94 | it "should have a type" do 95 | expect(car1.type).to eql Car.type 96 | end 97 | 98 | it "should have a type when loading a resource without one in the data store" do 99 | expect(car2.type).to eql Car.type 100 | end 101 | end 102 | 103 | context "when saving" do 104 | it "should save a type for resources which don't have one in the data store" do 105 | car2.save! 106 | expect(subject.query({subject: Cars.car2, predicate: RDF.type, object: Cars.car}).count).to eql 1 107 | end 108 | 109 | it "should save a type for newly-created resources which in the data store" do 110 | car3 = Car.for(Cars.car3) 111 | car3.save! 112 | expect(subject.query({subject: Cars.car3, predicate: RDF.type, object: Cars.car}).count).to eql 1 113 | end 114 | end 115 | 116 | context "When getting/setting" do 117 | before :each do 118 | expect(car1).not_to be_nil 119 | end 120 | 121 | it "should allow setting other properties" do 122 | car1.name = "prius" 123 | car1.save! 124 | expect(car1.type).to eql Cars.car 125 | expect(car1.name).to eql "prius" 126 | end 127 | 128 | it "should raise an exception when trying to change the type" do 129 | expect {car1.type = Cars.van}.to raise_error(NoMethodError) 130 | end 131 | 132 | it "should maintain all triples related to this object on save" do 133 | original_triples = subject.query({subject: Cars.car1}) 134 | car1.name = 'testing123' 135 | car1.save! 136 | expect(subject.query({subject: Cars.car1}).count).to eql original_triples.size 137 | end 138 | end 139 | 140 | context "when counting" do 141 | it "should count all projected types" do 142 | expect { 143 | Car.for(Cars.one).save! 144 | Van.for(Cars.two).save! 145 | }.to change(MultiCar, :count).by(2) 146 | end 147 | 148 | it "should provide a count method for resources with types" do 149 | expect(Car.count).to eql 1 150 | end 151 | 152 | it "should increase the count when items are saved" do 153 | Car.for(Cars.toyota).save! 154 | expect(Car.count).to eql 2 155 | end 156 | 157 | it "should decrease the count when items are destroyed" do 158 | expect { car1.destroy }.to change(Car, :count).from(1).to(0) 159 | end 160 | 161 | it "should raise a Spira::NoTypeError to call #count for models without types" do 162 | expect { Wagon.count }.to raise_error Spira::NoTypeError 163 | end 164 | end 165 | 166 | context "when enumerating" do 167 | it "should provide an each method for resources with types" do 168 | expect(Van.each.to_a.size).to eql 3 169 | end 170 | 171 | it "should raise a Spira::NoTypeError to call #each for models without types" do 172 | expect { Wagon.each }.to raise_error Spira::NoTypeError 173 | end 174 | 175 | it "should return an enumerator if no block is given" do 176 | expect(Van.each).to be_a Enumerator 177 | end 178 | 179 | it "should execute a block if one is given" do 180 | vans = [] 181 | Van.each do |resource| 182 | vans << resource 183 | end 184 | [Cars.van1, Cars.van2, Cars.van3].each do |uri| 185 | expect(vans.any? { |van| van.uri == uri }).to be_truthy 186 | end 187 | end 188 | end 189 | 190 | end 191 | -------------------------------------------------------------------------------- /spec/relations_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe "Spira resources" do 4 | 5 | before :all do 6 | 7 | class ::CDs < RDF::Vocabulary('http://example.org/') 8 | property :artist 9 | property :cds 10 | property :artists 11 | property :has_cd 12 | end 13 | 14 | class ::CD < Spira::Base 15 | configure base_uri: CDs.cds 16 | property :name, predicate: RDF::Vocab::DC.title, type: String 17 | property :artist, predicate: CDs.artist, type: 'Artist' 18 | end 19 | 20 | class ::Artist < Spira::Base 21 | configure base_uri: CDs.artists 22 | property :name, predicate: RDF::Vocab::DC.title, type: String 23 | has_many :cds, predicate: CDs.has_cd, type: :CD 24 | has_many :teams, predicate: CDs.teams, type: :Team 25 | end 26 | 27 | class ::Team < Spira::Base 28 | configure base_uri: CDs.teams 29 | has_many :artists, predicate: CDs.artist, type: 'Artist' 30 | end 31 | end 32 | 33 | context "when referencing relationships" do 34 | context "in the root namespace" do 35 | before :all do 36 | class ::RootNSTest < Spira::Base 37 | property :name, predicate: RDF::Vocab::DC.title, type: 'RootNSTest' 38 | end 39 | Spira.repository = RDF::Repository.new 40 | end 41 | 42 | it "should find a class based on the string version of the name" do 43 | test = RootNSTest.new 44 | subject = test.subject 45 | test.name = RootNSTest.new 46 | test.name.save! 47 | test.save! 48 | 49 | test = subject.as(RootNSTest) 50 | expect(test.name).to be_a RootNSTest 51 | end 52 | end 53 | 54 | context "in the same namespace" do 55 | before :all do 56 | module ::NSTest 57 | class X < Spira::Base 58 | property :name, predicate: RDF::Vocab::DC.title, type: 'Y' 59 | end 60 | class Y < Spira::Base 61 | end 62 | end 63 | Spira.repository = RDF::Repository.new 64 | end 65 | 66 | it "should find a class based on the string version of the name" do 67 | test = NSTest::X.new 68 | subject = test.subject 69 | test.name = NSTest::Y.new 70 | 71 | test.save! 72 | 73 | test = NSTest::X.for(subject) 74 | expect(test.name).to be_a NSTest::Y 75 | end 76 | end 77 | 78 | context "in another namespace" do 79 | before :all do 80 | module ::NSTest 81 | class Z < Spira::Base 82 | property :name, predicate: RDF::Vocab::DC.title, type: 'NSTestA::A' 83 | end 84 | end 85 | module ::NSTestA 86 | class A < Spira::Base 87 | end 88 | end 89 | end 90 | 91 | it "should find a class based on the string version of the name" do 92 | test = NSTest::Z.new 93 | subject = test.subject 94 | test.name = NSTestA::A.new 95 | 96 | test.save! 97 | 98 | test = NSTest::Z.for(subject) 99 | expect(test.name).to be_a NSTestA::A 100 | end 101 | end 102 | end 103 | 104 | context "with many-to-many relationship" do 105 | subject(:artist) {Artist.for "Beard"} 106 | subject(:team) {Team.for "ZZ Top"} 107 | 108 | before :all do 109 | Spira.repository = RDF::Repository.new 110 | end 111 | 112 | context "with resources referencing each other" do 113 | before do 114 | artist.teams = [team] 115 | team.artists = [artist] 116 | 117 | artist.save! 118 | team.save! 119 | end 120 | 121 | context "when reloading" do 122 | it "should not cause an infinite loop" do 123 | expect(artist.reload).to eql artist 124 | end 125 | end 126 | end 127 | end 128 | 129 | context "with a one-to-many relationship" do 130 | subject(:artist) {Artist.for "nirvana"} 131 | subject(:cd) {CD.for 'nevermind'} 132 | 133 | before :each do 134 | Spira.repository = RDF::Repository.load(fixture('relations.nt')) 135 | @cd = CD.for 'nevermind' 136 | end 137 | 138 | it "should find a cd" do 139 | expect(cd).to be_a CD 140 | end 141 | 142 | it "should find the artist" do 143 | expect(artist).to be_a Artist 144 | end 145 | 146 | it "should find an artist for a cd" do 147 | expect(cd.artist).to be_a Artist 148 | end 149 | 150 | it "should find the correct artist for a cd" do 151 | expect(cd.artist.uri).to eq artist.uri 152 | end 153 | 154 | it "should find CDs for an artist" do 155 | cds = artist.cds 156 | expect(cds).to be_a Array 157 | expect(cds.find { |cd| cd.name == 'Nevermind' }).to be_truthy 158 | expect(cds.find { |cd| cd.name == 'In Utero' }).to be_truthy 159 | end 160 | 161 | it "should not reload an object for a simple reverse relationship" do 162 | pending "no longer applies as the global cache is gone" 163 | 164 | expect(artist.cds.first.artist).to equal artist 165 | artist_cd = cd.artist.cds.find { | list_cd | list_cd.uri == cd.uri } 166 | expect(cd).to equal artist_cd 167 | end 168 | 169 | it "should find a model object for a uri" do 170 | expect(cd.artist).to eq artist 171 | end 172 | 173 | it "should make a valid statement referencing the assigned objects URI" do 174 | @kurt = Artist.for('kurt cobain') 175 | cd.artist = @kurt 176 | cd.query({predicate: CDs.artist}) do |statement| 177 | expect(statement.subject).to eq cd.uri 178 | expect(statement.predicate).to eq CDs.artist 179 | expect(statement.object).to eq @kurt.uri 180 | end 181 | end 182 | 183 | end 184 | 185 | context "with invalid relationships" do 186 | let(:invalid_repo) {RDF::Repository.new} 187 | 188 | before {Spira.repository = invalid_repo} 189 | 190 | context "when accessing a field named for a non-existant class" do 191 | 192 | before do 193 | class ::RelationsTestA < Spira::Base 194 | configure base_uri: CDs.cds 195 | property :invalid, predicate: CDs.artist, type: :non_existant_type 196 | end 197 | 198 | invalid_repo.insert(RDF::Statement.new(RDF::URI.new(CDs.cds.to_s + "/invalid_b"), CDs.artist, "whatever")) 199 | end 200 | 201 | it "should raise a NameError when saving an object with the invalid property" do 202 | expect { 203 | RelationsTestA.for('invalid_a', invalid: Object.new).save! 204 | }.to raise_error NameError 205 | end 206 | 207 | it "should raise a NameError when accessing the invalid property on an existing object" do 208 | expect { 209 | RelationsTestA.for('invalid_b').invalid 210 | }.to raise_error NameError 211 | end 212 | 213 | end 214 | 215 | context "when accessing a field for a class that is not a Spira::Resource" do 216 | before :all do 217 | class ::RelationsTestB < Spira::Base 218 | property :invalid, predicate: RDF::Vocab::DC.title, type: 'Object' 219 | end 220 | end 221 | 222 | it "should should raise a TypeError when saving an object with the invalid property" do 223 | expect { RelationsTestB.new(invalid: Object.new).save! }.to raise_error TypeError 224 | end 225 | 226 | it "should raise a TypeError when accessing the invalid property on an existing object" do 227 | subject = RDF::Node.new 228 | invalid_repo.insert [subject, RDF::Vocab::DC.title, 'something'] 229 | expect { RelationsTestB.for(subject).invalid }.to raise_error TypeError 230 | end 231 | end 232 | end 233 | end 234 | -------------------------------------------------------------------------------- /spec/repository_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | # Fixture to test :default repository loading 4 | 5 | describe Spira do 6 | let(:repo) { RDF::Repository.new } 7 | before {Spira.repository = repo} 8 | 9 | describe ".using_repository" do 10 | 11 | let(:new_repo) { RDF::Repository.new } 12 | 13 | it "should override the original repository for the block" do 14 | Spira.using_repository(new_repo) do 15 | expect(Spira.repository).to eql new_repo 16 | end 17 | end 18 | 19 | it "should restore the original repository after the block" do 20 | Spira.using_repository(new_repo) { } 21 | expect(Spira.repository).to eql repo 22 | end 23 | 24 | it "should return the given repository" do 25 | return_value = Spira.using_repository(new_repo) {} 26 | 27 | expect(return_value).to eql new_repo 28 | end 29 | 30 | context "when the block raises an error" do 31 | it "should restore the original repository" do 32 | begin 33 | Spira.using_repository(new_repo) do 34 | raise Exception.new('Some Error') 35 | end 36 | rescue Exception 37 | expect(Spira.repository).to eql repo 38 | end 39 | end 40 | end 41 | 42 | end 43 | 44 | context "when registering the repository" do 45 | 46 | class ::Event < Spira::Base 47 | property :name, predicate: RDF::Vocab::DC.title 48 | end 49 | 50 | before :each do 51 | Spira.clear_repository! 52 | end 53 | 54 | context "in a different thread" do 55 | before {Spira.repository = repo} 56 | 57 | it "should be another instance" do 58 | repo2 = nil 59 | 60 | t = Thread.new { 61 | repo2 = Spira.repository 62 | } 63 | t.join 64 | 65 | expect(repo2).not_to eql(repo) 66 | end 67 | end 68 | 69 | it "should allow updating of the repository" do 70 | new_repo = RDF::Repository.new 71 | Spira.repository = new_repo 72 | expect(Event.repository).to equal new_repo 73 | end 74 | 75 | it "should allow clearing the repository" do 76 | expect(Spira).to respond_to :clear_repository! 77 | Spira.repository = RDF::Repository.new 78 | Spira.clear_repository! 79 | expect(Spira.repository).to be_nil 80 | end 81 | 82 | end 83 | 84 | context "classes using the default repository" do 85 | context "without a set repository" do 86 | before { Spira.clear_repository! } 87 | 88 | it "should raise NoRepositoryError if a repository does not exist" do 89 | expect { Event.repository }.to raise_error Spira::NoRepositoryError 90 | end 91 | end 92 | 93 | context "with a set repository" do 94 | before :each do 95 | Spira.clear_repository! 96 | Spira.repository = repo 97 | end 98 | 99 | it "should know their repository" do 100 | expect(Event.repository).to equal repo 101 | end 102 | 103 | it "should allow accessing an attribute" do 104 | event = RDF::URI('http://example.org/events/that-one').as(Event) 105 | expect { event.name }.not_to raise_error 106 | end 107 | 108 | it "should allow calling instance#save!" do 109 | event = Event.for(RDF::URI.new('http://example.org/events/this-one')) 110 | expect { event.save! }.not_to raise_error 111 | end 112 | end 113 | end 114 | end 115 | -------------------------------------------------------------------------------- /spec/serialization_spec.rb: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | require "spec_helper" 3 | 4 | describe "serialization" do 5 | before :all do 6 | class SpiraResource < Spira::Base 7 | property :name, predicate: RDF::Vocab::FOAF.givenName, type: XSD.string 8 | end 9 | 10 | Spira.repository = RDF::Repository.new 11 | end 12 | 13 | it "should serialize a spira resource into its subject" do 14 | res = SpiraResource.for RDF::URI.new("http://example.com/resources/res1") 15 | 16 | serialized = SpiraResource.serialize(res) 17 | expect(serialized).not_to be_nil 18 | expect(serialized).to eql res.subject 19 | end 20 | 21 | it "should serialize a blank ruby object into nil" do 22 | expect(SpiraResource.serialize("")).to be_nil 23 | end 24 | 25 | it "should raise TypeError exception when trying to serialize an object it cannot serialize" do 26 | expect { SpiraResource.serialize(1) }.to raise_error TypeError 27 | end 28 | 29 | context "of UTF-8 literals" do 30 | it "should produce proper UTF-8 output" do 31 | res = SpiraResource.create(name: "日本語") 32 | expect(res.reload.name).to eql "日本語" 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require "bundler/setup" 2 | require 'rdf/spec/enumerable' 3 | require 'rdf/spec' 4 | require 'rdf/isomorphic' 5 | require 'rdf/ntriples' 6 | require 'rdf/turtle' 7 | require 'rdf/vocab' 8 | require 'amazing_print' 9 | 10 | begin 11 | require 'simplecov' 12 | require 'simplecov-lcov' 13 | 14 | SimpleCov::Formatter::LcovFormatter.config do |config| 15 | #Coveralls is coverage by default/lcov. Send info results 16 | config.report_with_single_file = true 17 | config.single_report_path = 'coverage/lcov.info' 18 | end 19 | 20 | SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter.new([ 21 | SimpleCov::Formatter::HTMLFormatter, 22 | SimpleCov::Formatter::LcovFormatter 23 | ]) 24 | SimpleCov.start do 25 | add_filter '/.bundle/' 26 | end 27 | rescue LoadError 28 | end 29 | 30 | require 'spira' 31 | 32 | require 'i18n' 33 | I18n.enforce_available_locales = false 34 | 35 | RSpec.configure do |config| 36 | config.filter_run focus: true 37 | config.run_all_when_everything_filtered = true 38 | config.exclusion_filter = { 39 | :ruby => lambda { |version| RUBY_VERSION.to_s !~ /^#{version}/}, 40 | } 41 | end 42 | 43 | def fixture(filename) 44 | File.join(File.dirname(__FILE__),'fixtures', filename) 45 | end 46 | -------------------------------------------------------------------------------- /spec/type_classes_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe 'types' do 4 | 5 | context "when declaring a new type" do 6 | before :all do 7 | class ::TypeA 8 | include Spira::Type 9 | end 10 | 11 | class ::TypeB 12 | include Spira::Type 13 | register_alias :typeb_alias 14 | end 15 | 16 | class ::TypeC 17 | include Spira::Type 18 | 19 | def self.serialize(value) 20 | value.to_s 21 | end 22 | 23 | def self.unserialize(value) 24 | value.to_i 25 | end 26 | end 27 | end 28 | 29 | it "should find itself registered as a type with spira" do 30 | expect(Spira.types[TypeA]).to eql TypeA 31 | end 32 | 33 | it "should have a class method to serialize" do 34 | expect(TypeA).to respond_to :serialize 35 | end 36 | 37 | it "should have a class method to unserialize" do 38 | expect(TypeA).to respond_to :unserialize 39 | end 40 | 41 | it "should allow itself to be aliased" do 42 | TypeA.register_alias(:typea_alias) 43 | expect(Spira.types[:typea_alias]).to eql TypeA 44 | end 45 | 46 | it "should allow aliases in the DSL" do 47 | expect(Spira.types[:typeb_alias]).to eql TypeB 48 | end 49 | 50 | it "should allow a self.serialize function" do 51 | expect(TypeC.serialize(5)).to eql "5" 52 | end 53 | 54 | it "should allow a self.unserialize function" do 55 | expect(TypeC.unserialize("5")).to eql 5 56 | end 57 | 58 | context "working with RDF vocabularies" do 59 | before :all do 60 | class ::TypeWithRDF 61 | include Spira::Type 62 | register_alias RDF::Vocab::DC.title 63 | end 64 | end 65 | 66 | it "should have the RDF namespace included for default vocabularies" do 67 | expect(Spira.types[::RDF::Vocab::DC.title]).to eql TypeWithRDF 68 | end 69 | end 70 | end 71 | end 72 | 73 | -------------------------------------------------------------------------------- /spec/types/anyURI_spec.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(File.expand_path(__FILE__)) + '/../spec_helper' 2 | 3 | describe Spira::Types::AnyURI do 4 | 5 | context "when serializing" do 6 | it "should be able to serialize URIs to XSD anyURI" do 7 | serialized = Spira::Types::AnyURI.serialize('http://host/path') 8 | expect(serialized).to be_a RDF::Literal 9 | expect(serialized).to have_datatype 10 | expect(serialized.datatype).to eql RDF::XSD.anyURI 11 | expect(serialized).to eql RDF::Literal.new('http://host/path', datatype: RDF::XSD.anyURI) 12 | end 13 | end 14 | 15 | context "when unserializing" do 16 | it "should unserialize XSD anyURI to String" do 17 | value = Spira::Types::AnyURI.unserialize(RDF::Literal.new('http://host/path', datatype: RDF::XSD.anyURI)) 18 | expect(value).to be_a String 19 | expect(value).to eql 'http://host/path' 20 | end 21 | end 22 | 23 | 24 | end 25 | 26 | -------------------------------------------------------------------------------- /spec/types/any_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe Spira::Types::Any do 4 | 5 | before :all do 6 | @uri = RDF::URI('http://example.org') 7 | end 8 | 9 | # this spec is going to be necessarily loose. The 'Any' type is defined to 10 | # use RDF.rb's automatic RDF Literal boxing and unboxing, which may or may 11 | # not change between verions. 12 | # 13 | context "when serializing" do 14 | it "should serialize literals to RDF Literals" do 15 | serialized = Spira::Types::Any.serialize(15) 16 | expect(serialized).to be_a RDF::Literal 17 | serialized = Spira::Types::Any.serialize("test") 18 | expect(serialized).to be_a RDF::Literal 19 | end 20 | 21 | it "should keep RDF::URIs as URIs" do 22 | expect(Spira::Types::Any.serialize(@uri)).to eql @uri 23 | end 24 | 25 | it "should fail to serialize collections" do 26 | expect { Spira::Types::Any.serialize([]) }.to raise_error TypeError 27 | end 28 | end 29 | 30 | context "when unserializing" do 31 | specify "datatyped literal" do 32 | expect(Spira::Types::Any.unserialize(RDF::Literal(5))).to eq 5 33 | end 34 | specify "plain literal" do 35 | expect(Spira::Types::Any.unserialize(RDF::Literal("a string"))).to eq "a string" 36 | end 37 | 38 | specify "URI" do 39 | expect(Spira::Types::Any.unserialize(@uri)).to include( 40 | :scheme=>"http", 41 | :authority=>"example.org", 42 | :host=>"example.org" 43 | ) 44 | end 45 | end 46 | 47 | 48 | end 49 | 50 | -------------------------------------------------------------------------------- /spec/types/boolean_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe Spira::Types::Boolean do 4 | 5 | context "when serializing" do 6 | it "should serialize booleans to XSD booleans" do 7 | serialized = Spira::Types::Boolean.serialize(true) 8 | expect(serialized).to be_a RDF::Literal 9 | expect(serialized).to have_datatype 10 | expect(serialized.datatype).to eql RDF::XSD.boolean 11 | expect(serialized).to eql RDF::Literal.new(true) 12 | end 13 | 14 | it "should serialize true-equivalents to XSD booleans" do 15 | serialized = Spira::Types::Boolean.serialize(15) 16 | expect(serialized).to be_a RDF::Literal 17 | expect(serialized).to have_datatype 18 | expect(serialized.datatype).to eql RDF::XSD.boolean 19 | expect(serialized).to eql RDF::Literal.new(true) 20 | end 21 | 22 | it "should serialize false-equivalents to XSD booleans" do 23 | serialized = Spira::Types::Boolean.serialize(nil) 24 | expect(serialized).to be_a RDF::Literal 25 | expect(serialized).to have_datatype 26 | expect(serialized.datatype).to eql RDF::XSD.boolean 27 | expect(serialized).to eql RDF::Literal.new(false) 28 | end 29 | end 30 | 31 | context "when unserializing" do 32 | it "should unserialize XSD booleans to booleans" do 33 | value = Spira::Types::Boolean.unserialize(RDF::Literal.new(true, datatype: RDF::XSD.boolean)) 34 | expect(value).to equal true 35 | value = Spira::Types::Boolean.unserialize(RDF::Literal.new(false, datatype: RDF::XSD.boolean)) 36 | expect(value).to equal false 37 | end 38 | end 39 | 40 | 41 | end 42 | 43 | -------------------------------------------------------------------------------- /spec/types/dateTime_spec.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(File.expand_path(__FILE__)) + '/../spec_helper' 2 | 3 | describe Spira::Types::DateTime do 4 | 5 | before :all do 6 | @date = DateTime.now 7 | end 8 | 9 | context "when serializing" do 10 | it "should serialize datetimes to XSD datetimes" do 11 | serialized = Spira::Types::DateTime.serialize(@date) 12 | expect(serialized).to be_a RDF::Literal 13 | expect(serialized).to have_datatype 14 | expect(serialized.datatype).to eql RDF::XSD.dateTime 15 | expect(serialized).to eql RDF::Literal.new(@date, datatype: RDF::XSD.dateTime) 16 | end 17 | end 18 | 19 | context "when unserializing" do 20 | it "should unserialize XSD datetimes to datetimes" do 21 | value = Spira::Types::DateTime.unserialize(RDF::Literal.new(@date, datatype: RDF::XSD.dateTime)) 22 | expect(value).to equal @date 23 | end 24 | end 25 | 26 | 27 | end 28 | 29 | -------------------------------------------------------------------------------- /spec/types/date_spec.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(File.expand_path(__FILE__)) + '/../spec_helper' 2 | 3 | describe Spira::Types::Date do 4 | 5 | before :all do 6 | @date = Date.today 7 | end 8 | 9 | context "when serializing" do 10 | it "should serialize dates to XSD dates" do 11 | serialized = Spira::Types::Date.serialize(@date) 12 | expect(serialized).to be_a RDF::Literal 13 | expect(serialized).to have_datatype 14 | expect(serialized.datatype).to eql RDF::XSD.date 15 | expect(serialized).to eql RDF::Literal.new(@date, datatype: RDF::XSD.date) 16 | end 17 | end 18 | 19 | context "when unserializing" do 20 | it "should unserialize XSD dates to dates" do 21 | value = Spira::Types::Date.unserialize(RDF::Literal.new(@date, datatype: RDF::XSD.date)) 22 | expect(value).to eql @date 23 | end 24 | end 25 | 26 | 27 | end 28 | 29 | -------------------------------------------------------------------------------- /spec/types/decimal_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe Spira::Types::Decimal do 4 | 5 | context "when serializing" do 6 | it "should serialize decimals to XSD decimals" do 7 | serialized = Spira::Types::Decimal.serialize(5.15) 8 | expect(serialized).to be_a RDF::Literal 9 | expect(serialized).to have_datatype 10 | expect(serialized.datatype).to eql RDF::XSD.decimal 11 | expect(serialized).to eql RDF::Literal.new(5.15, datatype: RDF::XSD.decimal) 12 | end 13 | end 14 | 15 | context "when unserializing" do 16 | it "should unserialize XSD decimals to BigDecimals" do 17 | value = Spira::Types::Decimal.unserialize(RDF::Literal.new(5.15, datatype: RDF::XSD.decimal)) 18 | expect(value).to be_a BigDecimal 19 | expect(value).to eql BigDecimal('5.15') 20 | end 21 | end 22 | 23 | # BigDecimal has a silly default to_s, which this test makes sure we are avoiding 24 | context "when round tripping" do 25 | it "should serialize to the original value after unserializing" do 26 | literal = RDF::Literal.new(5.15, datatype: RDF::XSD.decimal) 27 | unserialized = Spira::Types::Decimal.unserialize(literal) 28 | serialized = Spira::Types::Decimal.serialize(unserialized) 29 | expect(serialized).to eql literal 30 | end 31 | end 32 | 33 | end 34 | 35 | -------------------------------------------------------------------------------- /spec/types/double_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe Spira::Types::Double do 4 | 5 | context "when serializing" do 6 | it "should serialize floats to XSD doubles" do 7 | serialized = Spira::Types::Double.serialize(5.0) 8 | expect(serialized).to be_a RDF::Literal 9 | expect(serialized).to have_datatype 10 | expect(serialized.datatype).to eql RDF::XSD.double 11 | expect(serialized).to eql RDF::Literal.new(5.0) 12 | end 13 | 14 | it "should serialize integers to XSD doubles" do 15 | serialized = Spira::Types::Double.serialize(5) 16 | expect(serialized).to be_a RDF::Literal 17 | expect(serialized).to have_datatype 18 | expect(serialized.datatype).to eql RDF::XSD.double 19 | expect(serialized).to eql RDF::Literal.new(5.0) 20 | end 21 | 22 | end 23 | 24 | context "when unserializing" do 25 | it "should unserialize XSD doubles to floats" do 26 | value = Spira::Types::Double.unserialize(RDF::Literal.new(5, datatype: RDF::XSD.double)) 27 | expect(value).to be_a Float 28 | expect(value).to eql 5.0 29 | end 30 | end 31 | 32 | 33 | end 34 | 35 | -------------------------------------------------------------------------------- /spec/types/float_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe Spira::Types::Float do 4 | 5 | context "when serializing" do 6 | it "should serialize floats to XSD floats" do 7 | serialized = Spira::Types::Float.serialize(5.0) 8 | expect(serialized).to be_a RDF::Literal 9 | expect(serialized).to have_datatype 10 | expect(serialized.datatype).to eql RDF::XSD.float 11 | end 12 | 13 | it "should serialize integers to XSD floats" do 14 | serialized = Spira::Types::Float.serialize(5) 15 | expect(serialized).to be_a RDF::Literal 16 | expect(serialized).to have_datatype 17 | expect(serialized.datatype).to eql RDF::XSD.float 18 | end 19 | 20 | end 21 | 22 | context "when unserializing" do 23 | it "should unserialize XSD floats to floats" do 24 | value = Spira::Types::Float.unserialize(RDF::Literal.new(5, datatype: RDF::XSD.float)) 25 | expect(value).to be_a Float 26 | expect(value).to eql 5.0 27 | end 28 | end 29 | 30 | 31 | end 32 | 33 | -------------------------------------------------------------------------------- /spec/types/gYear_spec.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(File.expand_path(__FILE__)) + '/../spec_helper' 2 | 3 | describe Spira::Types::GYear do 4 | 5 | context "when serializing" do 6 | it "should be able to serialize integers to XSD gYear" do 7 | serialized = Spira::Types::GYear.serialize(2005) 8 | expect(serialized).to be_a RDF::Literal 9 | expect(serialized).to have_datatype 10 | expect(serialized.datatype).to eql RDF::XSD.gYear 11 | expect(serialized).to eql RDF::Literal.new(2005, datatype: RDF::XSD.gYear) 12 | end 13 | end 14 | 15 | context "when unserializing" do 16 | it "should unserialize XSD gYear to integers" do 17 | value = Spira::Types::Int.unserialize(RDF::Literal.new(2005, datatype: RDF::XSD.gYear)) 18 | expect(value).to be_a Integer 19 | expect(value).to eql 2005 20 | end 21 | end 22 | 23 | 24 | end 25 | 26 | -------------------------------------------------------------------------------- /spec/types/int_spec.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(File.expand_path(__FILE__)) + '/../spec_helper' 2 | 3 | describe Spira::Types::Int do 4 | 5 | context "when serializing" do 6 | it "should be able to serialize integers to XSD int" do 7 | serialized = Spira::Types::Int.serialize(5) 8 | expect(serialized).to be_a RDF::Literal 9 | expect(serialized).to have_datatype 10 | expect(serialized.datatype).to eql RDF::XSD.int 11 | expect(serialized).to eql RDF::Literal.new(5, datatype: RDF::XSD.int) 12 | end 13 | end 14 | 15 | context "when unserializing" do 16 | it "should unserialize XSD int to integers" do 17 | [5, "5"].each do |num| 18 | value = Spira::Types::Int.unserialize(RDF::Literal.new(num, datatype: RDF::XSD.int)) 19 | expect(value).to be_a Integer 20 | expect(value).to eql num.to_i 21 | end 22 | end 23 | end 24 | 25 | 26 | end 27 | 28 | -------------------------------------------------------------------------------- /spec/types/integer_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe Spira::Types::Integer do 4 | 5 | context "when serializing" do 6 | it "should serialize integers to XSD integers" do 7 | serialized = Spira::Types::Integer.serialize(5) 8 | expect(serialized).to be_a RDF::Literal 9 | expect(serialized).to have_datatype 10 | expect(serialized.datatype).to eql RDF::XSD.integer 11 | expect(serialized).to eql RDF::Literal.new(5) 12 | end 13 | end 14 | 15 | context "when unserializing" do 16 | it "should unserialize XSD integers to integers" do 17 | [5, "5"].each do |num| 18 | value = Spira::Types::Integer.unserialize(RDF::Literal.new(num, datatype: RDF::XSD.integer)) 19 | expect(value).to be_a Integer 20 | expect(value).to eql num.to_i 21 | end 22 | end 23 | end 24 | 25 | 26 | end 27 | 28 | -------------------------------------------------------------------------------- /spec/types/long_spec.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(File.expand_path(__FILE__)) + '/../spec_helper' 2 | 3 | describe Spira::Types::Long do 4 | 5 | context "when serializing" do 6 | it "should be able to serialize integers to XSD long" do 7 | serialized = Spira::Types::Long.serialize(5) 8 | expect(serialized).to be_a RDF::Literal 9 | expect(serialized).to have_datatype 10 | expect(serialized.datatype).to eql RDF::XSD.long 11 | expect(serialized).to eql RDF::Literal.new(5, datatype: RDF::XSD.long) 12 | end 13 | end 14 | 15 | context "when unserializing" do 16 | it "should unserialize XSD int to integers" do 17 | [5, "5"].each do |num| 18 | value = Spira::Types::Long.unserialize(RDF::Literal.new(num, datatype: RDF::XSD.long)) 19 | expect(value).to be_a Integer 20 | expect(value).to eql num.to_i 21 | end 22 | end 23 | end 24 | 25 | 26 | end 27 | 28 | -------------------------------------------------------------------------------- /spec/types/negativeInteger_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe Spira::Types::NegativeInteger do 4 | 5 | context "when serializing" do 6 | it "should serialize integers to XSD negative integers" do 7 | serialized = Spira::Types::NegativeInteger.serialize(-5) 8 | expect(serialized).to be_a RDF::Literal 9 | expect(serialized).to have_datatype 10 | expect(serialized.datatype).to eql RDF::XSD.negativeInteger 11 | expect(serialized).to eq RDF::Literal.new(-5) 12 | end 13 | end 14 | 15 | context "when unserializing" do 16 | it "should unserialize XSD non negative integers to integers" do 17 | [-5, "-5"].each do |num| 18 | value = Spira::Types::NegativeInteger.unserialize(RDF::Literal.new(num, datatype: RDF::XSD.negativeInteger)) 19 | expect(value).to be_a Integer 20 | expect(value).to eql num.to_i 21 | end 22 | end 23 | end 24 | 25 | 26 | end 27 | 28 | -------------------------------------------------------------------------------- /spec/types/nonNegativeInteger_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe Spira::Types::NonNegativeInteger do 4 | 5 | context "when serializing" do 6 | it "should serialize integers to XSD non negative integers" do 7 | serialized = Spira::Types::NonNegativeInteger.serialize(5) 8 | expect(serialized).to be_a RDF::Literal 9 | expect(serialized).to have_datatype 10 | expect(serialized.datatype).to eql RDF::XSD.nonNegativeInteger 11 | expect(serialized).to eq RDF::Literal.new(5) 12 | end 13 | end 14 | 15 | context "when unserializing" do 16 | it "should unserialize XSD non negative integers to integers" do 17 | [5, "5"].each do |num| 18 | value = Spira::Types::NonNegativeInteger.unserialize(RDF::Literal.new(num, datatype: RDF::XSD.nonNegativeInteger)) 19 | expect(value).to be_a Integer 20 | expect(value).to eql num.to_i 21 | end 22 | end 23 | end 24 | 25 | 26 | end 27 | 28 | -------------------------------------------------------------------------------- /spec/types/nonPositiveInteger_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe Spira::Types::NonPositiveInteger do 4 | 5 | context "when serializing" do 6 | it "should serialize integers to XSD non positive integers" do 7 | serialized = Spira::Types::NonPositiveInteger.serialize(-5) 8 | expect(serialized).to be_a RDF::Literal 9 | expect(serialized).to have_datatype 10 | expect(serialized.datatype).to eql RDF::XSD.nonPositiveInteger 11 | expect(serialized).to eq RDF::Literal.new(-5) 12 | end 13 | end 14 | 15 | context "when unserializing" do 16 | it "should unserialize XSD non positive integers to integers" do 17 | [-5, "-5"].each do |num| 18 | value = Spira::Types::NonPositiveInteger.unserialize(RDF::Literal.new(num, datatype: RDF::XSD.nonPositiveInteger)) 19 | expect(value).to be_a Integer 20 | expect(value).to eql num.to_i 21 | end 22 | end 23 | end 24 | 25 | 26 | end 27 | 28 | -------------------------------------------------------------------------------- /spec/types/positiveInteger_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe Spira::Types::PositiveInteger do 4 | 5 | context "when serializing" do 6 | it "should serialize integers to XSD negative integers" do 7 | serialized = Spira::Types::PositiveInteger.serialize(5) 8 | expect(serialized).to be_a RDF::Literal 9 | expect(serialized).to have_datatype 10 | expect(serialized.datatype).to eql RDF::XSD.positiveInteger 11 | expect(serialized).to eq RDF::Literal.new(5) 12 | end 13 | end 14 | 15 | context "when unserializing" do 16 | it "should unserialize XSD non negative integers to integers" do 17 | [5, "5"].each do |num| 18 | value = Spira::Types::PositiveInteger.unserialize(RDF::Literal.new(num, datatype: RDF::XSD.positiveInteger)) 19 | expect(value).to be_a Integer 20 | expect(value).to eql num.to_i 21 | end 22 | end 23 | end 24 | 25 | 26 | end 27 | 28 | -------------------------------------------------------------------------------- /spec/types/string_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe Spira::Types::String do 4 | 5 | context "when serializing" do 6 | it "should serialize strings to XSD strings" do 7 | serialized = Spira::Types::String.serialize("a string") 8 | expect(serialized).to be_a RDF::Literal 9 | expect(serialized).to eql RDF::Literal.new("a string") 10 | end 11 | 12 | it "should serialize other types to XSD strings" do 13 | serialized = Spira::Types::String.serialize(5) 14 | expect(serialized).to be_a RDF::Literal 15 | expect(serialized).to eql RDF::Literal.new("5") 16 | end 17 | end 18 | 19 | context "when unserializing" do 20 | it "should unserialize XSD strings to strings" do 21 | value = Spira::Types::String.unserialize(RDF::Literal.new("a string", datatype: RDF::XSD.string)) 22 | expect(value).to be_a String 23 | expect(value).to eql "a string" 24 | end 25 | 26 | it "should unserialize anything else to a string" do 27 | value = Spira::Types::String.unserialize(RDF::Literal.new(5, datatype: RDF::XSD.integer)) 28 | expect(value).to be_a String 29 | expect(value).to eql "5" 30 | end 31 | end 32 | 33 | 34 | end 35 | 36 | -------------------------------------------------------------------------------- /spec/types/time_spec.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(File.expand_path(__FILE__)) + '/../spec_helper' 2 | 3 | describe Spira::Types::Time do 4 | 5 | let!(:time) {Time.now} 6 | 7 | context "when serializing" do 8 | it "should serialize times to XSD times" do 9 | serialized = Spira::Types::Time.serialize(time) 10 | expect(serialized).to be_a RDF::Literal 11 | expect(serialized).to have_datatype 12 | expect(serialized.datatype).to eql RDF::XSD.time 13 | expect(serialized).to eql RDF::Literal.new(time, datatype: RDF::XSD.time) 14 | end 15 | end 16 | 17 | context "when unserializing" do 18 | it "should unserialize XSD times to times" do 19 | value = Spira::Types::Time.unserialize(RDF::Literal.new(time, datatype: RDF::XSD.time)) 20 | expect(value.strftime("%H:%M:%S")).to eq time.strftime("%H:%M:%S") 21 | end 22 | end 23 | end -------------------------------------------------------------------------------- /spec/types/uri_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe Spira::Types::URI do 4 | 5 | before :each do 6 | @uri = RDF::URI('http://example.org/example') 7 | end 8 | 9 | context "when serializing" do 10 | it "should serialize URIs to URIs" do 11 | serialized = Spira::Types::URI.serialize(@uri) 12 | expect(serialized).to be_a RDF::URI 13 | expect(serialized).to eql @uri 14 | end 15 | 16 | it "should serialize non-URIs to URIs based on the URI constructor" do 17 | serialized = Spira::Types::URI.serialize("test") 18 | expect(serialized).to be_a RDF::URI 19 | expect(serialized).to eql RDF::URI('test') 20 | end 21 | 22 | end 23 | 24 | context "when unserializing" do 25 | it "should unserialize URIs to themselves" do 26 | value = Spira::Types::URI.unserialize(@uri) 27 | expect(value).to be_a RDF::URI 28 | expect(value).to eql @uri 29 | end 30 | 31 | it "should unserialize non-URIs to URIs based on the URI constructor" do 32 | value = Spira::Types::URI.unserialize("test") 33 | expect(value).to be_a RDF::URI 34 | expect(value).to eql RDF::URI('test') 35 | end 36 | end 37 | 38 | 39 | end 40 | 41 | -------------------------------------------------------------------------------- /spec/update_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | # Tests update functionality--update, save, destroy 4 | 5 | describe Spira do 6 | 7 | before :all do 8 | class ::UpdateTest < Spira::Base 9 | configure base_uri: "http://example.org/example/people" 10 | property :name, predicate: RDF::RDFS.label 11 | property :age, predicate: RDF::Vocab::FOAF.age, type: Integer 12 | end 13 | end 14 | 15 | let(:test_uri) {RDF::URI('http://example.org/example/people')} 16 | let(:update_repo) { 17 | RDF::Repository.new do |repo| 18 | repo << RDF::Statement.new(test_uri, RDF::RDFS.label, 'Test') 19 | repo << RDF::Statement.new(test_uri, RDF::Vocab::FOAF.age, 15) 20 | end 21 | } 22 | let(:test) {UpdateTest.for(test_uri)} 23 | before {Spira.repository = update_repo} 24 | 25 | context "when updating" do 26 | 27 | context "via individual setters" do 28 | it "should allow setting properties" do 29 | test.name = 'Testing 1 2 3' 30 | expect(test.name).to eql 'Testing 1 2 3' 31 | end 32 | 33 | it "should return the newly set value" do 34 | expect(test.name = 'Testing 1 2 3').to eql 'Testing 1 2 3' 35 | expect(test.name).to eql 'Testing 1 2 3' 36 | end 37 | end 38 | 39 | context "via #update" do 40 | it "should allow setting a single property" do 41 | test.update_attributes(name: "Testing") 42 | expect(test.name).to eql "Testing" 43 | end 44 | 45 | it "should allow setting multiple properties" do 46 | test.update_attributes(name: "Testing", age: 10) 47 | expect(test.name).to eql "Testing" 48 | expect(test.age).to eql 10 49 | end 50 | 51 | it "should return self on success" do 52 | expect(test.update_attributes(name: "Testing", age: 10)).to eql test 53 | end 54 | end 55 | end 56 | 57 | context "when saving" do 58 | # @see validations.spec 59 | context "via #save!" do 60 | it "should save a resource's statements to the repository" do 61 | test.name = "Save" 62 | test.save! 63 | expect(update_repo).to have_statement(RDF::Statement.new(test_uri,RDF::RDFS.label,"Save")) 64 | end 65 | 66 | it "should return self on success" do 67 | test.name = "Save" 68 | expect(test.save).to eql test 69 | end 70 | 71 | it "should raise an exception on failure" do 72 | expect(test).to receive(:create_or_update).once.and_return(false) 73 | test.name = "Save" 74 | expect { test.save! }.to raise_error Spira::RecordNotSaved 75 | end 76 | 77 | it "should delete all existing statements for updated properties to the repository" do 78 | update_repo << RDF::Statement.new(test_uri, RDF::RDFS.label, 'Test 1') 79 | update_repo << RDF::Statement.new(test_uri, RDF::RDFS.label, 'Test 2') 80 | expect(update_repo.query({subject: test_uri, predicate: RDF::RDFS.label}).count).to eql 3 81 | test.name = "Save" 82 | test.save! 83 | expect(update_repo.query({subject: test_uri, predicate: RDF::RDFS.label}).count).to eql 1 84 | expect(update_repo).to have_statement(RDF::Statement.new(test_uri, RDF::RDFS.label, 'Save')) 85 | end 86 | 87 | it "should not update properties unless they are dirty" do 88 | expect(update_repo).to receive(:delete).once.with([test_uri, RDF::RDFS.label, nil]) 89 | test.name = "Save" 90 | test.save! 91 | end 92 | 93 | # Tests for a bug wherein the originally loaded attributes were being 94 | # deleted on save!, not the current ones 95 | it "should safely delete old repository information on updates" do 96 | test.age = 16 97 | test.save! 98 | test.age = 17 99 | test.save! 100 | expect(update_repo.query({subject: test_uri, predicate: RDF::Vocab::FOAF.age}).size).to eql 1 101 | expect(update_repo.first_value({subject: test_uri, predicate: RDF::Vocab::FOAF.age})).to eql "17" 102 | end 103 | 104 | it "should not remove non-model data" do 105 | update_repo << RDF::Statement.new(test_uri, RDF.type, RDF::URI('http://example.org/type')) 106 | test.name = "Testing 1 2 3" 107 | test.save! 108 | expect(update_repo.query({subject: test_uri, predicate: RDF.type}).size).to eql 1 109 | end 110 | 111 | it "should not be changed afterwards" do 112 | test.name = "Test" 113 | test.save! 114 | expect(test).not_to be_changed 115 | end 116 | 117 | it "removes items set to nil from the repository" do 118 | test.name = nil 119 | test.save! 120 | expect(update_repo.query({subject: test_uri, predicate: RDF::RDFS.label}).size).to eql 0 121 | end 122 | 123 | end 124 | end 125 | 126 | context "when destroying" do 127 | context "after destroyed" do 128 | before { test.destroy! } 129 | 130 | it "should be able to validate" do 131 | expect(test).to be_valid 132 | end 133 | 134 | it "should be frozen" do 135 | expect(test).to be_frozen 136 | end 137 | end 138 | 139 | context "via #destroy" do 140 | before :each do 141 | update_repo << RDF::Statement.new(test_uri, RDF::Vocab::FOAF.name, 'Not in model') 142 | update_repo << RDF::Statement.new(RDF::URI('http://example.org/test'), RDF::RDFS.seeAlso, test_uri) 143 | end 144 | 145 | it "should return true on success" do 146 | expect(test.destroy).to be_truthy 147 | end 148 | 149 | it "should return false on failure" do 150 | expect(update_repo).to receive(:delete).once.and_return(nil) 151 | expect(test.destroy).to be_falsey 152 | end 153 | 154 | it "should raise an exception on failure" do 155 | expect(update_repo).to receive(:delete).once.and_return(nil) 156 | expect { test.destroy! }.to raise_error Spira::RecordNotSaved 157 | end 158 | 159 | it "should delete all statements in the model" do 160 | test.destroy! 161 | expect(update_repo).not_to have_predicate(RDF::RDFS.label) 162 | expect(update_repo).not_to have_predicate(RDF::Vocab::FOAF.age) 163 | end 164 | 165 | it "should delete all statements not in the model where it is referred to as object" do 166 | test.destroy! 167 | expect(update_repo).not_to have_predicate(RDF::RDFS.seeAlso) 168 | end 169 | 170 | it "should not delete statements with predicates not defined in the model" do 171 | test.destroy! 172 | expect(update_repo).to have_predicate(RDF::Vocab::FOAF.name) 173 | end 174 | 175 | end 176 | 177 | end 178 | 179 | context "when copying" do 180 | let(:new_uri) {RDF::URI('http://example.org/people/test2')} 181 | before {update_repo << RDF::Statement.new(test_uri, RDF::Vocab::FOAF.name, 'Not in model')} 182 | 183 | context "with #copy" do 184 | it "supports #copy" do 185 | expect(test.respond_to?(:copy)).to be_truthy 186 | end 187 | 188 | it "copies to a given subject" do 189 | new = test.copy(new_uri) 190 | expect(new.subject).to eql new_uri 191 | end 192 | 193 | it "copies model data" do 194 | new = test.copy(new_uri) 195 | expect(new.name).to eql test.name 196 | expect(new.age).to eql test.age 197 | end 198 | 199 | end 200 | 201 | context "with #copy!" do 202 | it "supports #copy!" do 203 | expect(test.respond_to?(:copy!)).to be_truthy 204 | end 205 | 206 | it "copies to a given subject" do 207 | new = test.copy!(new_uri) 208 | expect(new.subject).to eql new_uri 209 | end 210 | 211 | it "copies model data" do 212 | new = test.copy!(new_uri) 213 | expect(new.name).to eql test.name 214 | expect(new.age).to eql test.age 215 | end 216 | 217 | it "saves the copy immediately" do 218 | new = test.copy!(new_uri) 219 | expect(update_repo).to have_statement RDF::Statement.new(new_uri, RDF::RDFS.label, test.name) 220 | expect(update_repo).to have_statement RDF::Statement.new(new_uri, RDF::Vocab::FOAF.age, test.age) 221 | end 222 | end 223 | end 224 | 225 | end 226 | -------------------------------------------------------------------------------- /spec/utils_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe Spira::Utils do 4 | class ::Person < Spira::Base 5 | configure base_uri: "http://example.org/example/people" 6 | property :name, predicate: RDF::RDFS.label 7 | property :age, predicate: RDF::Vocab::FOAF.age, type: Integer 8 | end 9 | 10 | let(:repository) do 11 | repo = RDF::Repository.load(fixture('bob.nt')) 12 | repo << RDF::Statement.new(test_uri, RDF::Vocab::FOAF.name, 'Not in model') 13 | repo 14 | end 15 | before(:each) {Spira.repository = repository} 16 | 17 | describe "rename!" do 18 | let(:test_uri) {RDF::URI('http://example.org/example/people/bob')} 19 | let(:new_uri) {RDF::URI('http://example.org/people/test2')} 20 | let(:other_uri) {RDF::URI('http://example.org/people/test3')} 21 | let(:test) {Person.for(test_uri)} 22 | let(:name) {test.name} 23 | let(:age) {test.age} 24 | subject {test} 25 | 26 | it "supports #rename!" do 27 | is_expected.to respond_to(:rename!) 28 | end 29 | 30 | it "copies model data to a given subject" do 31 | subject.rename!(new_uri) 32 | expect(subject.name).to eql name 33 | expect(subject.age).to eql age 34 | end 35 | 36 | it "updates references to the old subject as objects" do 37 | subject.rename!(new_uri) 38 | expect(repository).to have_statement RDF::Statement(new_uri, RDF::RDFS.label, name) 39 | expect(repository).not_to have_statement RDF::Statement(test_uri, RDF::RDFS.label, name) 40 | end 41 | 42 | it "saves the copy immediately" do 43 | subject.rename!(new_uri) 44 | expect(subject.name).to eql name 45 | expect(subject.age).to eql age 46 | expect(repository).to have_statement RDF::Statement.new(new_uri, RDF::RDFS.label, name) 47 | expect(repository).to have_statement RDF::Statement.new(new_uri, RDF::Vocab::FOAF.age, age) 48 | end 49 | 50 | it "deletes the old model data" do 51 | subject.rename!(new_uri) 52 | expect(repository).not_to have_statement RDF::Statement.new(test_uri, RDF::RDFS.label, name) 53 | expect(repository).not_to have_statement RDF::Statement.new(test_uri, RDF::Vocab::FOAF.age, age) 54 | end 55 | 56 | it "copies non-model data to the given subject" do 57 | subject.rename!(new_uri) 58 | expect(repository).to have_statement RDF::Statement.new(new_uri, RDF::Vocab::FOAF.name, 'Not in model') 59 | end 60 | 61 | it "deletes all data about the old subject" do 62 | subject.rename!(new_uri) 63 | expect(repository.query({subject: test_uri}).size).to eql 0 64 | expect(repository.query({object: test_uri}).size).to eql 0 65 | end 66 | end 67 | end 68 | -------------------------------------------------------------------------------- /spec/validations_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe 'A Spira resource' do 4 | let(:uri) {RDF::URI.intern('http://example.org/bank1')} 5 | let(:valid) {V2.for(uri, title: 'xyz')} 6 | let(:invalid) {V2.for(uri, title: 'not xyz')} 7 | 8 | class ::Bank < Spira::Base 9 | configure default_vocabulary: RDF::URI.new('http://example.org/banks/vocab') 10 | 11 | property :title, predicate: RDF::RDFS.label 12 | property :balance, type: Integer 13 | 14 | validate :validate_bank 15 | 16 | def validate_bank 17 | errors.add(:title, "must not be blank") if title.blank? 18 | errors.add(:balance, "must be a number") unless balance.is_a?(Numeric) 19 | end 20 | end 21 | 22 | before do 23 | Spira.repository = RDF::Repository.new 24 | end 25 | 26 | context "when validating" do 27 | class ::V2 < Spira::Base 28 | property :title, predicate: RDF::Vocab::DC.title 29 | validate :title_is_bad 30 | 31 | def title_is_bad 32 | errors.add(:title, "is not xyz") unless title == "xyz" 33 | end 34 | end 35 | 36 | context "with #validate" do 37 | it "returns true when the model is valid" do 38 | expect(valid).to be_valid 39 | end 40 | 41 | it "returns an empty errors object after validating when the model is valid" do 42 | valid.valid? 43 | expect(valid.errors).to be_empty 44 | end 45 | 46 | it "returns false when the model is invalid" do 47 | expect(invalid).not_to be_valid 48 | end 49 | 50 | it "returns an errors object with errors after validating when the model is invalid" do 51 | invalid.valid? 52 | expect(invalid.errors).not_to be_empty 53 | end 54 | end 55 | 56 | context "an invalid model" do 57 | before :each do 58 | invalid.valid? 59 | end 60 | 61 | it "returns a non-empty errors object afterwards" do 62 | expect(invalid.errors).not_to be_empty 63 | end 64 | 65 | it "has an error for an invalid field" do 66 | expect(invalid.errors[:title]).not_to be_empty 67 | end 68 | 69 | it "has the correct error string for the invalid field" do 70 | expect(invalid.errors[:title].first).to eql 'is not xyz' 71 | end 72 | 73 | end 74 | end 75 | 76 | context "when saving with validations, " do 77 | it "does not save an invalid model" do 78 | bank = Bank.for RDF::URI.new('http://example.org/banks/bank1') 79 | expect { bank.save! }.to raise_error Spira::RecordInvalid 80 | end 81 | 82 | it "saves a valid model" do 83 | bank = Bank.for RDF::URI.new('http://example.org/banks/bank1') 84 | bank.title = "A bank" 85 | bank.balance = 1000 86 | expect { bank.save! }.not_to raise_error 87 | end 88 | end 89 | 90 | context "using the included validation" do 91 | describe "validates_inclusion_of" do 92 | 93 | before :all do 94 | class ::V1 < Spira::Base 95 | property :title, predicate: RDF::Vocab::DC.title 96 | validates_inclusion_of :title, in: ["xyz"] 97 | end 98 | end 99 | 100 | let(:v1) {V1.for RDF::URI.new('http://example.org/v1/first')} 101 | 102 | it "does not save when the assertion is false" do 103 | v1.title = 'abc' 104 | expect { v1.save! }.to raise_error Spira::RecordInvalid 105 | end 106 | 107 | it "saves when the assertion is true" do 108 | v1.title = 'xyz' 109 | expect { v1.save! }.not_to raise_error 110 | end 111 | end 112 | 113 | describe "validates_uniqueness_of" do 114 | before :all do 115 | class ::V4 < Spira::Base 116 | type RDF::Vocab::FOAF.Person 117 | property :name, predicate: RDF::Vocab::DC.title 118 | validates_uniqueness_of :name 119 | end 120 | end 121 | 122 | before do 123 | v1 = V4.for RDF::URI.new('http://example.org/v2/first') 124 | v1.name = "unique name" 125 | v1.save 126 | end 127 | 128 | it "should not have errors on name for the same record" do 129 | v1 = V4.for RDF::URI.new('http://example.org/v2/first') 130 | v1.name = v1.name 131 | v1.save 132 | expect(v1.errors[:name]).to be_empty 133 | end 134 | 135 | it "should have errors on :name" do 136 | v2 = V4.for RDF::URI.new('http://example.org/v2/second') 137 | v2.name = "unique name" 138 | v2.save 139 | expect(v2.errors[:name]).not_to be_empty 140 | end 141 | 142 | it "should have no errors on :name" do 143 | v3 = V4.for RDF::URI.new('http://example.org/v2/second') 144 | v3.name = "another name" 145 | v3.save 146 | expect(v3.errors[:name]).to be_empty 147 | end 148 | end 149 | 150 | describe "validates_presence_of" do 151 | before :all do 152 | class ::V2 < Spira::Base 153 | property :title, predicate: RDF::Vocab::DC.title 154 | validates_presence_of :title 155 | end 156 | end 157 | 158 | let(:v2) {V2.for RDF::URI.new('http://example.org/v2/first')} 159 | 160 | it "does not save when the field is nil" do 161 | expect { v2.save! }.to raise_error Spira::RecordInvalid 162 | end 163 | 164 | it "saves when the field is not nil" do 165 | v2.title = 'xyz' 166 | expect { v2.save! }.not_to raise_error 167 | end 168 | end 169 | 170 | describe "validates_numericality_of" do 171 | before :all do 172 | class ::V3 < Spira::Base 173 | property :title, predicate: RDF::Vocab::DC.title, type: Integer 174 | validates_numericality_of :title 175 | end 176 | end 177 | 178 | let(:v3) {V3.for RDF::URI.new('http://example.org/v3/first')} 179 | 180 | it "does not save when the field is nil" do 181 | expect { v3.save! }.to raise_error Spira::RecordInvalid 182 | end 183 | 184 | it "does not save when the field is not numeric" do 185 | v3.title = 'xyz' 186 | expect { v3.save! }.to raise_error Spira::RecordInvalid 187 | end 188 | 189 | it "saves when the field is numeric" do 190 | v3.title = 15 191 | expect { v3.save! }.not_to raise_error 192 | end 193 | end 194 | end 195 | 196 | end 197 | -------------------------------------------------------------------------------- /spec/vocabulary_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | # testing out default vocabularies 3 | 4 | describe 'default vocabularies' do 5 | 6 | before :all do 7 | Spira.repository = RDF::Repository.new 8 | end 9 | 10 | context "defining classes" do 11 | it "should allow a property without a predicate if there is a default vocabulary" do 12 | expect { 13 | class VocabTestX < Spira::Base 14 | configure default_vocabulary: RDF::URI.new('http://example.org/vocabulary/') 15 | property :test 16 | end 17 | }.not_to raise_error 18 | end 19 | 20 | it "should raise a ResourceDeclarationError to set a property without a default vocabulary" do 21 | expect { 22 | class VocabTestY < Spira::Base 23 | property :test 24 | end 25 | }.to raise_error Spira::ResourceDeclarationError 26 | end 27 | 28 | # FIXME: reexamine this behavior. Static typing in the DSL? Why? Why not create a URI out of anything we can #to_s? 29 | it "should raise a ResourceDelcarationError to set a predicate without a default vocabulary that is not an RDF::URI" do 30 | expect { 31 | class VocabTestY < Spira::Base 32 | property :test, predicate: "http://example.org/test" 33 | end 34 | }.to raise_error Spira::ResourceDeclarationError 35 | end 36 | end 37 | 38 | context "using classes with a default vocabulary" do 39 | 40 | before :all do 41 | class ::Bubble < Spira::Base 42 | configure default_vocabulary: RDF::URI.new('http://example.org/vocab/'), 43 | base_uri: "http://example.org/bubbles/" 44 | property :year, type: Integer 45 | property :name 46 | property :title, predicate: RDF::Vocab::DC.title, type: String 47 | end 48 | end 49 | 50 | let(:year) {RDF::URI.new 'http://example.org/vocab/year'} 51 | let(:name) {RDF::URI.new 'http://example.org/vocab/name'} 52 | 53 | it "should do non-default sets and gets normally" do 54 | bubble = Bubble.for 'tulips' 55 | bubble.year = 1500 56 | bubble.title = "Holland tulip" 57 | bubble.save! 58 | 59 | expect(bubble.title).to eql "Holland tulip" 60 | expect(bubble).to have_predicate RDF::Vocab::DC.title 61 | end 62 | it "should create a predicate for a given property" do 63 | bubble = Bubble.for 'dotcom' 64 | bubble.year = 2000 65 | bubble.name = 'Dot-com boom' 66 | 67 | bubble.save! 68 | expect(bubble).to have_predicate year 69 | expect(bubble).to have_predicate name 70 | end 71 | 72 | context "that ends in a hash seperator" do 73 | before :all do 74 | class ::DefaultVocabVocab < ::RDF::Vocabulary('http://example.org/test#') ; end 75 | 76 | class ::HashVocabTest < Spira::Base 77 | configure default_vocabulary: DefaultVocabVocab, 78 | base_uri: "http://example.org/testing/" 79 | property :name 80 | end 81 | end 82 | 83 | let(:name) {RDF::URI.new 'http://example.org/test#name'} 84 | 85 | it "should correctly not append a slash" do 86 | test = HashVocabTest.for('test1') 87 | test.name = "test1" 88 | test.save! 89 | expect(test).to have_predicate name 90 | end 91 | 92 | end 93 | end 94 | end 95 | -------------------------------------------------------------------------------- /spira.gemspec: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby -rubygems 2 | # -*- encoding: utf-8 -*- 3 | $:.unshift File.expand_path('../lib', __FILE__) 4 | require 'spira/version' 5 | 6 | 7 | Gem::Specification.new do |gem| 8 | gem.version = Spira::VERSION.to_s 9 | gem.date = Time.now.strftime('%Y-%m-%d') 10 | 11 | gem.name = 'spira' 12 | gem.homepage = 'https://github.com/ruby-rdf/spira' 13 | gem.license = 'Unlicense' 14 | gem.summary = 'A framework for using the information in RDF.rb repositories as model objects.' 15 | gem.description = 'Spira is a framework for using the information in RDF.rb repositories as model objects.' 16 | gem.metadata = { 17 | "documentation_uri" => "https://ruby-rdf.github.io/spira", 18 | "bug_tracker_uri" => "https://github.com/ruby-rdf/spira/issues", 19 | "homepage_uri" => "https://github.com/ruby-rdf/spira", 20 | "mailing_list_uri" => "https://lists.w3.org/Archives/Public/public-rdf-ruby/", 21 | "source_code_uri" => "https://github.com/ruby-rdf/spira", 22 | } 23 | 24 | gem.authors = ['Ben Lavender'] 25 | gem.email = 'blavender@gmail.com' 26 | 27 | gem.platform = Gem::Platform::RUBY 28 | gem.files = %w(AUTHORS README.md UNLICENSE VERSION) + Dir.glob('lib/**/*.rb') 29 | gem.require_paths = %w(lib) 30 | gem.has_yardoc = true if gem.respond_to?(:has_yardoc) 31 | 32 | gem.required_ruby_version = '>= 3.0' 33 | gem.requirements = [] 34 | 35 | gem.add_runtime_dependency 'rdf', '~> 3.3' 36 | gem.add_runtime_dependency 'rdf-isomorphic', '~> 3.3' 37 | gem.add_runtime_dependency 'promise', '~> 0.3' 38 | gem.add_runtime_dependency 'activemodel', '~> 7.0' 39 | gem.add_runtime_dependency 'activesupport', '~> 7.0' 40 | gem.add_runtime_dependency 'i18n', '~> 1.14' 41 | 42 | gem.add_development_dependency 'rdf-spec', '~> 3.3' 43 | gem.add_development_dependency 'rdf-turtle', '~> 3.3' 44 | gem.add_development_dependency 'rdf-vocab', '~> 3.3' 45 | gem.add_development_dependency 'rspec', '~> 3.12' 46 | gem.add_development_dependency 'rspec-its', '~> 1.3' 47 | gem.add_development_dependency 'yard', '~> 0.9' 48 | 49 | gem.post_install_message = nil 50 | end 51 | --------------------------------------------------------------------------------