ladder 0.1.2 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: eae01f78a4a1a7f0b6a81fdae649c7b8afff280e
4
- data.tar.gz: d5ea2050c1d1b9972b46c8edb5a0e37d1f9ec206
3
+ metadata.gz: b9a1843bcc9f4d0ff1d006c83df08c744fb96c93
4
+ data.tar.gz: 2c7ed1fbff81db63f50bc3d55ae0999895b409b9
5
5
  SHA512:
6
- metadata.gz: 1715db5f2296eaf36139f553fefdbb5a3d071c578c7fc3ef132cf61023a8641740b4fff28bce5dbf85486067ff5e2e25e71fd8821360bc03834b4791df3812ff
7
- data.tar.gz: 7aaf433f31b7edf78f752f374830e6b4dac9c7f54707c1ffba731fda7b4a918783e64b5d5269a0b163a5ac72c7ec35be397fea0df8c73759e6d2ef492eb6653e
6
+ metadata.gz: c0544b5405bdee76c45d6fe64caa87dad1d4273d54e74bbc7c2779169222ef59570685b0e4d8230e9d2c9abf7965f14c9e4ff43ecb77f18406b53f873fec7a76
7
+ data.tar.gz: c962ddc67778d0ab32653ebead42aab4caff5e911850cf0e42adf2898bdfd5acc7a925c126526844221b7832d37ba95f4a7bdc331247b5dce4cf26ab6337eee5
data/.travis.yml CHANGED
@@ -1,10 +1,14 @@
1
1
  language: ruby
2
+ cache: bundler
3
+ sudo: false
2
4
  rvm:
3
5
  - 1.9.3
4
6
  - 2.1.1
5
7
  # - jruby-19mode
6
- # uncomment this line if your project needs to run something other than `rake`:
7
8
  script: bundle exec rspec spec
9
+ branches:
10
+ only:
11
+ - master
8
12
  services:
9
13
  - mongodb
10
14
  - elasticsearch
data/README.md CHANGED
@@ -4,20 +4,22 @@
4
4
 
5
5
  # Ladder
6
6
 
