ladder 0.0.3 → 0.0.4

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,80 @@
1
+ require 'mongoid'
2
+ require 'active_triples'
3
+ require 'json/ld'
4
+
5
+ module Ladder::Resource
6
+ extend ActiveSupport::Concern
7
+
8
+ include Mongoid::Document
9
+ include ActiveTriples::Identifiable
10
+
11
+ included do
12
+ configure base_uri: RDF::URI.new(LADDER_BASE_URI) / name.underscore.pluralize if defined? LADDER_BASE_URI
13
+ end
14
+
15
+ ##
16
+ # Convenience method to return JSON-LD representation
17
+ #
18
+ def as_jsonld(opts = {})
19
+ update_resource(opts.slice :related).dump(:jsonld, {standard_prefixes: true}.merge(opts))
20
+ end
21
+
22
+ ##
23
+ # Overload ActiveTriples #update_resource
24
+ #
25
+ # @see ActiveTriples::Identifiable
26
+ def update_resource(opts = {})
27
+ relation_hash = opts[:related] ? relations : embedded_relations
28
+
29
+ super() do |name, prop|
30
+ object = self.send(prop.term)
31
+ next if object.nil?
32
+
33
+ objects = object.is_a?(Enumerable) ? object : [object]
34
+
35
+ values = objects.map do |obj|
36
+ if obj.is_a?(ActiveTriples::Identifiable)
37
+ if relation_hash.keys.include? name
38
+ obj.update_resource
39
+ obj.resource.set_value(relation_hash[name].inverse, self.rdf_subject) if relation_hash[name].inverse
40
+ obj
41
+ else
42
+ resource.delete [obj.rdf_subject] if resource.enum_subjects.include? obj.rdf_subject and ! opts[:related]
43
+ obj.rdf_subject
44
+ end
45
+ else
46
+ if fields[name].localized?
47
+ read_attribute(name).map { |lang, val| RDF::Literal.new(val, language: lang) }
48
+ else
49
+ obj
50
+ end
51
+ end
52
+ end
53
+
54
+ resource.set_value(prop.predicate, values.flatten)
55
+ end
56
+
57
+ resource
58
+ end
59
+
60
+ module ClassMethods
61
+
62
+ ##
63
+ # Overload ActiveTriples #property
64
+ #
65
+ # @see ActiveTriples::Properties
66
+ def property(name, opts={})
67
+ if class_name = opts[:class_name]
68
+ mongoid_opts = opts.except(:predicate, :multivalue).merge(autosave: true)
69
+ opts.except! *mongoid_opts.keys
70
+
71
+ has_and_belongs_to_many(name, mongoid_opts) unless relations.keys.include? name.to_s
72
+ else
73
+ field(name, localize: true)
74
+ end
75
+
76
+ super
77
+ end
78
+ end
79
+
80
+ end
@@ -0,0 +1,53 @@
1
+ require 'ladder/resource'
2
+ require 'elasticsearch/model'
3
+ require 'elasticsearch/model/callbacks'
4
+
5
+ module Ladder::Searchable
6
+ extend ActiveSupport::Concern
7
+
8
+ included do
9
+ include Elasticsearch::Model
10
+ include Elasticsearch::Model::Callbacks
11
+ end
12
+
13
+ ##
14
+ # Generate a qname-based JSON representation
15
+ #
16
+ def as_qname
17
+ qname_hash = type.empty? ? {} : {rdf: {type: type.first.pname }}
18
+
19
+ resource_class.properties.each do |field_name, property|
20
+ ns, name = property.predicate.qname
21
+ qname_hash[ns] ||= Hash.new
22
+
23
+ object = self.send(field_name)
24
+
25
+ if relations.keys.include? field_name
26
+ qname_hash[ns][name] = object.to_a.map { |obj| "#{obj.class.name.underscore.pluralize}:#{obj.id}" }
27
+ elsif fields.keys.include? field_name
28
+ qname_hash[ns][name] = read_attribute(field_name)
29
+ end
30
+ end
31
+
32
+ qname_hash
33
+ end
34
+
35
+ module ClassMethods
36
+
37
+ ##
38
+ # Specify type of serialization to use for indexing
39
+ #
40
+ def index(opts={})
41
+ case opts[:as]
42
+ when :jsonld
43
+ define_method(:as_indexed_json) { |opts = {}| as_jsonld }
44
+ when :qname
45
+ define_method(:as_indexed_json) { |opts = {}| as_qname }
46
+ else
47
+ define_method(:as_indexed_json) { |opts = {}| as_json(except: ['id', '_id']) }
48
+ end
49
+ end
50
+
51
+ end
52
+
53
+ end
@@ -0,0 +1,3 @@
1
+ module Ladder
2
+ VERSION = "0.0.4"
3
+ end
data/lib/ladder.rb ADDED
@@ -0,0 +1,6 @@
1
+ require "ladder/version"
2
+
3
+ module Ladder
4
+ autoload :Resource, 'ladder/resource'
5
+ autoload :Searchable, 'ladder/searchable'
6
+ end
data/mongoid.yml ADDED
@@ -0,0 +1,6 @@
1
+ development:
2
+ sessions:
3
+ default:
4
+ database: ladder
5
+ hosts:
6
+ - localhost:27017
@@ -0,0 +1,305 @@
1
+ require 'spec_helper'
2
+ require 'pry'
3
+
4
+ describe Ladder::Resource do
5
+ before do
6
+ Mongoid.load!('mongoid.yml', :development)
7
+ Mongoid.logger.level = Moped.logger.level = Logger::DEBUG
8
+ Mongoid.purge!
9
+
10
+ LADDER_BASE_URI = 'http://example.org'
11
+
12
+ class Thing
13
+ include Ladder::Resource
14
+ end
15
+
16
+ class Person
17
+ include Ladder::Resource
18
+ end
19
+ end
20
+
21
+ after do
22
+ Object.send(:remove_const, :LADDER_BASE_URI) if Object
23
+ Object.send(:remove_const, "Thing") if Object
24
+ Object.send(:remove_const, "Person") if Object
25
+ end
26
+
27
+ subject { Thing.new }
28
+ let(:person) { Person.new }
29
+
30
+ shared_context 'with data' do
31
+ let(:concept) { Concept.new }
32
+ let(:part) { Part.new }
33
+
34
+ before do
35
+ class Concept
36
+ include Ladder::Resource
37
+ end
38
+
39
+ class Part
40
+ include Ladder::Resource
41
+ embedded_in :thing
42
+ property :thing, :predicate => RDF::DC.relation, :class_name => 'Thing'
43
+ end
44
+
45
+ # localized literal
46
+ subject.class.property :title, :predicate => RDF::DC.title
47
+ subject.title = 'Comet in Moominland'
48
+
49
+ # many-to-many
50
+ person.class.property :things, :predicate => RDF::DC.relation, :class_name => 'Thing'
51
+ subject.class.property :people, :predicate => RDF::DC.creator, :class_name => 'Person'
52
+ subject.people << person
53
+
54
+ # one-sided has-many
55
+ subject.class.has_and_belongs_to_many :concepts, inverse_of: nil
56
+ subject.class.property :concepts, :predicate => RDF::DC.subject, :class_name => 'Concept'
57
+ subject.concepts << concept
58
+
59
+ # embedded many
60
+ subject.class.embeds_many :parts, cascade_callbacks: true
61
+ subject.class.property :parts, :predicate => RDF::DC.hasPart, :class_name => 'Part'
62
+ subject.parts << part
63
+ subject.save
64
+ end
65
+
66
+ after do
67
+ Object.send(:remove_const, 'Concept')
68
+ Object.send(:remove_const, 'Part')
69
+ end
70
+
71
+ it 'should have relations' do
72
+ expect(subject.title).to eq 'Comet in Moominland'
73
+ expect(subject.people.to_a).to include person
74
+ expect(subject.concepts.to_a).to include concept
75
+ expect(subject.parts.to_a).to include part
76
+ end
77
+
78
+ it 'should have reverse relations' do
79
+ expect(person.things.to_a).to include subject
80
+ expect(concept.relations).to be_empty
81
+ expect(part.thing).to eq subject
82
+ end
83
+ end
84
+
85
+ describe 'LADDER_BASE_URI' do
86
+ it 'should automatically have a base URI' do
87
+ expect(subject.rdf_subject.parent).to eq RDF::URI('http://example.org/things/')
88
+ end
89
+ end
90
+
91
+ describe '#property' do
92
+ context 'with localized literal' do
93
+ before do
94
+ subject.class.property :title, :predicate => RDF::DC.title
95
+ subject.title = 'Comet in Moominland'
96
+ end
97
+
98
+ it 'should return localized value' do
99
+ expect(subject.title).to eq 'Comet in Moominland'
100
+ end
101
+
102
+ it 'should return all locales' do
103
+ expect(subject.attributes['title']).to eq Hash({'en' => 'Comet in Moominland'})
104
+ end
105
+
106
+ it 'should have a valid predicate' do
107
+ expect(subject.class.properties).to include 'title'
108
+ expect(t = subject.class.properties['title']).to be_a ActiveTriples::NodeConfig
109
+ expect(t.predicate).to eq RDF::DC.title
110
+ end
111
+ end
112
+
113
+ context 'with many-to-many' do
114
+ before do
115
+ subject.class.property :people, :predicate => RDF::DC.creator, :class_name => 'Person'
116
+ person.class.property :things, :predicate => RDF::DC.relation, :class_name => 'Thing'
117
+ subject.people << person
118
+ subject.save
119
+ end
120
+
121
+ it 'should have a relation' do
122
+ expect(subject.relations).to include 'people'
123
+ expect(subject.relations['people'].relation).to eq (Mongoid::Relations::Referenced::ManyToMany)
124
+ expect(subject.people.to_a).to include person
125
+ end
126
+
127
+ it 'should have an inverse relation' do
128
+ expect(person.relations).to include 'things'
129
+ expect(person.relations['things'].relation).to eq (Mongoid::Relations::Referenced::ManyToMany)
130
+ expect(person.things.to_a).to include subject
131
+ end
132
+
133
+ it 'should have a valid predicate' do
134
+ expect(subject.class.properties).to include 'people'
135
+ expect(t = subject.class.properties['people']).to be_a ActiveTriples::NodeConfig
136
+ expect(t.predicate).to eq RDF::DC.creator
137
+ end
138
+
139
+ it 'should have a valid inverse predicate' do
140
+ expect(person.class.properties).to include 'things'
141
+ expect(t = person.class.properties['things']).to be_a ActiveTriples::NodeConfig
142
+ expect(t.predicate).to eq RDF::DC.relation
143
+ end
144
+ end
145
+
146
+ context 'with one-sided has-many' do
147
+ before do
148
+ subject.class.has_and_belongs_to_many :people, inverse_of: nil
149
+ subject.class.property :people, :predicate => RDF::DC.creator, :class_name => 'Person'
150
+ subject.people << person
151
+ end
152
+
153
+ it 'should have a relation' do
154
+ expect(subject.relations).to include 'people'
155
+ expect(subject.relations['people'].relation).to eq (Mongoid::Relations::Referenced::ManyToMany)
156
+ expect(subject.people.to_a).to include person
157
+ end
158
+
159
+ it 'should not have an inverse relation' do
160
+ expect(subject.relations['people'].inverse_of).to be nil
161
+ expect(person.relations).to be_empty
162
+ end
163
+
164
+ it 'should have a valid predicate' do
165
+ expect(subject.class.properties).to include 'people'
166
+ expect(t = subject.class.properties['people']).to be_a ActiveTriples::NodeConfig
167
+ expect(t.predicate).to eq RDF::DC.creator
168
+ end
169
+
170
+ it 'should not have an inverse predicate' do
171
+ expect(person.class.properties).to be_empty
172
+ end
173
+ end
174
+
175
+ context 'with embeds-many' do
176
+ before do
177
+ subject.class.embeds_many :people
178
+ subject.class.property :people, :predicate => RDF::DC.creator, :class_name => 'Person'
179
+
180
+ person.class.embedded_in :thing
181
+ person.class.property :thing, :predicate => RDF::DC.relation, :class_name => 'Thing'
182
+
183
+ subject.people << person
184
+ end
185
+
186
+ it 'should have a relation' do
187
+ expect(subject.relations).to include 'people'
188
+ expect(subject.relations['people'].relation).to eq (Mongoid::Relations::Embedded::Many)
189
+ expect(subject.people.to_a).to include person
190
+ end
191
+
192
+ it 'should have an inverse relation' do
193
+ expect(person.relations).to include 'thing'
194
+ expect(person.relations['thing'].relation).to eq (Mongoid::Relations::Embedded::In)
195
+ expect(person.thing).to eq subject
196
+ end
197
+
198
+ it 'should have a valid predicate' do
199
+ expect(subject.class.properties).to include 'people'
200
+ expect(t = subject.class.properties['people']).to be_a ActiveTriples::NodeConfig
201
+ expect(t.predicate).to eq RDF::DC.creator
202
+ end
203
+
204
+ it 'should have a valid inverse predicate' do
205
+ expect(person.class.properties).to include 'thing'
206
+ expect(t = person.class.properties['thing']).to be_a ActiveTriples::NodeConfig
207
+ expect(t.predicate).to eq RDF::DC.relation
208
+ end
209
+ end
210
+ end
211
+
212
+ describe '#update_resource' do
213
+
214
+ context 'without related: true' do
215
+ include_context 'with data'
216
+
217
+ before do
218
+ subject.update_resource
219
+ end
220
+
221
+ it 'should have a literal object' do
222
+ subject.resource.query(:subject => subject.rdf_subject, :predicate => RDF::DC.title).each_statement do |s|
223
+ expect(s.object.to_s).to eq 'Comet in Moominland'
224
+ end
225
+ end
226
+
227
+ it 'should have an embedded object' do
228
+ subject.resource.query(:subject => part.rdf_subject, :predicate => RDF::DC.relation).each_statement do |s|
229
+ expect(s.object).to eq subject.rdf_subject
230
+ end
231
+ end
232
+
233
+ it 'should have an embedded object relation' do
234
+ subject.resource.query(:subject => subject.rdf_subject, :predicate => RDF::DC.hasPart).each_statement do |s|
235
+ expect(s.object).to eq part.rdf_subject
236
+ end
237
+ end
238
+
239
+ it 'should not have related objects' do
240
+ expect(subject.resource.query(:subject => person.rdf_subject)).to be_empty
241
+ expect(subject.resource.query(:subject => concept.rdf_subject)).to be_empty
242
+ end
243
+
244
+ it 'should not have related object relations' do
245
+ expect(person.resource.statements).to be_empty
246
+ expect(concept.resource.statements).to be_empty
247
+ end
248
+ end
249
+
250
+ context 'with related: true' do
251
+ include_context 'with data'
252
+
253
+ before do
254
+ subject.update_resource(:related => true)
255
+ end
256
+
257
+ it 'should have a literal object' do
258
+ subject.resource.query(:subject => subject.rdf_subject, :predicate => RDF::DC.title).each_statement do |s|
259
+ expect(s.object.to_s).to eq 'Comet in Moominland'
260
+ end
261
+ end
262
+
263
+ it 'should have an embedded object' do
264
+ subject.resource.query(:subject => part.rdf_subject, :predicate => RDF::DC.relation).each_statement do |s|
265
+ expect(s.object).to eq subject.rdf_subject
266
+ end
267
+ end
268
+
269
+ it 'should have an embedded object relation' do
270
+ subject.resource.query(:subject => subject.rdf_subject, :predicate => RDF::DC.hasPart).each_statement do |s|
271
+ expect(s.object).to eq part.rdf_subject
272
+ end
273
+ end
274
+
275
+ it 'should have related objects' do
276
+ subject.resource.query(:subject => subject.rdf_subject, :predicate => RDF::DC.subject).each_statement do |s|
277
+ expect(s.object).to eq concept.rdf_subject
278
+ end
279
+ subject.resource.query(:subject => subject.rdf_subject, :predicate => RDF::DC.creator).each_statement do |s|
280
+ expect(s.object).to eq person.rdf_subject
281
+ end
282
+ end
283
+
284
+ it 'should have related object relations' do
285
+ person.resource.query(:subject => person.rdf_subject, :predicate => RDF::DC.relation).each_statement do |s|
286
+ expect(s.object).to eq subject.rdf_subject
287
+ end
288
+ end
289
+ end
290
+ end
291
+
292
+ describe '#as_jsonld' do
293
+ include_context 'with data'
294
+
295
+ before do
296
+ subject.update_resource
297
+ end
298
+
299
+ it 'should output a valid jsonld representation of itself' do
300
+ g = RDF::Graph.new << JSON::LD::API.toRdf(JSON.parse subject.as_jsonld)
301
+ expect(subject.resource.to_hash == g.to_hash).to be true
302
+ end
303
+ end
304
+
305
+ end
@@ -0,0 +1,179 @@
1
+ require 'spec_helper'
2
+ require 'pry'
3
+
4
+ describe Ladder::Searchable do
5
+ before do
6
+ Mongoid.load!('mongoid.yml', :development)
7
+ Mongoid.logger.level = Moped.logger.level = Logger::DEBUG
8
+ Mongoid.purge!
9
+
10
+ Elasticsearch::Model.client = Elasticsearch::Client.new host: 'localhost:9200', log: true
11
+ Elasticsearch::Model.client.indices.delete index: '_all'
12
+
13
+ LADDER_BASE_URI = 'http://example.org'
14
+
15
+ class Thing
16
+ include Ladder::Resource
17
+ include Ladder::Searchable
18
+ end
19
+
20
+ class Person
21
+ include Ladder::Resource
22
+ include Ladder::Searchable
23
+ end
24
+ end
25
+
26
+ after do
27
+ Object.send(:remove_const, "Thing") if Object
28
+ Object.send(:remove_const, "Person") if Object
29
+ end
30
+
31
+ subject { Thing.new }
32
+ let(:person) { Person.new }
33
+
34
+ shared_context 'with data' do
35
+ before do
36
+ subject.class.configure type: RDF::DC.BibliographicResource
37
+ subject.class.property :title, :predicate => RDF::DC.title
38
+ subject.title = 'Comet in Moominland'
39
+ end
40
+ end
41
+
42
+ describe '#index' do
43
+ include_context 'with data'
44
+
45
+ context 'with default' do
46
+ before do
47
+ subject.class.index
48
+ subject.save
49
+ Elasticsearch::Model.client.indices.flush
50
+ end
51
+
52
+ it 'should exist in the index' do
53
+ results = subject.class.search('title:moomin*')
54
+ expect(results.count).to eq 1
55
+ expect(results.first._source.to_hash).to eq JSON.parse(subject.as_indexed_json.to_json)
56
+ end
57
+ end
58
+
59
+ context 'with as qname' do
60
+ before do
61
+ subject.class.index as: :qname
62
+ subject.save
63
+ Elasticsearch::Model.client.indices.flush
64
+ end
65
+
66
+ it 'should exist in the index' do
67
+ results = subject.class.search('dc.title.en:moomin*')
68
+ expect(results.count).to eq 1
69
+ expect(results.first._source.to_hash).to eq JSON.parse(subject.as_qname.to_json)
70
+ end
71
+ end
72
+
73
+ context 'with as jsonld' do
74
+ before do
75
+ subject.class.index as: :jsonld
76
+ subject.save
77
+ Elasticsearch::Model.client.indices.flush
78
+ end
79
+
80
+ it 'should exist in the index' do
81
+ results = subject.class.search('dc\:title.@value:moomin*')
82
+ expect(results.count).to eq 1
83
+ expect(results.first._source.to_hash).to eq JSON.parse(subject.as_jsonld)
84
+ end
85
+ end
86
+ end
87
+
88
+ describe '#index with related' do
89
+ include_context 'with data'
90
+
91
+ before do
92
+ # related object
93
+ person.class.configure type: RDF::FOAF.Person
94
+ person.class.property :name, :predicate => RDF::FOAF.name
95
+ person.class.property :things, :predicate => RDF::DC.relation, :class_name => 'Thing'
96
+ person.name = 'Tove Jansson'
97
+
98
+ # many-to-many relation
99
+ subject.class.property :people, :predicate => RDF::DC.creator, :class_name => 'Person'
100
+ subject.people << person
101
+ end
102
+
103
+ context 'with default' do
104
+ before do
105
+ person.class.index
106
+ subject.class.index
107
+ subject.save
108
+ Elasticsearch::Model.client.indices.flush
109
+ end
110
+
111
+ it 'should contain an ID for the related object' do
112
+ results = subject.class.search('person_ids.$oid:' + person.id)
113
+ expect(results.count).to eq 1
114
+ end
115
+
116
+ it 'should include the related object in the index' do
117
+ results = person.class.search('name:tove')
118
+ expect(results.count).to eq 1
119
+ expect(results.first._source.to_hash).to eq JSON.parse(person.as_indexed_json.to_json)
120
+ end
121
+
122
+ it 'should contain an ID for the subject' do
123
+ results = person.class.search('thing_ids.$oid:' + subject.id)
124
+ expect(results.count).to eq 1
125
+ end
126
+ end
127
+
128
+ context 'with as qname' do
129
+ before do
130
+ person.class.index as: :qname
131
+ subject.class.index as: :qname
132
+ subject.save
133
+ Elasticsearch::Model.client.indices.flush
134
+ end
135
+
136
+ it 'should contain an ID for the related object' do
137
+ results = subject.class.search('dc.creator:' + person.id)
138
+ expect(results.count).to eq 1
139
+ end
140
+
141
+ it 'should include the related object in the index' do
142
+ results = person.class.search('foaf.name.en:tove')
143
+ expect(results.count).to eq 1
144
+ expect(results.first._source.to_hash).to eq JSON.parse(person.as_qname.to_json)
145
+ end
146
+
147
+ it 'should contain an ID for the subject' do
148
+ results = person.class.search('dc.relation:' + subject.id)
149
+ expect(results.count).to eq 1
150
+ end
151
+ end
152
+
153
+ context 'with as_jsonld' do
154
+ before do
155
+ person.class.index as: :jsonld
156
+ subject.class.index as: :jsonld
157
+ subject.save
158
+ Elasticsearch::Model.client.indices.flush
159
+ end
160
+
161
+ it 'should contain an ID for the related object' do
162
+ results = subject.class.search('dc\:creator.@id:' + person.id)
163
+ expect(results.count).to eq 1
164
+ end
165
+
166
+ it 'should include the related object in the index' do
167
+ results = person.class.search('foaf\:name.@value:tove')
168
+ expect(results.count).to eq 1
169
+ expect(results.first._source.to_hash).to eq JSON.parse(person.as_jsonld)
170
+ end
171
+
172
+ it 'should contain an ID for the subject' do
173
+ results = person.class.search('dc\:relation.@id:' + subject.id)
174
+ expect(results.count).to eq 1
175
+ end
176
+ end
177
+ end
178
+
179
+ end
@@ -0,0 +1,15 @@
1
+ require 'bundler/setup'
2
+ Bundler.setup
3
+
4
+ require 'ladder'
5
+
6
+ RSpec.configure do |config|
7
+ config.color = true
8
+ config.tty = true
9
+
10
+ # Uncomment the following line to get errors and backtrace for deprecation warnings
11
+ # config.raise_errors_for_deprecations!
12
+
13
+ # Use the specified formatter
14
+ config.formatter = :documentation
15
+ end