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.
@@ -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
- cast_uri = RDF::URI.new(value)
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
- localized_hash.map { |lang, val| RDF::Literal.new(val, language: lang) } unless localized_hash.nil?
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
- public
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
@@ -1,16 +1,17 @@
1
- require 'ladder/resource'
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 :Resource, 'ladder/searchable/resource'
9
- autoload :File, 'ladder/searchable/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
- # Generate a qname-based JSON representation
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
- case opts[:as]
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
@@ -1,3 +1,3 @@
1
1
  module Ladder
2
- VERSION = "0.2.1"
2
+ VERSION = "0.3.0"
3
3
  end
@@ -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 = 'http://example.org'
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 = './spec/shared/moomin.pdf'
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