ladder 0.0.3 → 0.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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