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 +4 -4
- data/.travis.yml +5 -1
- data/README.md +82 -11
- data/ladder.gemspec +5 -3
- data/lib/ladder.rb +2 -1
- data/lib/ladder/file.rb +59 -0
- data/lib/ladder/resource/dynamic.rb +7 -15
- data/lib/ladder/version.rb +1 -1
- data/spec/ladder/file_spec.rb +42 -0
- data/spec/ladder/{dynamic_spec.rb → resource/dynamic_spec.rb} +0 -0
- data/spec/ladder/searchable_spec.rb +190 -1
- data/spec/shared/file.rb +114 -0
- data/spec/shared/moomin.pdf +0 -0
- data/spec/shared/resource.rb +1 -1
- metadata +41 -8
- data/spec/shared/searchable.rb +0 -191
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b9a1843bcc9f4d0ff1d006c83df08c744fb96c93
|
4
|
+
data.tar.gz: 2c7ed1fbff81db63f50bc3d55ae0999895b409b9
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
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](#
|
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
|
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
|
-
#
|
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.
|
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
data/lib/ladder/file.rb
ADDED
@@ -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 =
|
66
|
-
|
67
|
-
|
68
|
-
|
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
|
81
|
-
|
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
|
data/lib/ladder/version.rb
CHANGED
@@ -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
|
File without changes
|
@@ -23,7 +23,196 @@ describe Ladder::Searchable do
|
|
23
23
|
end
|
24
24
|
|
25
25
|
it_behaves_like 'a Resource'
|
26
|
-
|
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
|
data/spec/shared/file.rb
ADDED
@@ -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
|
data/spec/shared/resource.rb
CHANGED
@@ -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
|
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.
|
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:
|
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.
|
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.
|
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/
|
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/
|
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:
|
data/spec/shared/searchable.rb
DELETED
@@ -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
|