ladder 0.3.1 → 0.3.2
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/.gitignore +4 -1
- data/.semver +5 -0
- data/Gemfile +1 -1
- data/README.md +11 -11
- data/Rakefile +1 -2
- data/ladder.gemspec +26 -23
- data/lib/ladder.rb +1 -1
- data/lib/ladder/file.rb +49 -40
- data/lib/ladder/resource.rb +204 -60
- data/lib/ladder/resource/dynamic.rb +134 -83
- data/lib/ladder/resource/serializable.rb +56 -43
- data/lib/ladder/searchable.rb +14 -12
- data/lib/ladder/searchable/background.rb +40 -31
- data/lib/ladder/searchable/file.rb +33 -26
- data/lib/ladder/searchable/resource.rb +26 -15
- data/lib/ladder/version.rb +2 -2
- data/spec/ladder/file_spec.rb +9 -7
- data/spec/ladder/resource/dynamic_spec.rb +13 -143
- data/spec/ladder/resource_spec.rb +47 -226
- data/spec/ladder/searchable/background_spec.rb +37 -42
- data/spec/ladder/searchable/file_spec.rb +8 -5
- data/spec/ladder/searchable/resource_spec.rb +30 -38
- data/spec/shared/file.rb +9 -9
- data/spec/shared/graph.jsonld +31 -0
- data/spec/shared/resource.rb +397 -14
- data/spec/shared/searchable/file.rb +2 -4
- data/spec/shared/searchable/resource.rb +137 -145
- data/spec/spec_helper.rb +3 -2
- metadata +49 -4
@@ -1,104 +1,155 @@
|
|
1
|
-
module Ladder
|
2
|
-
|
1
|
+
module Ladder
|
2
|
+
module Resource
|
3
|
+
module Dynamic
|
4
|
+
extend ActiveSupport::Concern
|
3
5
|
|
4
|
-
|
5
|
-
include Ladder::Resource
|
6
|
-
include InstanceMethods
|
6
|
+
include Ladder::Resource
|
7
7
|
|
8
|
-
|
8
|
+
included do
|
9
|
+
include InstanceMethods
|
9
10
|
|
10
|
-
|
11
|
-
|
11
|
+
field :_context, type: Hash
|
12
|
+
field :_types, type: Array
|
12
13
|
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
# Store context information
|
17
|
-
self._context ||= Hash.new(nil)
|
14
|
+
after_find :apply_context
|
15
|
+
after_find :apply_types
|
16
|
+
end
|
18
17
|
|
19
|
-
|
20
|
-
|
21
|
-
|
18
|
+
##
|
19
|
+
# Dynamically define a field on the object instance; in addition to
|
20
|
+
# (or overloading) class-level properties
|
21
|
+
#
|
22
|
+
# @see Ladder::Resource#property
|
23
|
+
#
|
24
|
+
# @param [String] field_name ActiveModel attribute name for the field
|
25
|
+
# @param [Hash] opts options to pass to Mongoid / ActiveTriples
|
26
|
+
# @option opts [RDF::Term] :predicate RDF predicate for this property
|
27
|
+
# @return [Hash] an updated context for the object
|
28
|
+
def property(field_name, opts = {})
|
29
|
+
# Store context information
|
30
|
+
self._context ||= Hash.new(nil)
|
31
|
+
|
32
|
+
# Ensure new field name is unique
|
33
|
+
field_name = opts[:predicate].qname.join('_').to_sym if resource_class.properties.symbolize_keys.keys.include? field_name
|
34
|
+
|
35
|
+
self._context[field_name] = opts[:predicate].to_s
|
36
|
+
apply_context
|
37
|
+
end
|
22
38
|
|
23
|
-
|
24
|
-
end
|
39
|
+
private
|
25
40
|
|
26
|
-
|
41
|
+
##
|
42
|
+
# Dynamically define field accessors
|
43
|
+
#
|
44
|
+
# @see http://mongoid.org/en/mongoid/v3/documents.html#dynamic_fields Mongoid Dynamic Fields
|
45
|
+
#
|
46
|
+
# @param [String] field_name ActiveModel attribute name for the field
|
47
|
+
# @return [void]
|
48
|
+
def create_accessors(field_name)
|
49
|
+
define_singleton_method(field_name) { read_attribute(field_name) }
|
50
|
+
define_singleton_method("#{field_name}=") { |value| write_attribute(field_name, value) }
|
51
|
+
end
|
27
52
|
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
##
|
36
|
-
# Apply dynamic fields and properties to this instance
|
37
|
-
def apply_context
|
38
|
-
return unless self._context
|
53
|
+
##
|
54
|
+
# Apply dynamic fields and properties to this instance
|
55
|
+
#
|
56
|
+
# @return [void]
|
57
|
+
def apply_context
|
58
|
+
return unless self._context
|
39
59
|
|
40
|
-
|
41
|
-
|
60
|
+
self._context.each do |field_name, uri|
|
61
|
+
next if fields.keys.include? field_name
|
42
62
|
|
43
|
-
|
44
|
-
|
63
|
+
if RDF::Vocabulary.find_term(uri)
|
64
|
+
create_accessors field_name
|
45
65
|
|
46
|
-
|
47
|
-
|
66
|
+
# Update resource properties
|
67
|
+
resource_class.property(field_name.to_sym, predicate: RDF::Vocabulary.find_term(uri))
|
68
|
+
end
|
48
69
|
end
|
49
70
|
end
|
50
|
-
end
|
51
71
|
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
self._context.each do |field_name, uri|
|
64
|
-
value = self.send(field_name)
|
65
|
-
cast_uri = RDF::URI.new(value)
|
66
|
-
resource.set_value(RDF::Vocabulary.find_term(uri), cast_uri.valid? ? cast_uri : value)
|
72
|
+
##
|
73
|
+
# Apply dynamic types to this instance
|
74
|
+
#
|
75
|
+
# @return [void]
|
76
|
+
def apply_types
|
77
|
+
return unless _types
|
78
|
+
|
79
|
+
_types.each do |rdf_type|
|
80
|
+
unless resource.type.include? RDF::Vocabulary.find_term(rdf_type)
|
81
|
+
resource << RDF::Statement.new(rdf_subject, RDF.type, RDF::Vocabulary.find_term(rdf_type))
|
82
|
+
end
|
67
83
|
end
|
68
84
|
end
|
69
85
|
|
70
|
-
|
71
|
-
|
86
|
+
module InstanceMethods
|
87
|
+
##
|
88
|
+
# Update the delegated ActiveTriples::Resource from
|
89
|
+
# ActiveModel properties & relations
|
90
|
+
#
|
91
|
+
# @see Ladder::Resource#update_resource
|
92
|
+
#
|
93
|
+
# @param [Hash] opts options to pass to Mongoid / ActiveTriples
|
94
|
+
# @return [ActiveTriples::Resource] resource for the object
|
95
|
+
def update_resource(opts = {})
|
96
|
+
# NB: super has to go first or AT clobbers properties
|
97
|
+
super(opts)
|
98
|
+
|
99
|
+
if self._context
|
100
|
+
self._context.each do |field_name, uri|
|
101
|
+
value = send(field_name)
|
102
|
+
cast_uri = RDF::URI.new(value)
|
103
|
+
resource.set_value(RDF::Vocabulary.find_term(uri), cast_uri.valid? ? cast_uri : value)
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
resource
|
108
|
+
end
|
72
109
|
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
110
|
+
##
|
111
|
+
# Push an RDF::Statement into the object
|
112
|
+
#
|
113
|
+
# @see Ladder::Resource#<<
|
114
|
+
#
|
115
|
+
# @param [RDF::Statement, Hash, Array] statement @see RDF::Statement#from
|
116
|
+
# @return [void]
|
117
|
+
def <<(statement)
|
118
|
+
# ActiveTriples::Resource expects: RDF::Statement, Hash, or Array
|
119
|
+
statement = RDF::Statement.from(statement) unless statement.is_a? RDF::Statement
|
120
|
+
|
121
|
+
# Don't store statically-defined types
|
122
|
+
return if resource_class.type == statement.object
|
123
|
+
|
124
|
+
if RDF.type == statement.predicate
|
125
|
+
# Store type information
|
126
|
+
self._types ||= []
|
127
|
+
self._types << statement.object.to_s
|
128
|
+
|
129
|
+
apply_types
|
130
|
+
return
|
131
|
+
end
|
132
|
+
|
133
|
+
# If we have an undefined predicate, then dynamically define it
|
134
|
+
return unless statement.predicate.qname
|
135
|
+
property statement.predicate.qname.last, predicate: statement.predicate unless field_from_predicate statement.predicate
|
136
|
+
|
137
|
+
super
|
138
|
+
end
|
92
139
|
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
@
|
140
|
+
private
|
141
|
+
|
142
|
+
##
|
143
|
+
# Return a cloned, mutatable copy of the
|
144
|
+
# ActiveTriples::Resource class for this instance
|
145
|
+
#
|
146
|
+
# @see ActiveTriples::Identifiable#resource_class
|
147
|
+
#
|
148
|
+
# @return [Class] a GeneratedResourceSchema for this class
|
149
|
+
def resource_class
|
150
|
+
@modified_resource_class ||= self.class.resource_class.clone
|
151
|
+
end
|
100
152
|
end
|
101
|
-
|
153
|
+
end
|
102
154
|
end
|
103
|
-
|
104
|
-
end
|
155
|
+
end
|
@@ -1,54 +1,67 @@
|
|
1
1
|
require 'json/ld'
|
2
2
|
|
3
|
-
module Ladder
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
3
|
+
module Ladder
|
4
|
+
module Resource
|
5
|
+
module Serializable
|
6
|
+
##
|
7
|
+
# Return a JSON-LD representation for the resource
|
8
|
+
#
|
9
|
+
# @see ActiveTriples::Resource#dump
|
10
|
+
#
|
11
|
+
# @param [Hash] opts options to pass to ActiveTriples
|
12
|
+
# @option opts [Boolean] :related whether to include related resources
|
13
|
+
# @return [Hash] a serialized JSON-LD version of the resource
|
14
|
+
def as_jsonld(opts = {})
|
15
|
+
JSON.parse update_resource(opts.slice :related).dump(:jsonld, { standard_prefixes: true }.merge(opts))
|
16
|
+
end
|
17
17
|
|
18
|
-
|
19
|
-
|
20
|
-
|
18
|
+
##
|
19
|
+
# Return a framed, compacted JSON-LD representation
|
20
|
+
# by embedding related objects from the graph
|
21
|
+
#
|
22
|
+
# NB: Will NOT embed related objects with same @type.
|
23
|
+
# Spec under discussion, see https://github.com/json-ld/json-ld.org/issues/110
|
24
|
+
#
|
25
|
+
# @return [Hash] a serialized JSON-LD version of the resource
|
26
|
+
def as_framed_jsonld
|
27
|
+
json_hash = as_jsonld related: true
|
21
28
|
|
22
|
-
|
29
|
+
context = json_hash['@context']
|
30
|
+
frame = { '@context' => context }
|
31
|
+
frame['@type'] = type.first.pname unless type.empty?
|
23
32
|
|
24
|
-
|
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)
|
33
|
+
JSON::LD::API.compact(JSON::LD::API.frame(json_hash, frame), context)
|
32
34
|
end
|
33
|
-
end
|
34
35
|
|
35
|
-
|
36
|
-
|
36
|
+
##
|
37
|
+
# Return a qname-based JSON representation
|
38
|
+
#
|
39
|
+
# @param [Hash] opts options for serializaiton
|
40
|
+
# @option opts [Boolean] :related whether to include related resources
|
41
|
+
# @return [Hash] a serialized 'qname' version of the resource
|
42
|
+
def as_qname(opts = {})
|
43
|
+
qname_hash = type.empty? ? {} : { rdf: { type: type.first.pname } }
|
44
|
+
|
45
|
+
resource_class.properties.each do |field_name, property|
|
46
|
+
ns, name = property.predicate.qname
|
47
|
+
qname_hash[ns] ||= {}
|
37
48
|
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
49
|
+
if relations.keys.include? field_name
|
50
|
+
if opts[:related]
|
51
|
+
qname_hash[ns][name] = send(field_name).to_a.map(&:as_qname)
|
52
|
+
else
|
53
|
+
qname_hash[ns][name] = send(field_name).to_a.map { |obj| "#{obj.class.name.underscore.pluralize}:#{obj.id}" }
|
54
|
+
end
|
55
|
+
elsif fields.keys.include? field_name
|
56
|
+
qname_hash[ns][name] = read_attribute(field_name)
|
57
|
+
end
|
46
58
|
|
47
|
-
|
48
|
-
|
49
|
-
|
59
|
+
# Remove empty/null values
|
60
|
+
qname_hash[ns].delete_if { |_k, v| v.blank? }
|
61
|
+
end
|
50
62
|
|
51
|
-
|
63
|
+
qname_hash
|
64
|
+
end
|
65
|
+
end
|
52
66
|
end
|
53
|
-
|
54
|
-
end
|
67
|
+
end
|
data/lib/ladder/searchable.rb
CHANGED
@@ -2,18 +2,20 @@ require 'active_support/concern'
|
|
2
2
|
require 'elasticsearch/model'
|
3
3
|
require 'elasticsearch/model/callbacks'
|
4
4
|
|
5
|
-
module Ladder
|
6
|
-
|
5
|
+
module Ladder
|
6
|
+
module Searchable
|
7
|
+
extend ActiveSupport::Concern
|
7
8
|
|
8
|
-
|
9
|
-
|
10
|
-
|
9
|
+
autoload :Background, 'ladder/searchable/background'
|
10
|
+
autoload :File, 'ladder/searchable/file'
|
11
|
+
autoload :Resource, 'ladder/searchable/resource'
|
11
12
|
|
12
|
-
|
13
|
-
|
14
|
-
|
13
|
+
included do
|
14
|
+
include Elasticsearch::Model
|
15
|
+
include Elasticsearch::Model::Callbacks unless ancestors.include? Ladder::Searchable::Background
|
15
16
|
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
end
|
17
|
+
include Ladder::Searchable::Resource if ancestors.include? Ladder::Resource
|
18
|
+
include Ladder::Searchable::File if ancestors.include? Ladder::File
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -1,43 +1,52 @@
|
|
1
1
|
require 'active_job'
|
2
2
|
|
3
|
-
module Ladder
|
4
|
-
|
3
|
+
module Ladder
|
4
|
+
module Searchable
|
5
|
+
module Background
|
6
|
+
extend ActiveSupport::Concern
|
5
7
|
|
6
|
-
|
7
|
-
|
8
|
-
|
8
|
+
included do
|
9
|
+
include Ladder::Searchable
|
10
|
+
include GlobalID::Identification
|
9
11
|
|
10
|
-
|
12
|
+
GlobalID.app = 'Ladder'
|
11
13
|
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
14
|
+
after_create { enqueue :index }
|
15
|
+
after_update { enqueue :update }
|
16
|
+
before_destroy { enqueue :delete }
|
17
|
+
end
|
16
18
|
|
17
|
-
|
19
|
+
private
|
18
20
|
|
19
|
-
|
20
|
-
#
|
21
|
-
|
21
|
+
##
|
22
|
+
# Queue an index operation for asynchronous execution
|
23
|
+
#
|
24
|
+
# @param [Symbol] operation the kind of operation to perform: index, delete, update
|
25
|
+
# @return [void]
|
26
|
+
def enqueue(operation)
|
27
|
+
# Force autosave of related documents before queueing for indexing or updating
|
28
|
+
methods.select { |i| i[/autosave_documents/] }.each { |m| send m } unless :delete == operation
|
22
29
|
|
23
|
-
|
24
|
-
|
30
|
+
Indexer.set(queue: self.class.name.underscore.pluralize).perform_later(operation.to_s, self)
|
31
|
+
end
|
25
32
|
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
33
|
+
class Indexer < ActiveJob::Base
|
34
|
+
queue_as :elasticsearch
|
35
|
+
|
36
|
+
##
|
37
|
+
# Perform a queued index operation
|
38
|
+
#
|
39
|
+
# @param [String] operation the kind of operation to perform: index, delete, update
|
40
|
+
# @param [Ladder::Resource, Ladder::File] model the object instance to modify in the index
|
41
|
+
# @return [void]
|
42
|
+
def perform(operation, model)
|
43
|
+
case operation
|
44
|
+
when 'index' then model.__elasticsearch__.index_document
|
45
|
+
when 'update' then model.__elasticsearch__.update_document
|
46
|
+
when 'delete' then model.__elasticsearch__.delete_document
|
47
|
+
end
|
48
|
+
end
|
38
49
|
end
|
39
50
|
end
|
40
|
-
|
41
51
|
end
|
42
|
-
|
43
|
-
end
|
52
|
+
end
|