7
- Ladder is a dynamic, scalable metadata framework written in Ruby using well-known components for Linked Data ([ActiveTriples](https://github.com/no-reply/ActiveTriples)/RDF.rb), persistence ([Mongoid](http://mongoid.org)/MongoDB), indexing ([ElasticSearch](http://www.elasticsearch.org)), asynchronicity ([Sidekiq](http://sidekiq.org)/Redis) and HTTP interaction ([Padrino](http://www.padrinorb.com)/Sinatra). It is designed around the following philosophical goals:
8
-
9
- - make it as modular as possible
10
- - use as much commodity tooling as possible
11
- - make it as easy to use as possible
7
+ Ladder is a dynamic [Linked Data](http://en.wikipedia.org/wiki/Linked_data) framework for ActiveModel implemented as a series of Ruby modules that can be used individually and incorporated within existing frameworks (eg. [Project Hydra](http://projecthydra.org)), or combined as a comprehensive stack.
12
8
 
13
9
  ## History
14
10
 
15
- Ladder was loosely conceived over the course of several years prior to 2011. In early 2012, Ladder began existence as an opportunity to escape from a decade of LAMP development and become familiar with Ruby. From 2012 to late 2013, a closed prototype was built under the auspices of [Deliberate Data](http://deliberatedata.com) as a proof-of-concept to test the feasibility of the design.
16
-
17
- From mid-2014, Ladder is being re-architected as a series of Ruby modules that can be used individually and incorporated within existing Ruby frameworks (eg. [Project Hydra](http://projecthydra.org)), or used together as a comprehensive stack. Ladder is intended to encourage the [LAM](http://en.wikipedia.org/wiki/GLAM_(industry_sector)) community to think less dogmatically about our established (often monolithic and/or niche) toolsets and instead embrace a broader vision of adopting more widely-used technologies.
11
+ Ladder was loosely conceived over the course of several years prior to 2011. In early 2012, Ladder began existence as an opportunity to escape from a decade of LAMP development and become familiar with Ruby. From 2012 to late 2013, a closed prototype was built under the auspices of [Deliberate Data](http://deliberatedata.com) as a proof-of-concept to test the feasibility of the design. Ladder is intended to encourage the [GLAM](http://en.wikipedia.org/wiki/GLAM_(industry_sector)) community to think less dogmatically about established (often monolithic and/or niche) tools and instead embrace a broader vision of adopting more widely-used technologies.
18
12
 
19
13
  For those interested in the historical code, the original [prototype](https://github.com/ladder/ladder/tree/prototype) branch is available, as is an [experimental](https://github.com/ladder/ladder/tree/l2) branch.
20
14
 
15
+ ## Components
16
+
17
+ - Persistence ([Mongoid](http://mongoid.org)/MongoDB)
18
+ - Full-text indexing ([ElasticSearch](http://www.elasticsearch.org))
19
+ - RDF ([ActiveTriples](https://github.com/no-reply/ActiveTriples)/RDF.rb)
20
+ - Asynchronous job execution ([Sidekiq](http://sidekiq.org)/Redis)
21
+ - HTTP interaction ([Padrino](http://www.padrinorb.com)/Sinatra)
22
+
21
23
  ## Installation
22
24
 
23
25
  Add this line to your application's Gemfile:
@@ -36,9 +38,10 @@ Or install it yourself as:
36
38
 
37
39
  ## Usage
38
40
 
39
- * [Resources](#resource)
41
+ * [Resources](#resources)
40
42
  * [Configuring Resources](#configuring-resources)
41
43
  * [Dynamic Resources](#dynamic-resources)
44
+ * [Files](#files)
42
45
  * [Indexing for Search](#indexing-for-search)
43
46
 
44
47
  ### Resources
@@ -84,7 +87,7 @@ steve.as_jsonld
84
87
  # }
85
88
  ```
86
89
 
87
- The `#property` method takes care of setting both Mongoid fields and ActiveTriples properties. Properties with literal values are localized by default. Properties with a supplied `:class_name` will create a has-and-belongs-to-many (HABTM) relation:
90
+ The `#property` method takes care of setting both Mongoid fields and ActiveTriples properties. Properties with literal values are localized by default. Properties with a supplied `class_name:` will create a has-and-belongs-to-many (HABTM) relation:
88
91
 
89
92
  ```ruby
90
93
  class Person
@@ -337,6 +340,73 @@ steve.as_jsonld
337
340
 
338
341
  Note that due to the way Mongoid handles dynamic fields, dynamic properties **can not** be localized. They can be any kind of literal, but they **can not** be a related object. They can, however, contain a reference to the related object's URI.
339
342
 
343
+ ### Files
344
+
345
+ Files are bytestreams that store binary content using MongoDB's GridFS storage system. They are still identifiable by a URI, and contain technical metadata about the File's contents.
346
+
347
+ ```ruby
348
+ class Person
349
+ include Ladder::Resource
350
+
351
+ configure type: RDF::FOAF.Person
352
+
353
+ property :first_name, predicate: RDF::FOAF.name
354
+ property :thumbnails, predicate: RDF::FOAF.depiction, class_name: 'Image', inverse_of: nil
355
+ end
356
+
357
+ class Image
358
+ include Ladder::File
359
+ end
360
+ ```
361
+
362
+ Similar to Resources, using `#property` as above will create a has-many relation for a File by default; however, because Files must be the target of a one-way relation, the `inverse_of: nil` option is required. Note that due to the way GridFS is designed, Files **can not** be embedded.
363
+
364
+ ```ruby
365
+ steve = Person.new(first_name: 'Steve')
366
+ thumb = Image.new(file: open('http://some.image/pic.jpg'))
367
+ steve.thumbnails << thumb
368
+
369
+ steve.as_jsonld
370
+ # => {
371
+ # "@context": {
372
+ # "foaf": "http://xmlns.com/foaf/0.1/"
373
+ # },
374
+ # "@id": "http://example.org/people/549d83c64169720b32010000",
375
+ # "@type": "foaf:Person",
376
+ # "foaf:depiction": {
377
+ # "@id": "http://example.org/images/549d83c24169720b32000000"
378
+ # },
379
+ # "foaf:name": {
380
+ # "@language": "en",
381
+ # "@value": "Steve"
382
+ # }
383
+ # }
384
+
385
+ steve.save
386
+ # ... File is stored to GridFS ...
387
+ => true
388
+ ```
389
+
390
+ Files have all the attributes of a GridFS file, and the stored binary content is accessed using `#data`.
391
+
392
+ ```ruby
393
+ thumb.reload
394
+ thumb.as_document
395
+ => {"_id"=>BSON::ObjectId('549d86184169720b6a000000'),
396
+ "length"=>59709,
397
+ "chunkSize"=>4194304,
398
+ "uploadDate"=>2014-12-26 16:00:29 UTC,
399
+ "md5"=>"0d4a486e2cd71c51b7a92cfe96f29324",
400
+ "contentType"=>"image/jpeg",
401
+ "filename"=>"549d86184169720b6a000000/open-uri20141226-2922-u66ap6"}
402
+
403
+ thumb.length
404
+ => 59709
405
+
406
+ thumb.data
407
+ => # ... binary data ...
408
+ ```
409
+
340
410
  ### Indexing for Search
341
411
 
342
412
  You can also index your model classes for keyword searching through ElasticSearch by mixing in the Ladder::Searchable module:
@@ -447,7 +517,6 @@ Person.property :projects, predicate: RDF::FOAF.made, class_name: 'Project'
447
517
 
448
518
  es = Project.new(project_name: 'ElasticSearch', description: 'You know, for search')
449
519
  es.developers << kimchy
450
- es.save
451
520
 
452
521
  Person.index_for_search as: :jsonld, related: true
453
522
  => :as_indexed_json
@@ -594,6 +663,8 @@ MJ Suhonos / mj@suhonos.ca
594
663
 
595
664
  ## Acknowledgements
596
665
 
666
+ My biggest thanks to all the wonderful people who have shown interest and support for Ladder over the years.
667
+
597
668
  Many thanks to Christopher Knight [@NomadicKnight](https://twitter.com/Nomadic_Knight) for ceding the "ladder" gem name. Check out his startup, [Adventure Local](http://advlo.com) / [@advlo_](https://twitter.com/Advlo_).
598
669
 
599
670
  ## License
data/ladder.gemspec CHANGED
@@ -1,4 +1,4 @@
1
- # coding: utf-8
1
+ # encoding: utf-8
2
2
  lib = File.expand_path('../lib', __FILE__)
3
3
  $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
4
  require 'ladder/version'
@@ -21,14 +21,16 @@ Gem::Specification.new do |spec|
21
21
  spec.require_paths = ["lib"]
22
22
 
23
23
  spec.add_dependency "mongoid", "~> 4.0"
24
+ spec.add_dependency "mongoid-grid_fs", "~> 2.1"
24
25
  spec.add_dependency "active-triples", "~> 0.4"
25
26
  spec.add_dependency "elasticsearch-model", "~> 0.1"
26
-
27
+ spec.add_dependency "mimemagic", "~> 0.2"
28
+
27
29
  spec.add_development_dependency "bundler", "~> 1.7"
28
30
  spec.add_development_dependency "pry", "~> 0.10"
29
31
  spec.add_development_dependency "wirble", "~> 0.1"
30
32
  spec.add_development_dependency "rspec", "~> 3.1"
31
- spec.add_development_dependency "rake", "~> 10.3"
33
+ spec.add_development_dependency "rake", "~> 10.4"
32
34
  spec.add_development_dependency "yard", "~> 0.8"
33
35
  spec.add_development_dependency "simplecov", "~> 0.9"
34
36
  end
data/lib/ladder.rb CHANGED
@@ -1,6 +1,7 @@
1
1
  require "ladder/version"
2
2
 
3
3
  module Ladder
4
- autoload :Resource, 'ladder/resource'
4
+ autoload :File, 'ladder/file'
5
+ autoload :Resource, 'ladder/resource'
5
6
  autoload :Searchable, 'ladder/searchable'
6
7
  end
@@ -0,0 +1,59 @@
1
+ require 'mongoid/grid_fs'
2
+ require 'active_triples'
3
+
4
+ module Ladder::File
5
+ extend ActiveSupport::Concern
6
+
7
+ include Mongoid::Document
8
+ include ActiveTriples::Identifiable
9
+
10
+ included do
11
+ configure base_uri: RDF::URI.new(LADDER_BASE_URI) / name.underscore.pluralize if defined? LADDER_BASE_URI
12
+
13
+ store_in :collection => "#{ grid.prefix }.files"
14
+
15
+ # Define accessor methods for attributes
16
+ define_method(:content_type) { read_attribute(:contentType) }
17
+
18
+ grid::File.fields.keys.map(&:to_sym).each do |attr|
19
+ define_method(attr) { read_attribute(attr) }
20
+ end
21
+ end
22
+
23
+ attr_accessor :file
24
+
25
+ ##
26
+ # Make save behave like Mongoid::Document as much as possible
27
+ def save
28
+ raise Mongoid::Errors::InvalidValue.new(IO, NilClass) if file.nil?
29
+
30
+ attributes[:content_type] = file.content_type if file.respond_to? :content_type
31
+ @grid_file ? @grid_file.save : !! @grid_file = self.class.grid.put(file, attributes.symbolize_keys)
32
+ end
33
+
34
+ ##
35
+ # Output content of object from stored file or readable input
36
+ def data
37
+ @grid_file ||= self.class.grid.get(id) if persisted?
38
+ return @grid_file.data if @grid_file
39
+
40
+ file.rewind if file.respond_to? :rewind
41
+ file.read
42
+ end
43
+
44
+ ##
45
+ # Return an empty ActiveTriples resource for serializing related resources
46
+ def update_resource
47
+ resource
48
+ end
49
+
50
+ module ClassMethods
51
+ ##
52
+ # Create a namespaced GridFS module for this class
53
+ def grid
54
+ @grid ||= Mongoid::GridFs.build_namespace_for name
55
+ end
56
+
57
+ end
58
+
59
+ end
@@ -36,7 +36,6 @@ module Ladder::Resource::Dynamic
36
36
 
37
37
  # Ensure new field name is unique
38
38
  field_name = opts.first[:predicate].qname.join('_').to_sym if respond_to? field_name or :name == field_name
39
-
40
39
  self._context[field_name] = opts.first[:predicate].to_s
41
40
 
42
41
  apply_context
@@ -62,11 +61,10 @@ module Ladder::Resource::Dynamic
62
61
  end
63
62
 
64
63
  # Set the value in Mongoid
65
- value = case data.object
66
- when RDF::Literal
67
- data.object.object
68
- else
69
- data.object.to_s
64
+ value = if data.object.is_a? RDF::Literal
65
+ data.object.object
66
+ else
67
+ data.object.to_s
70
68
  end
71
69
 
72
70
  self.send("#{field_name}=", value)
@@ -77,13 +75,8 @@ module Ladder::Resource::Dynamic
77
75
  ##
78
76
  # Dynamic field accessors (Mongoid)
79
77
  def create_accessors(field_name)
80
- define_singleton_method field_name do
81
- read_attribute(field_name)
82
- end
83
-
84
- define_singleton_method "#{field_name}=" do |value|
85
- write_attribute(field_name, value)
86
- end
78
+ define_singleton_method(field_name) { read_attribute(field_name) }
79
+ define_singleton_method("#{field_name}=") { |value| write_attribute(field_name, value) }
87
80
  end
88
81
 
89
82
  ##
@@ -108,7 +101,6 @@ module Ladder::Resource::Dynamic
108
101
  module ClassMethods
109
102
 
110
103
  private
111
-
112
104
  ##
113
105
  # Overload ActiveTriples #resource_class
114
106
  #
@@ -118,4 +110,4 @@ module Ladder::Resource::Dynamic
118
110
  end
119
111
  end
120
112
 
121
- end
113
+ end
@@ -1,3 +1,3 @@
1
1
  module Ladder
2
- VERSION = "0.1.2"
2
+ VERSION = "0.2.0"
3
3
  end
@@ -0,0 +1,42 @@
1
+ require 'spec_helper'
2
+
3
+ describe Ladder::File do
4
+ before do
5
+ Mongoid.load!('mongoid.yml', :development)
6
+ Mongoid.logger.level = Moped.logger.level = Logger::DEBUG
7
+ Mongoid.purge!
8
+
9
+ LADDER_BASE_URI = 'http://example.org'
10
+
11
+ class Datastream
12
+ include Ladder::File
13
+ end
14
+ end
15
+
16
+ context 'with data from file' do
17
+ TEST_FILE = './spec/shared/moomin.pdf'
18
+
19
+ let(:subject) { Datastream.new file: open(TEST_FILE) }
20
+ let(:source) { open(TEST_FILE).read } # ASCII-8BIT (binary)
21
+
22
+ it_behaves_like 'a File'
23
+ end
24
+
25
+ context 'with data from string after creation' do
26
+ data = "And so Moomintroll was helplessly thrown out into a strange and dangerous world and dropped up to his ears in the first snowdrift of his experience. It felt unpleasantly prickly to his velvet skin, but at the same time his nose caught a new smell. It was a more serious smell than any he had met before, and slightly frightening. But it made him wide awake and greatly interested."
27
+
28
+ let(:subject) { Datastream.new }
29
+ let(:source) { data } # UTF-8 (string)
30
+
31
+ before do
32
+ subject.file = StringIO.new(source)
33
+ end
34
+
35
+ it_behaves_like 'a File'
36
+ end
37
+
38
+ after do
39
+ Object.send(:remove_const, :LADDER_BASE_URI) if Object
40
+ Object.send(:remove_const, "Datastream") if Object
41
+ end
42
+ end
@@ -23,7 +23,196 @@ describe Ladder::Searchable do
23
23
  end
24
24
 
25
25
  it_behaves_like 'a Resource'
26
- it_behaves_like 'a Searchable'
26
+
27
+ let(:subject) { Thing.new }
28
+ let(:person) { Person.new }
29
+
30
+ shared_context 'with data' do
31
+ before do
32
+ subject.class.configure type: RDF::DC.BibliographicResource
33
+ subject.class.property :title, :predicate => RDF::DC.title
34
+ subject.title = 'Comet in Moominland'
35
+ end
36
+ end
37
+
38
+ describe '#index_for_search' do
39
+ include_context 'with data'
40
+
41
+ context 'with default' do
42
+ before do
43
+ subject.class.index_for_search
44
+ subject.save
45
+ Elasticsearch::Model.client.indices.flush
46
+ end
47
+
48
+ it 'should exist in the index' do
49
+ results = subject.class.search('title:moomin*')
50
+ expect(results.count).to eq 1
51
+ expect(results.first._source.to_hash).to eq JSON.parse(subject.as_indexed_json.to_json)
52
+ end
53
+ end
54
+
55
+ context 'with as qname' do
56
+ before do
57
+ subject.class.index_for_search as: :qname
58
+ subject.save
59
+ Elasticsearch::Model.client.indices.flush
60
+ end
61
+
62
+ it 'should exist in the index' do
63
+ results = subject.class.search('dc.title.en:moomin*')
64
+ expect(results.count).to eq 1
65
+ expect(results.first._source.to_hash).to eq JSON.parse(subject.as_qname.to_json)
66
+ end
67
+ end
68
+
69
+ context 'with as jsonld' do
70
+ before do
71
+ subject.class.index_for_search as: :jsonld
72
+ subject.save
73
+ Elasticsearch::Model.client.indices.flush
74
+ end
75
+
76
+ it 'should exist in the index' do
77
+ results = subject.class.search('dc\:title.@value:moomin*')
78
+ expect(results.count).to eq 1
79
+ expect(results.first._source.to_hash).to eq subject.as_jsonld
80
+ end
81
+ end
82
+ end
83
+
84
+ describe '#index_for_search related' do
85
+ include_context 'with data'
86
+
87
+ before do
88
+ # related object
89
+ person.class.configure type: RDF::FOAF.Person
90
+ person.class.property :foaf_name, :predicate => RDF::FOAF.name
91
+ person.foaf_name = 'Tove Jansson'
92
+
93
+ # many-to-many relation
94
+ person.class.property :things, :predicate => RDF::DC.relation, :class_name => 'Thing'
95
+ subject.class.property :people, :predicate => RDF::DC.creator, :class_name => 'Person'
96
+ subject.people << person
97
+ end
98
+
99
+ context 'with default' do
100
+ before do
101
+ person.class.index_for_search
102
+ subject.class.index_for_search
103
+ subject.save
104
+ Elasticsearch::Model.client.indices.flush
105
+ end
106
+
107
+ it 'should contain an ID for the related object' do
108
+ results = subject.class.search('person_ids.$oid:' + person.id)
109
+ expect(results.count).to eq 1
110
+ end
111
+
112
+ it 'should include the related object in the index' do
113
+ results = person.class.search('foaf_name:tove')
114
+ expect(results.count).to eq 1
115
+ expect(results.first._source.to_hash).to eq JSON.parse(person.as_indexed_json.to_json)
116
+ end
117
+
118
+ it 'should contain an ID for the subject' do
119
+ results = person.class.search('thing_ids.$oid:' + subject.id)
120
+ expect(results.count).to eq 1
121
+ end
122
+ end
123
+
124
+ context 'with as qname' do
125
+ before do
126
+ person.class.index_for_search as: :qname
127
+ subject.class.index_for_search as: :qname
128
+ subject.save
129
+ Elasticsearch::Model.client.indices.flush
130
+ end
131
+
132
+ it 'should contain an ID for the related object' do
133
+ results = subject.class.search('dc.creator:' + person.id)
134
+ expect(results.count).to eq 1
135
+ end
136
+
137
+ it 'should include the related object in the index' do
138
+ results = person.class.search('foaf.name.en:tove')
139
+ expect(results.count).to eq 1
140
+ expect(results.first._source.to_hash).to eq JSON.parse(person.as_qname.to_json)
141
+ end
142
+
143
+ it 'should contain an ID for the subject' do
144
+ results = person.class.search('dc.relation:' + subject.id)
145
+ expect(results.count).to eq 1
146
+ end
147
+ end
148
+
149
+ context 'with as_qname related' do
150
+ before do
151
+ person.class.index_for_search as: :qname, related: true
152
+ subject.class.index_for_search as: :qname, related: true
153
+ subject.save
154
+ Elasticsearch::Model.client.indices.flush
155
+ end
156
+
157
+ it 'should contain a embedded related object' do
158
+ results = subject.class.search('dc.creator.foaf.name.en:tove')
159
+ expect(results.count).to eq 1
160
+ expect(results.first._source['dc']['creator'].first).to eq Hashie::Mash.new person.as_qname
161
+ end
162
+
163
+ it 'should contain an embedded subject in the related object' do
164
+ results = person.class.search('dc.relation.dc.title.en:moomin*')
165
+ expect(results.count).to eq 1
166
+ expect(results.first._source['dc']['relation'].first).to eq Hashie::Mash.new subject.as_qname
167
+ end
168
+ end
169
+
170
+ context 'with as_jsonld' do
171
+ before do
172
+ person.class.index_for_search as: :jsonld
173
+ subject.class.index_for_search as: :jsonld
174
+ subject.save
175
+ Elasticsearch::Model.client.indices.flush
176
+ end
177
+
178
+ it 'should contain an ID for the related object' do
179
+ results = subject.class.search('dc\:creator.@id:' + person.id)
180
+ expect(results.count).to eq 1
181
+ end
182
+
183
+ it 'should include the related object in the index' do
184
+ results = person.class.search('foaf\:name.@value:tove')
185
+ expect(results.count).to eq 1
186
+ expect(results.first._source.to_hash).to eq person.as_jsonld
187
+ end
188
+
189
+ it 'should contain an ID for the subject' do
190
+ results = person.class.search('dc\:relation.@id:' + subject.id)
191
+ expect(results.count).to eq 1
192
+ end
193
+ end
194
+
195
+ context 'with as_jsonld related' do
196
+ before do
197
+ person.class.index_for_search as: :jsonld, related: true
198
+ subject.class.index_for_search as: :jsonld, related: true
199
+ subject.save
200
+ Elasticsearch::Model.client.indices.flush
201
+ end
202
+
203
+ it 'should contain a embedded related object' do
204
+ results = subject.class.search('dc\:creator.foaf\:name.@value:tove')
205
+ expect(results.count).to eq 1
206
+ expect(results.first._source.to_hash['dc:creator']).to eq person.as_jsonld.except '@context'
207
+ end
208
+
209
+ it 'should contain an embedded subject in the related object' do
210
+ results = person.class.search('dc\:relation.dc\:title.@value:moomin*')
211
+ expect(results.count).to eq 1
212
+ expect(results.first._source.to_hash['dc:relation']).to eq subject.as_jsonld.except '@context'
213
+ end
214
+ end
215
+ end
27
216
 
28
217
  after do
29
218
  Object.send(:remove_const, "Thing") if Object
@@ -0,0 +1,114 @@
1
+ require 'mimemagic'
2
+
3
+ shared_examples 'a File' do
4
+
5
+ shared_context 'with relations' do
6
+ let(:thing) { Thing.new }
7
+
8
+ before do
9
+ class Thing
10
+ include Ladder::Resource
11
+ end
12
+
13
+ # implicit from #property
14
+ thing.class.property :files, :predicate => RDF::DC.relation, :class_name => subject.class.name, :inverse_of => nil
15
+ thing.files << subject
16
+
17
+ # TODO: build some relations of various types
18
+ # explicit using HABTM
19
+ # explicit has-one
20
+ end
21
+
22
+ after do
23
+ Object.send(:remove_const, 'Thing')
24
+ end
25
+ end
26
+
27
+ describe 'LADDER_BASE_URI' do
28
+ it 'should automatically have a base URI' do
29
+ expect(subject.rdf_subject.parent).to eq RDF::URI('http://example.org/datastreams/')
30
+ end
31
+ end
32
+
33
+ describe '#initialize' do
34
+ it 'should have an id' do
35
+ expect(subject.id).to be_kind_of BSON::ObjectId
36
+ end
37
+ end
38
+
39
+ describe '#data' do
40
+ it 'should return a data stream' do
41
+ expect(subject.data).to eq source
42
+ end
43
+ end
44
+
45
+ describe '#save' do
46
+ it 'should persist' do
47
+ expect(subject.save).to be true
48
+ end
49
+ end
50
+
51
+ describe '#find' do
52
+ it 'should be retrievable' do
53
+ subject.save
54
+ found = subject.class.find(subject.id)
55
+
56
+ expect(found).to eq subject
57
+ expect(found.data).to eq subject.data
58
+ expect(found.data).to eq source.force_encoding(found.data.encoding)
59
+ end
60
+ end
61
+
62
+ describe '#attributes' do
63
+ before do
64
+ subject.save
65
+ subject.reload
66
+ end
67
+
68
+ it 'should have a #length' do
69
+ expect(subject.length).to eq source.force_encoding(subject.data.encoding).length
70
+ end
71
+
72
+ it 'should have a #md5' do
73
+ expect(subject.md5).to eq Digest::MD5.hexdigest(source)
74
+ end
75
+
76
+ it 'should have a #content_type' do
77
+ source_type = MimeMagic.by_magic(source).to_s
78
+ expect(subject.content_type).to eq source_type.empty? ? "application/octet-stream" : source_type
79
+ end
80
+ end
81
+
82
+ describe '#update_resource' do
83
+ it 'should not have related objects' do
84
+ expect(subject.resource).to eq subject.update_resource
85
+ end
86
+
87
+ it 'should not have related object relations' do
88
+ expect(subject.resource.statements).to be_empty
89
+ end
90
+ end
91
+
92
+ context 'with one-sided has-many' do
93
+ include_context 'with relations'
94
+
95
+ it 'should have a relation' do
96
+ expect(thing.relations['files'].relation).to eq (Mongoid::Relations::Referenced::ManyToMany)
97
+ expect(thing.files.to_a).to include subject
98
+ end
99
+
100
+ it 'should not have an inverse relation' do
101
+ expect(thing.relations['files'].inverse_of).to be nil
102
+ expect(subject.relations).to be_empty
103
+ end
104
+
105
+ it 'should have a valid predicate' do
106
+ expect(thing.class.properties['files'].predicate).to eq RDF::DC.relation
107
+ end
108
+
109
+ it 'should not have an inverse predicate' do
110
+ expect(subject.class.properties).to be_empty
111
+ end
112
+ end
113
+
114
+ end
Binary file
@@ -61,7 +61,7 @@ shared_examples 'a Resource' do
61
61
  expect(subject.part).to eq part
62
62
  end
63
63
 
64
- it 'should have reverse relations' do
64
+ it 'should have inverse relations' do
65
65
  expect(person.things.to_a).to include subject
66
66
  expect(concept.relations).to be_empty
67
67
  expect(part.thing).to eq subject
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ladder
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.2
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - MJ Suhonos
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2014-11-20 00:00:00.000000000 Z
11
+ date: 2015-01-06 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: mongoid
@@ -24,6 +24,20 @@ dependencies:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
26
  version: '4.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: mongoid-grid_fs
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '2.1'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '2.1'
27
41
  - !ruby/object:Gem::Dependency
28
42
  name: active-triples
29
43
  requirement: !ruby/object:Gem::Requirement
@@ -52,6 +66,20 @@ dependencies:
52
66
  - - "~>"
53
67
  - !ruby/object:Gem::Version
54
68
  version: '0.1'
69
+ - !ruby/object:Gem::Dependency
70
+ name: mimemagic
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '0.2'
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '0.2'
55
83
  - !ruby/object:Gem::Dependency
56
84
  name: bundler
57
85
  requirement: !ruby/object:Gem::Requirement
@@ -114,14 +142,14 @@ dependencies:
114
142
  requirements:
115
143
  - - "~>"
116
144
  - !ruby/object:Gem::Version
117
- version: '10.3'
145
+ version: '10.4'
118
146
  type: :development
119
147
  prerelease: false
120
148
  version_requirements: !ruby/object:Gem::Requirement
121
149
  requirements:
122
150
  - - "~>"
123
151
  - !ruby/object:Gem::Version
124
- version: '10.3'
152
+ version: '10.4'
125
153
  - !ruby/object:Gem::Dependency
126
154
  name: yard
127
155
  requirement: !ruby/object:Gem::Requirement
@@ -167,17 +195,20 @@ files:
167
195
  - Rakefile
168
196
  - ladder.gemspec
169
197
  - lib/ladder.rb
198
+ - lib/ladder/file.rb
170
199
  - lib/ladder/resource.rb
171
200
  - lib/ladder/resource/dynamic.rb
172
201
  - lib/ladder/searchable.rb
173
202
  - lib/ladder/version.rb
174
203
  - logo.png
175
204
  - mongoid.yml
176
- - spec/ladder/dynamic_spec.rb
205
+ - spec/ladder/file_spec.rb
206
+ - spec/ladder/resource/dynamic_spec.rb
177
207
  - spec/ladder/resource_spec.rb
178
208
  - spec/ladder/searchable_spec.rb
209
+ - spec/shared/file.rb
210
+ - spec/shared/moomin.pdf
179
211
  - spec/shared/resource.rb
180
- - spec/shared/searchable.rb
181
212
  - spec/spec_helper.rb
182
213
  homepage: https://github.com/ladder/ladder
183
214
  licenses:
@@ -204,10 +235,12 @@ signing_key:
204
235
  specification_version: 4
205
236
  summary: Opinionated ActiveModel framework.
206
237
  test_files:
207
- - spec/ladder/dynamic_spec.rb
238
+ - spec/ladder/file_spec.rb
239
+ - spec/ladder/resource/dynamic_spec.rb
208
240
  - spec/ladder/resource_spec.rb
209
241
  - spec/ladder/searchable_spec.rb
242
+ - spec/shared/file.rb
243
+ - spec/shared/moomin.pdf
210
244
  - spec/shared/resource.rb
211
- - spec/shared/searchable.rb
212
245
  - spec/spec_helper.rb
213
246
  has_rdoc:
@@ -1,191 +0,0 @@
1
- shared_examples 'a Searchable' do
2
- let(:subject) { Thing.new }
3
- let(:person) { Person.new }
4
-
5
- shared_context 'with data' do
6
- before do
7
- subject.class.configure type: RDF::DC.BibliographicResource
8
- subject.class.property :title, :predicate => RDF::DC.title
9
- subject.title = 'Comet in Moominland'
10
- end
11
- end
12
-
13
- describe '#index_for_search' do
14
- include_context 'with data'
15
-
16
- context 'with default' do
17
- before do
18
- subject.class.index_for_search
19
- subject.save
20
- Elasticsearch::Model.client.indices.flush
21
- end
22
-
23
- it 'should exist in the index' do
24
- results = subject.class.search('title:moomin*')
25
- expect(results.count).to eq 1
26
- expect(results.first._source.to_hash).to eq JSON.parse(subject.as_indexed_json.to_json)
27
- end
28
- end
29
-
30
- context 'with as qname' do
31
- before do
32
- subject.class.index_for_search as: :qname
33
- subject.save
34
- Elasticsearch::Model.client.indices.flush
35
- end
36
-
37
- it 'should exist in the index' do
38
- results = subject.class.search('dc.title.en:moomin*')
39
- expect(results.count).to eq 1
40
- expect(results.first._source.to_hash).to eq JSON.parse(subject.as_qname.to_json)
41
- end
42
- end
43
-
44
- context 'with as jsonld' do
45
- before do
46
- subject.class.index_for_search as: :jsonld
47
- subject.save
48
- Elasticsearch::Model.client.indices.flush
49
- end
50
-
51
- it 'should exist in the index' do
52
- results = subject.class.search('dc\:title.@value:moomin*')
53
- expect(results.count).to eq 1
54
- expect(results.first._source.to_hash).to eq subject.as_jsonld
55
- end
56
- end
57
- end
58
-
59
- describe '#index_for_search related' do
60
- include_context 'with data'
61
-
62
- before do
63
- # related object
64
- person.class.configure type: RDF::FOAF.Person
65
- person.class.property :foaf_name, :predicate => RDF::FOAF.name
66
- person.foaf_name = 'Tove Jansson'
67
-
68
- # many-to-many relation
69
- person.class.property :things, :predicate => RDF::DC.relation, :class_name => 'Thing'
70
- subject.class.property :people, :predicate => RDF::DC.creator, :class_name => 'Person'
71
- subject.people << person
72
- end
73
-
74
- context 'with default' do
75
- before do
76
- person.class.index_for_search
77
- subject.class.index_for_search
78
- subject.save
79
- Elasticsearch::Model.client.indices.flush
80
- end
81
-
82
- it 'should contain an ID for the related object' do
83
- results = subject.class.search('person_ids.$oid:' + person.id)
84
- expect(results.count).to eq 1
85
- end
86
-
87
- it 'should include the related object in the index' do
88
- results = person.class.search('foaf_name:tove')
89
- expect(results.count).to eq 1
90
- expect(results.first._source.to_hash).to eq JSON.parse(person.as_indexed_json.to_json)
91
- end
92
-
93
- it 'should contain an ID for the subject' do
94
- results = person.class.search('thing_ids.$oid:' + subject.id)
95
- expect(results.count).to eq 1
96
- end
97
- end
98
-
99
- context 'with as qname' do
100
- before do
101
- person.class.index_for_search as: :qname
102
- subject.class.index_for_search as: :qname
103
- subject.save
104
- Elasticsearch::Model.client.indices.flush
105
- end
106
-
107
- it 'should contain an ID for the related object' do
108
- results = subject.class.search('dc.creator:' + person.id)
109
- expect(results.count).to eq 1
110
- end
111
-
112
- it 'should include the related object in the index' do
113
- results = person.class.search('foaf.name.en:tove')
114
- expect(results.count).to eq 1
115
- expect(results.first._source.to_hash).to eq JSON.parse(person.as_qname.to_json)
116
- end
117
-
118
- it 'should contain an ID for the subject' do
119
- results = person.class.search('dc.relation:' + subject.id)
120
- expect(results.count).to eq 1
121
- end
122
- end
123
-
124
- context 'with as_qname related' do
125
- before do
126
- person.class.index_for_search as: :qname, related: true
127
- subject.class.index_for_search as: :qname, related: true
128
- subject.save
129
- Elasticsearch::Model.client.indices.flush
130
- end
131
-
132
- it 'should contain a embedded related object' do
133
- results = subject.class.search('dc.creator.foaf.name.en:tove')
134
- expect(results.count).to eq 1
135
- expect(results.first._source['dc']['creator'].first).to eq Hashie::Mash.new person.as_qname
136
- end
137
-
138
- it 'should contain an embedded subject in the related object' do
139
- results = person.class.search('dc.relation.dc.title.en:moomin*')
140
- expect(results.count).to eq 1
141
- expect(results.first._source['dc']['relation'].first).to eq Hashie::Mash.new subject.as_qname
142
- end
143
- end
144
-
145
- context 'with as_jsonld' do
146
- before do
147
- person.class.index_for_search as: :jsonld
148
- subject.class.index_for_search as: :jsonld
149
- subject.save
150
- Elasticsearch::Model.client.indices.flush
151
- end
152
-
153
- it 'should contain an ID for the related object' do
154
- results = subject.class.search('dc\:creator.@id:' + person.id)
155
- expect(results.count).to eq 1
156
- end
157
-
158
- it 'should include the related object in the index' do
159
- results = person.class.search('foaf\:name.@value:tove')
160
- expect(results.count).to eq 1
161
- expect(results.first._source.to_hash).to eq person.as_jsonld
162
- end
163
-
164
- it 'should contain an ID for the subject' do
165
- results = person.class.search('dc\:relation.@id:' + subject.id)
166
- expect(results.count).to eq 1
167
- end
168
- end
169
-
170
- context 'with as_jsonld related' do
171
- before do
172
- person.class.index_for_search as: :jsonld, related: true
173
- subject.class.index_for_search as: :jsonld, related: true
174
- subject.save
175
- Elasticsearch::Model.client.indices.flush
176
- end
177
-
178
- it 'should contain a embedded related object' do
179
- results = subject.class.search('dc\:creator.foaf\:name.@value:tove')
180
- expect(results.count).to eq 1
181
- expect(results.first._source.to_hash['dc:creator']).to eq person.as_jsonld.except '@context'
182
- end
183
-
184
- it 'should contain an embedded subject in the related object' do
185
- results = person.class.search('dc\:relation.dc\:title.@value:moomin*')
186
- expect(results.count).to eq 1
187
- expect(results.first._source.to_hash['dc:relation']).to eq subject.as_jsonld.except '@context'
188
- end
189
- end
190
- end
191
- end