ladder 0.2.1 → 0.3.0
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.
- checksums.yaml +4 -4
- data/.travis.yml +3 -1
- data/README.md +133 -110
- data/ladder.gemspec +6 -5
- data/lib/ladder/file.rb +15 -17
- data/lib/ladder/resource.rb +32 -22
- data/lib/ladder/resource/dynamic.rb +43 -49
- data/lib/ladder/resource/serializable.rb +54 -0
- data/lib/ladder/searchable.rb +5 -4
- data/lib/ladder/searchable/background.rb +43 -0
- data/lib/ladder/searchable/resource.rb +5 -64
- data/lib/ladder/version.rb +1 -1
- data/spec/ladder/file_spec.rb +53 -6
- data/spec/ladder/resource/dynamic_spec.rb +117 -115
- data/spec/ladder/resource_spec.rb +273 -8
- data/spec/ladder/searchable/background_spec.rb +112 -0
- data/spec/ladder/{file/searchable_spec.rb → searchable/file_spec.rb} +6 -26
- data/spec/ladder/searchable/resource_spec.rb +83 -0
- data/spec/shared/file.rb +8 -52
- data/spec/shared/resource.rb +54 -272
- data/spec/shared/searchable/file.rb +50 -0
- data/spec/shared/searchable/resource.rb +223 -0
- data/spec/spec_helper.rb +1 -0
- metadata +55 -33
- data/spec/ladder/resource/searchable_spec.rb +0 -221
data/lib/ladder/resource.rb
CHANGED
@@ -1,35 +1,21 @@
|
|
1
1
|
require 'mongoid'
|
2
2
|
require 'active_triples'
|
3
|
-
require 'json/ld'
|
4
3
|
|
5
4
|
module Ladder::Resource
|
5
|
+
autoload :Dynamic, 'ladder/resource/dynamic'
|
6
|
+
autoload :Serializable, 'ladder/resource/serializable'
|
7
|
+
|
6
8
|
extend ActiveSupport::Concern
|
7
9
|
|
8
10
|
include Mongoid::Document
|
9
11
|
include ActiveTriples::Identifiable
|
10
|
-
|
11
|
-
autoload :Dynamic, 'ladder/resource/dynamic'
|
12
|
+
include Ladder::Resource::Serializable
|
12
13
|
|
13
14
|
included do
|
14
15
|
configure base_uri: RDF::URI.new(LADDER_BASE_URI) / name.underscore.pluralize if defined? LADDER_BASE_URI
|
15
16
|
end
|
16
17
|
|
17
|
-
|
18
|
-
# Return JSON-LD representation
|
19
|
-
#
|
20
|
-
# @see ActiveTriples::Resource#dump
|
21
|
-
def as_jsonld(opts = {})
|
22
|
-
JSON.parse update_resource(opts.slice :related).dump(:jsonld, {standard_prefixes: true}.merge(opts))
|
23
|
-
end
|
24
|
-
|
25
|
-
##
|
26
|
-
# Overload ActiveTriples #rdf_label
|
27
|
-
#
|
28
|
-
# @see ActiveTriples::Resource
|
29
|
-
def rdf_label
|
30
|
-
update_resource
|
31
|
-
resource.rdf_label
|
32
|
-
end
|
18
|
+
delegate :rdf_label, to: :update_resource
|
33
19
|
|
34
20
|
##
|
35
21
|
# Populate resource properties from ActiveModel
|
@@ -38,19 +24,40 @@ module Ladder::Resource
|
|
38
24
|
value = update_from_field(name) if fields[name]
|
39
25
|
value = update_from_relation(name, opts) if relations[name]
|
40
26
|
|
41
|
-
|
42
|
-
resource.set_value(property.predicate, cast_uri.valid? ? cast_uri : value) if value
|
27
|
+
resource.set_value(property.predicate, value) #if value
|
43
28
|
end
|
44
29
|
|
45
30
|
resource
|
46
31
|
end
|
47
32
|
|
33
|
+
##
|
34
|
+
# Push RDF statement into resource
|
35
|
+
def <<(data)
|
36
|
+
# ActiveTriples::Resource expects: RDF::Statement, Hash, or Array
|
37
|
+
data = RDF::Statement.from(data) unless data.is_a? RDF::Statement
|
38
|
+
|
39
|
+
# Only push statement if the statement's predicate is defined on the class
|
40
|
+
if resource_class.properties.values.map(&:predicate).include? data.predicate
|
41
|
+
field_name = resource_class.properties.select { |name, term| term.predicate == data.predicate }.keys.first.to_sym
|
42
|
+
|
43
|
+
# Set the value in Mongoid
|
44
|
+
value = data.object.is_a?(RDF::Literal) ? data.object.object : data.object.to_s
|
45
|
+
self.send("#{field_name}=", value)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
48
49
|
private
|
49
50
|
|
50
51
|
def update_from_field(name)
|
51
52
|
if fields[name].localized?
|
52
53
|
localized_hash = read_attribute(name)
|
53
|
-
|
54
|
+
|
55
|
+
unless localized_hash.nil?
|
56
|
+
localized_hash.map do |lang, value|
|
57
|
+
cast_uri = RDF::URI.new(value)
|
58
|
+
cast_uri.valid? ? cast_uri : RDF::Literal.new(value, language: lang)
|
59
|
+
end
|
60
|
+
end
|
54
61
|
else
|
55
62
|
self.send(name)
|
56
63
|
end
|
@@ -60,6 +67,9 @@ module Ladder::Resource
|
|
60
67
|
objects = self.send(name).to_a
|
61
68
|
|
62
69
|
if opts[:related] or embedded_relations[name]
|
70
|
+
# Force autosave of related documents to ensure correct serialization
|
71
|
+
methods.select{|i| i[/autosave_documents/] }.each{|m| send m}
|
72
|
+
|
63
73
|
# update inverse relation properties
|
64
74
|
relation_def = relations[name]
|
65
75
|
objects.each { |object| object.resource.set_value(relation_def.inverse, self.rdf_subject) } if relation_def.inverse
|
@@ -3,29 +3,12 @@ module Ladder::Resource::Dynamic
|
|
3
3
|
|
4
4
|
included do
|
5
5
|
include Ladder::Resource
|
6
|
+
include InstanceMethods
|
7
|
+
include ClassMethods
|
6
8
|
|
7
9
|
field :_context, type: Hash
|
8
10
|
|
9
11
|
after_find :apply_context
|
10
|
-
|
11
|
-
##
|
12
|
-
# Overload Ladder #update_resource
|
13
|
-
#
|
14
|
-
# @see Ladder::Resource
|
15
|
-
def update_resource(opts = {})
|
16
|
-
# FIXME: for some reason super has to go first or AT clobbers properties
|
17
|
-
super(opts)
|
18
|
-
|
19
|
-
if self._context
|
20
|
-
self._context.each do |field_name, uri|
|
21
|
-
value = self.send(field_name)
|
22
|
-
cast_uri = RDF::URI.new(value)
|
23
|
-
resource.set_value(RDF::Vocabulary.find_term(uri), cast_uri.valid? ? cast_uri : value)
|
24
|
-
end
|
25
|
-
end
|
26
|
-
|
27
|
-
resource
|
28
|
-
end
|
29
12
|
end
|
30
13
|
|
31
14
|
##
|
@@ -41,35 +24,6 @@ module Ladder::Resource::Dynamic
|
|
41
24
|
apply_context
|
42
25
|
end
|
43
26
|
|
44
|
-
def <<(data)
|
45
|
-
# ActiveTriples::Resource expects: RDF::Statement, Hash, or Array
|
46
|
-
data = RDF::Statement.from(data) unless data.is_a? RDF::Statement
|
47
|
-
|
48
|
-
# Define predicate on object unless it's defined on the class
|
49
|
-
if resource_class.properties.values.map(&:predicate).include? data.predicate
|
50
|
-
field_name = resource_class.properties.select { |name, term| term.predicate == data.predicate }.keys.first.to_sym
|
51
|
-
else
|
52
|
-
qname = data.predicate.qname
|
53
|
-
|
54
|
-
if respond_to? qname.last or :name == qname.last
|
55
|
-
field_name = qname.join('_').to_sym
|
56
|
-
else
|
57
|
-
field_name = qname.last
|
58
|
-
end
|
59
|
-
|
60
|
-
property field_name, predicate: data.predicate
|
61
|
-
end
|
62
|
-
|
63
|
-
# Set the value in Mongoid
|
64
|
-
value = if data.object.is_a? RDF::Literal
|
65
|
-
data.object.object
|
66
|
-
else
|
67
|
-
data.object.to_s
|
68
|
-
end
|
69
|
-
|
70
|
-
self.send("#{field_name}=", value)
|
71
|
-
end
|
72
|
-
|
73
27
|
private
|
74
28
|
|
75
29
|
##
|
@@ -96,7 +50,47 @@ module Ladder::Resource::Dynamic
|
|
96
50
|
end
|
97
51
|
end
|
98
52
|
|
99
|
-
|
53
|
+
module InstanceMethods
|
54
|
+
|
55
|
+
##
|
56
|
+
# Overload Ladder #update_resource
|
57
|
+
#
|
58
|
+
# @see Ladder::Resource
|
59
|
+
def update_resource(opts = {})
|
60
|
+
# NB: super has to go first or AT clobbers properties
|
61
|
+
super(opts)
|
62
|
+
|
63
|
+
if self._context
|
64
|
+
self._context.each do |field_name, uri|
|
65
|
+
value = self.send(field_name)
|
66
|
+
cast_uri = RDF::URI.new(value)
|
67
|
+
resource.set_value(RDF::Vocabulary.find_term(uri), cast_uri.valid? ? cast_uri : value)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
resource
|
72
|
+
end
|
73
|
+
|
74
|
+
##
|
75
|
+
# Overload Ladder #<<
|
76
|
+
#
|
77
|
+
# @see Ladder::Resource
|
78
|
+
def <<(data)
|
79
|
+
# ActiveTriples::Resource expects: RDF::Statement, Hash, or Array
|
80
|
+
data = RDF::Statement.from(data) unless data.is_a? RDF::Statement
|
81
|
+
|
82
|
+
unless resource_class.properties.values.map(&:predicate).include? data.predicate
|
83
|
+
# Generate a dynamic field name
|
84
|
+
qname = data.predicate.qname
|
85
|
+
field_name = (respond_to? qname.last or :name == qname.last) ? qname.join('_').to_sym : qname.last
|
86
|
+
|
87
|
+
# Define property on class
|
88
|
+
property field_name, predicate: data.predicate
|
89
|
+
end
|
90
|
+
|
91
|
+
super(data)
|
92
|
+
end
|
93
|
+
end
|
100
94
|
|
101
95
|
module ClassMethods
|
102
96
|
|
@@ -0,0 +1,54 @@
|
|
1
|
+
require 'json/ld'
|
2
|
+
|
3
|
+
module Ladder::Resource::Serializable
|
4
|
+
##
|
5
|
+
# Return JSON-LD representation
|
6
|
+
#
|
7
|
+
# @see ActiveTriples::Resource#dump
|
8
|
+
def as_jsonld(opts = {})
|
9
|
+
JSON.parse update_resource(opts.slice :related).dump(:jsonld, {standard_prefixes: true}.merge(opts))
|
10
|
+
end
|
11
|
+
|
12
|
+
##
|
13
|
+
# Generate a qname-based JSON representation
|
14
|
+
#
|
15
|
+
def as_qname(opts = {})
|
16
|
+
qname_hash = type.empty? ? {} : {rdf: {type: type.first.pname }}
|
17
|
+
|
18
|
+
resource_class.properties.each do |field_name, property|
|
19
|
+
ns, name = property.predicate.qname
|
20
|
+
qname_hash[ns] ||= Hash.new
|
21
|
+
|
22
|
+
object = self.send(field_name)
|
23
|
+
|
24
|
+
if relations.keys.include? field_name
|
25
|
+
if opts[:related]
|
26
|
+
qname_hash[ns][name] = object.to_a.map { |obj| obj.as_qname }
|
27
|
+
else
|
28
|
+
qname_hash[ns][name] = object.to_a.map { |obj| "#{obj.class.name.underscore.pluralize}:#{obj.id}" }
|
29
|
+
end
|
30
|
+
elsif fields.keys.include? field_name
|
31
|
+
qname_hash[ns][name] = read_attribute(field_name)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
qname_hash
|
36
|
+
end
|
37
|
+
|
38
|
+
##
|
39
|
+
# Return a framed, compacted JSON-LD representation
|
40
|
+
# by embedding related objects from the graph
|
41
|
+
#
|
42
|
+
# NB: Will NOT embed related objects with same @type.
|
43
|
+
# Spec under discussion, see https://github.com/json-ld/json-ld.org/issues/110
|
44
|
+
def as_framed_jsonld
|
45
|
+
json_hash = as_jsonld related: true
|
46
|
+
|
47
|
+
context = json_hash['@context']
|
48
|
+
frame = {'@context' => context}
|
49
|
+
frame['@type'] = type.first.pname unless type.empty?
|
50
|
+
|
51
|
+
JSON::LD::API.compact(JSON::LD::API.frame(json_hash, frame), context)
|
52
|
+
end
|
53
|
+
|
54
|
+
end
|
data/lib/ladder/searchable.rb
CHANGED
@@ -1,16 +1,17 @@
|
|
1
|
-
require '
|
1
|
+
require 'active_support/concern'
|
2
2
|
require 'elasticsearch/model'
|
3
3
|
require 'elasticsearch/model/callbacks'
|
4
4
|
|
5
5
|
module Ladder::Searchable
|
6
6
|
extend ActiveSupport::Concern
|
7
7
|
|
8
|
-
autoload :
|
9
|
-
autoload :File,
|
8
|
+
autoload :Background, 'ladder/searchable/background'
|
9
|
+
autoload :File, 'ladder/searchable/file'
|
10
|
+
autoload :Resource, 'ladder/searchable/resource'
|
10
11
|
|
11
12
|
included do
|
12
13
|
include Elasticsearch::Model
|
13
|
-
include Elasticsearch::Model::Callbacks
|
14
|
+
include Elasticsearch::Model::Callbacks unless self.ancestors.include? Ladder::Searchable::Background
|
14
15
|
|
15
16
|
include Ladder::Searchable::Resource if self.ancestors.include? Ladder::Resource
|
16
17
|
include Ladder::Searchable::File if self.ancestors.include? Ladder::File
|
@@ -0,0 +1,43 @@
|
|
1
|
+
require 'active_job'
|
2
|
+
|
3
|
+
module Ladder::Searchable::Background
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
included do
|
7
|
+
include Ladder::Searchable
|
8
|
+
include GlobalID::Identification
|
9
|
+
|
10
|
+
GlobalID.app = 'Ladder'
|
11
|
+
|
12
|
+
after_create { enqueue :index }
|
13
|
+
after_update { enqueue :update }
|
14
|
+
before_destroy { enqueue :delete }
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def enqueue(operation)
|
20
|
+
# Force autosave of related documents before queueing for indexing or updating
|
21
|
+
methods.select{|i| i[/autosave_documents/] }.each{|m| send m} unless :delete == operation
|
22
|
+
|
23
|
+
Indexer.set(queue: self.class.name.underscore.pluralize).perform_later(operation.to_s, self)
|
24
|
+
end
|
25
|
+
|
26
|
+
class Indexer < ActiveJob::Base
|
27
|
+
queue_as :elasticsearch
|
28
|
+
|
29
|
+
def perform(operation, model)
|
30
|
+
case operation
|
31
|
+
when 'index'
|
32
|
+
model.__elasticsearch__.index_document
|
33
|
+
when 'update'
|
34
|
+
model.__elasticsearch__.update_document
|
35
|
+
when 'delete'
|
36
|
+
model.__elasticsearch__.delete_document
|
37
|
+
# else raise ArgumentError, "Unknown operation '#{operation}'"
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
@@ -1,76 +1,17 @@
|
|
1
1
|
module Ladder::Searchable::Resource
|
2
2
|
extend ActiveSupport::Concern
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
#
|
7
|
-
def as_qname(opts = {})
|
8
|
-
qname_hash = type.empty? ? {} : {rdf: {type: type.first.pname }}
|
9
|
-
|
10
|
-
resource_class.properties.each do |field_name, property|
|
11
|
-
ns, name = property.predicate.qname
|
12
|
-
qname_hash[ns] ||= Hash.new
|
13
|
-
|
14
|
-
object = self.send(field_name)
|
15
|
-
|
16
|
-
if relations.keys.include? field_name
|
17
|
-
if opts[:related]
|
18
|
-
qname_hash[ns][name] = object.to_a.map { |obj| obj.as_qname }
|
19
|
-
else
|
20
|
-
qname_hash[ns][name] = object.to_a.map { |obj| "#{obj.class.name.underscore.pluralize}:#{obj.id}" }
|
21
|
-
end
|
22
|
-
elsif fields.keys.include? field_name
|
23
|
-
qname_hash[ns][name] = read_attribute(field_name)
|
24
|
-
end
|
25
|
-
end
|
26
|
-
|
27
|
-
qname_hash
|
3
|
+
|
4
|
+
def as_indexed_json(opts = {})
|
5
|
+
respond_to?(:serialized_json) ? serialized_json : as_json(except: [:id, :_id])
|
28
6
|
end
|
29
7
|
|
30
|
-
private
|
31
|
-
|
32
|
-
##
|
33
|
-
# Return a framed, compacted JSON-LD representation
|
34
|
-
# by embedding related objects from the graph
|
35
|
-
#
|
36
|
-
# NB: Will NOT embed related objects with same @type. Spec under discussion, see https://github.com/json-ld/json-ld.org/issues/110
|
37
|
-
def as_framed_jsonld
|
38
|
-
json_hash = as_jsonld related: true
|
39
|
-
context = json_hash['@context']
|
40
|
-
frame = {'@context' => context, '@type' => type.first.pname}
|
41
|
-
JSON::LD::API.compact(JSON::LD::API.frame(json_hash, frame), context)
|
42
|
-
end
|
43
|
-
|
44
|
-
##
|
45
|
-
# Force autosave of related documents using Mongoid-defined methods
|
46
|
-
# Required for explicit autosave prior to after_update index callbacks
|
47
|
-
#
|
48
|
-
def autosave
|
49
|
-
methods.select{|i| i[/autosave_documents/] }.each{|m| send m}
|
50
|
-
end
|
51
|
-
|
52
8
|
module ClassMethods
|
53
9
|
|
54
10
|
##
|
55
11
|
# Specify type of serialization to use for indexing
|
56
12
|
#
|
57
|
-
def index_for_search(opts = {})
|
58
|
-
|
59
|
-
when :jsonld
|
60
|
-
if opts[:related]
|
61
|
-
define_method(:as_indexed_json) { |opts = {}| autosave; as_framed_jsonld }
|
62
|
-
else
|
63
|
-
define_method(:as_indexed_json) { |opts = {}| as_jsonld }
|
64
|
-
end
|
65
|
-
when :qname
|
66
|
-
if opts[:related]
|
67
|
-
define_method(:as_indexed_json) { |opts = {}| as_qname related: true }
|
68
|
-
else
|
69
|
-
define_method(:as_indexed_json) { |opts = {}| as_qname }
|
70
|
-
end
|
71
|
-
else
|
72
|
-
define_method(:as_indexed_json) { |opts = {}| as_json(except: [:id, :_id]) }
|
73
|
-
end
|
13
|
+
def index_for_search(opts = {}, &block)
|
14
|
+
define_method(:serialized_json, block)
|
74
15
|
end
|
75
16
|
|
76
17
|
end
|
data/lib/ladder/version.rb
CHANGED
data/spec/ladder/file_spec.rb
CHANGED
@@ -6,19 +6,69 @@ describe Ladder::File do
|
|
6
6
|
Mongoid.logger.level = Moped.logger.level = Logger::DEBUG
|
7
7
|
Mongoid.purge!
|
8
8
|
|
9
|
-
LADDER_BASE_URI
|
9
|
+
LADDER_BASE_URI ||= 'http://example.org'
|
10
10
|
|
11
11
|
class Datastream
|
12
12
|
include Ladder::File
|
13
13
|
end
|
14
14
|
end
|
15
15
|
|
16
|
+
after do
|
17
|
+
Object.send(:remove_const, :LADDER_BASE_URI) if Object
|
18
|
+
Object.send(:remove_const, "Datastream") if Object
|
19
|
+
end
|
20
|
+
|
21
|
+
shared_context 'with relations' do
|
22
|
+
let(:thing) { Thing.new }
|
23
|
+
|
24
|
+
before do
|
25
|
+
class Thing
|
26
|
+
include Ladder::Resource
|
27
|
+
end
|
28
|
+
|
29
|
+
# implicit from #property
|
30
|
+
thing.class.property :files, predicate: RDF::DC.relation, class_name: subject.class.name, inverse_of: nil
|
31
|
+
thing.files << subject
|
32
|
+
thing.save
|
33
|
+
|
34
|
+
# TODO: build some relations of various types
|
35
|
+
# explicit using HABTM
|
36
|
+
# explicit has-one
|
37
|
+
end
|
38
|
+
|
39
|
+
after do
|
40
|
+
Object.send(:remove_const, 'Thing') if Object
|
41
|
+
end
|
42
|
+
|
43
|
+
context 'with one-sided has-many' do
|
44
|
+
it 'should have a relation' do
|
45
|
+
expect(thing.relations['files'].relation).to eq (Mongoid::Relations::Referenced::ManyToMany)
|
46
|
+
expect(thing.files.to_a).to include subject
|
47
|
+
end
|
48
|
+
|
49
|
+
it 'should not have an inverse relation' do
|
50
|
+
expect(thing.relations['files'].inverse_of).to be nil
|
51
|
+
expect(subject.relations).to be_empty
|
52
|
+
end
|
53
|
+
|
54
|
+
it 'should have a valid predicate' do
|
55
|
+
expect(thing.class.properties['files'].predicate).to eq RDF::DC.relation
|
56
|
+
end
|
57
|
+
|
58
|
+
it 'should not have an inverse predicate' do
|
59
|
+
expect(subject.class.properties).to be_empty
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
end
|
64
|
+
|
16
65
|
context 'with data from file' do
|
17
|
-
TEST_FILE
|
66
|
+
TEST_FILE ||= './spec/shared/moomin.pdf'
|
18
67
|
|
19
68
|
let(:subject) { Datastream.new file: open(TEST_FILE) }
|
20
69
|
let(:source) { open(TEST_FILE).read } # ASCII-8BIT (binary)
|
21
70
|
|
71
|
+
include_context 'with relations'
|
22
72
|
it_behaves_like 'a File'
|
23
73
|
end
|
24
74
|
|
@@ -32,11 +82,8 @@ describe Ladder::File do
|
|
32
82
|
subject.file = StringIO.new(source)
|
33
83
|
end
|
34
84
|
|
85
|
+
include_context 'with relations'
|
35
86
|
it_behaves_like 'a File'
|
36
87
|
end
|
37
88
|
|
38
|
-
after do
|
39
|
-
Object.send(:remove_const, :LADDER_BASE_URI) if Object
|
40
|
-
Object.send(:remove_const, "Datastream") if Object
|
41
|
-
end
|
42
89
|
end
|