ladder 0.3.2 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 94262dd2e8eb69badfa00eeb2671a19f8637614c
4
- data.tar.gz: 0d2b3f22370d591e2c0aa12ccb803156c9712ba6
3
+ metadata.gz: 1f0ab0c27e81412e4f124be731b6de0abb4ce723
4
+ data.tar.gz: 3d3e89d64df981809eebb4fdbdc8755033676a6c
5
5
  SHA512:
6
- metadata.gz: f8afcec27c1a5f503137ebbfba804f7392fed957a62058d2d1c2a3632121b72607cfb96bc2ef44ebc55049666fe55bc45620de08f93fc1b2b308303191ae51fe
7
- data.tar.gz: 2c7025c69534858a30b0c5717bf4575a98f3800f85b92111100f28185b6a891394066eab0bbd75f348658e837d836edcf18b5b58e5b188036878d9366adf4459
6
+ metadata.gz: 6f9e6883c2d960327de40b1d82fe22d38c1e8f8b43072ac6f6bf2ed8b1646b90e4e3ebeb8ed8d8c0006a8a02072ee1f4fddbb4d3787b0c179d72346f77f0d2dd
7
+ data.tar.gz: f5c77d39b0890fa5a5af2ebc5ea7a361c1f24a6ae6b2e5bc05f6c8d9f5289716ae26f180f922f66c74bf7af4a93af51906b627e0b87893ab15bd85a189e065cd
data/.semver CHANGED
@@ -1,5 +1,5 @@
1
1
  ---
2
2
  :major: 0
3
- :minor: 3
4
- :patch: 2
3
+ :minor: 4
4
+ :patch: 0
5
5
  :special: ''
data/README.md CHANGED
@@ -6,7 +6,7 @@
6
6
 
7
7
  Ladder is a dynamic framework for [Linked Data](http://en.wikipedia.org/wiki/Linked_data) modelling, persistence, and full-text indexing. It is implemented as a series of Ruby modules that can be used individually and incorporated within existing ActiveModel frameworks (eg. [Project Hydra](http://projecthydra.org)), or combined as a comprehensive stack.
8
8
 
9
- Ladder is intended to encourage the [GLAM](http://en.wikipedia.org/wiki/GLAM_(industry_sector)) community to think less dogmatically about established (often monolithic and/or niche) tools and instead embrace a broader vision of adopting more widely-used technologies.
9
+ Although conceptually similar to [Spira](https://github.com/ruby-rdf/spira), Ladder takes the opposite approach: instead of making RDF repositories (triple stores) behave like ActiveModel, it allows ActiveModel objects to behave like RDF resources.
10
10
 
11
11
  ### Components
12
12
 
@@ -17,7 +17,9 @@ Ladder is intended to encourage the [GLAM](http://en.wikipedia.org/wiki/GLAM_(in
17
17
 
18
18
  ## History
19
19
 
20
- Ladder was loosely conceived over the course of several years prior to 2011. In early 2012, Ladder began existence as an opportunity to escape from a decade of LAMP development and become familiar with Ruby. From 2012 to late 2013, a closed prototype was built under the auspices of [Deliberate Data](http://deliberatedata.com) as a proof-of-concept to test the feasibility of the design. For those interested in the historical code, the original [prototype](https://github.com/ladder/ladder/tree/prototype) branch is available, as is an [experimental](https://github.com/ladder/ladder/tree/l2) branch.
20
+ Ladder was loosely conceived over the course of several years prior to 2011 as a way to encourage the [GLAM](http://en.wikipedia.org/wiki/GLAM_(industry_sector)) community to think less dogmatically about established (often monolithic and/or niche) tools and instead embrace a broader vision of adopting more widely-used technologies.
21
+
22
+ In early 2012, Ladder began existence as an opportunity to escape from a decade of LAMP development and become familiar with Ruby. From 2012 to late 2013, a closed prototype was built under the auspices of [Deliberate Data](http://deliberatedata.com) as a proof-of-concept to test the feasibility of the design. For those interested in the historical code, the original [prototype](https://github.com/ladder/ladder/tree/prototype) branch is available, as is an [experimental](https://github.com/ladder/ladder/tree/l2) branch.
21
23
 
22
24
  ## Installation
23
25
 
@@ -38,13 +40,13 @@ Or install it yourself as:
38
40
  ## Usage
39
41
 
40
42
  * [Resources](#resources)
41
- * [Configuring Resources](#configuring-resources)
42
43
  * [Dynamic Resources](#dynamic-resources)
43
44
  * [Files](#files)
44
45
  * [Indexing](#indexing)
45
46
  * [Indexing Resources](#indexing-resources)
46
47
  * [Indexing Files](#indexing-files)
47
48
  * [Background Indexing](#background-indexing)
49
+ * [Configuration](#configuration)
48
50
 
49
51
  ### Resources
50
52
 
@@ -89,7 +91,19 @@ steve.as_jsonld
89
91
  # }
90
92
  ```
91
93
 
92
- The `#property` method takes care of setting both Mongoid fields and ActiveTriples properties. Properties with literal values are localized by default. Properties with a supplied `class_name:` will create a has-and-belongs-to-many (HABTM) relation:
94
+ The `#property` method takes care of setting both Mongoid fields and ActiveTriples properties. Properties with literal values (Mongoid fields) can be localized by default. Properties with a supplied `class_name:` will create a many-to-many relation.
95
+
96
+ By default, URIs are dynamically generated based on the name of the model class and the configured base URI. However, you can still set the base URI for a class explicitly just as you would in ActiveTriples, eg:
97
+
98
+ ```ruby
99
+ Person.base_uri
100
+ => #<RDF::URI:0x3fecf69da274 URI:http://example.org/people>
101
+
102
+ Person.configure base_uri: 'http://some.other.uri/'
103
+ => "http://some.other.uri/"
104
+ ```
105
+
106
+ See the [configuration](#configuration) section for more information on configuring default behaviour.
93
107
 
94
108
  ```ruby
95
109
  class Person
@@ -240,20 +254,6 @@ steve.as_jsonld
240
254
 
241
255
  Note in this case that both objects are included in the RDF graph, thanks to embedded relations. This can be useful to avoid additional queries to the database for objects that are tightly coupled.
242
256
 
243
- #### Configuring Resources
244
-
245
- If the LADDER_BASE_URI global constant is set, base URIs are dynamically generated based on the name of the model class. However, you can still set the base URI for a class explicitly just as you would in ActiveTriples:
246
-
247
- ```ruby
248
- LADDER_BASE_URI = 'http://example.org'
249
-
250
- Person.resource_class.base_uri
251
- => #<RDF::URI:0x3fecf69da274 URI:http://example.org/people>
252
-
253
- Person.configure base_uri: 'http://some.other.uri/'
254
- => "http://some.other.uri/"
255
- ```
256
-
257
257
  #### Dynamic Resources
258
258
 
259
259
  In line with ActiveTriples' [Open Model](https://github.com/ActiveTriples/ActiveTriples#open-model) design, you can define properties on any Resource instance similarly to how you would on the class:
@@ -363,7 +363,7 @@ class Image
363
363
  end
364
364
  ```
365
365
 
366
- Similar to Resources, using `#property` will create a has-many relation for a File by default; however, because Files must be the target of a one-way relation, the `inverse_of: nil` option is required. Note that due to the way GridFS is designed, Files **can not** be embedded.
366
+ As with Resources, using `#property` will create a many-to-many relation for a File by default; however, because Files must be the target of a one-way relation, the `inverse_of: nil` option is required (unless the `one_sided_relations` [configuration](#configuration) option is set). Note that due to the way GridFS is designed, Files **can not** be embedded.
367
367
 
368
368
  ```ruby
369
369
  steve = Person.new(first_name: 'Steve')
@@ -779,6 +779,24 @@ ActiveJob::Base.queue_adapter = :sidekiq
779
779
 
780
780
  For more information on available queueing adapters and their features, see the [ActiveJob documentation](http://api.rubyonrails.org/classes/ActiveJob/QueueAdapters.html).
781
781
 
782
+ ### Configuration
783
+
784
+ Configuration options are set using `Ladder::Config#settings`, eg:
785
+
786
+ ```ruby
787
+ Ladder::Config.settings[:base_uri] = 'http://example.org'
788
+ => "http://example.org"
789
+
790
+ Ladder::Config.settings
791
+ => {:base_uri=>"http://example.org", :localize_fields=>false, :one_sided_relations=>false}
792
+ ```
793
+
794
+ Ladder currently supports the following configuration options (defaults in parentheses):
795
+
796
+ * `:base_uri ('urn:x-ladder')`: Tells Ladder the base (root) URI to use for generating model URIs. For a Rack-based linked data application, this will typically be the HTTP(S) URL, eg. "http://some.domain/my_application/"
797
+ * `:localize_fields (false)`: When set to `true`, Ladder will set fields defined using `#property` to be localized by default.
798
+ * `:one_sided_relations (false)`: When set to `true`, Ladder will set relations defined using `#property` to be [one-sided many-to-many](http://mongoid.org/en/mongoid/docs/relations.html#has_and_belongs_to_many) relations. Otherwise, it will define has-and-belongs-to-many (HABTM) relations.
799
+
782
800
  ## Contributing
783
801
 
784
802
  Anyone and everyone is welcome to contribute. Go crazy.
@@ -1,7 +1,9 @@
1
1
  require 'ladder/version'
2
+ require 'ladder/config'
2
3
 
3
4
  module Ladder
4
- autoload :File, 'ladder/file'
5
- autoload :Resource, 'ladder/resource'
6
- autoload :Searchable, 'ladder/searchable'
5
+ autoload :File, 'ladder/file'
6
+ autoload :Resource, 'ladder/resource'
7
+ autoload :Searchable, 'ladder/searchable'
8
+ autoload :Configurable, 'ladder/configurable'
7
9
  end
@@ -0,0 +1,73 @@
1
+ require 'mongoid'
2
+ require 'mongoid/config/validators/option'
3
+ require 'uri'
4
+
5
+ module Ladder
6
+ module Config
7
+ extend self
8
+ extend ::Mongoid::Config::Options
9
+
10
+ LOCK = Mutex.new
11
+
12
+ option :base_uri, default: URI('urn:x-ladder') # typically a URL (configured)
13
+ option :localize_fields, default: false # self-explanatory
14
+ option :one_sided_relations, default: false # otherwise HABTM
15
+
16
+ # Get all the models in the application - this is everything that includes
17
+ # Ladder::Resource or Ladder::File.
18
+ #
19
+ # @example Get all the models.
20
+ # config.models
21
+ #
22
+ # @return [ Array<Class> ] All the models in the application.
23
+ #
24
+ # @since 3.1.0
25
+ def models
26
+ @models ||= []
27
+ end
28
+
29
+ # Register a model in the application with Ladder.
30
+ #
31
+ # @example Register a model.
32
+ # config.register_model(Band)
33
+ #
34
+ # @param [ Class ] klass The model to register.
35
+ #
36
+ # @since 3.1.0
37
+ def register_model(klass)
38
+ LOCK.synchronize do
39
+ models.push(klass) unless models.include?(klass)
40
+ end
41
+ end
42
+
43
+ # From a hash of settings, load all the configuration.
44
+ #
45
+ # @example Load the configuration.
46
+ # config.load_configuration(settings)
47
+ #
48
+ # @param [ Hash ] settings The configuration settings.
49
+ #
50
+ # @since 3.1.0
51
+ def load_configuration(settings)
52
+ configuration = settings.with_indifferent_access
53
+ self.options = configuration[:options]
54
+ end
55
+
56
+ # Set the configuration options. Will validate each one individually.
57
+ #
58
+ # @example Set the options.
59
+ # config.options = { raise_not_found_error: true }
60
+ #
61
+ # @param [ Hash ] options The configuration options.
62
+ #
63
+ # @since 3.0.0
64
+ def options=(options)
65
+ if options
66
+ options.each_pair do |option, value|
67
+ ::Mongoid::Config::Validators::Option.validate(option)
68
+ send("#{option}=", value)
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,26 @@
1
+ module Ladder
2
+ module Configurable
3
+ extend ActiveSupport::Concern
4
+
5
+ module ClassMethods
6
+ ##
7
+ # Set a default base URI based on the Ladder::Config settings
8
+ #
9
+ # @return [RDF::URI]
10
+ def base_uri
11
+ RDF::URI.new(Ladder::Config.settings[:base_uri]) / name.underscore.pluralize
12
+ end
13
+
14
+ protected
15
+
16
+ ##
17
+ # Register with Ladder and set the default base URI
18
+ #
19
+ # @return [void]
20
+ def configure_model
21
+ configure base_uri: base_uri
22
+ Ladder::Config.register_model self unless Ladder::Config.models.include? self
23
+ end
24
+ end
25
+ end
26
+ end
@@ -7,15 +7,24 @@ module Ladder
7
7
 
8
8
  include Mongoid::Document
9
9
  include ActiveTriples::Identifiable
10
+ include Ladder::Configurable
10
11
 
11
12
  included do
12
- configure base_uri: RDF::URI.new(LADDER_BASE_URI) / name.underscore.pluralize if defined? LADDER_BASE_URI
13
+ configure_model
13
14
 
14
15
  store_in collection: "#{ grid.prefix }.files"
15
16
 
16
17
  # Define accessor methods for attributes
17
18
  define_method(:content_type) { read_attribute(:contentType) }
18
19
 
20
+ # Attributes are:
21
+ # length -> RDF::DC.extent
22
+ # chunkSize -> (internal) ?
23
+ # uploadDate -> RDF::DC.created
24
+ # md5 -> premis:hasMessageDigest ? with premis:hasMessageDigestAlgorithm = 'MD5'
25
+ # contentType -> RDF::DC.format
26
+ # filename -> RDF::RDFS.label
27
+
19
28
  grid::File.fields.keys.map(&:to_sym).each do |attr|
20
29
  define_method(attr) { read_attribute(attr) }
21
30
  end
@@ -10,11 +10,10 @@ module Ladder
10
10
 
11
11
  include Mongoid::Document
12
12
  include ActiveTriples::Identifiable
13
+ include Ladder::Configurable
13
14
  include Ladder::Resource::Serializable
14
15
 
15
- included do
16
- configure base_uri: RDF::URI.new(LADDER_BASE_URI) / name.underscore.pluralize if defined? LADDER_BASE_URI
17
- end
16
+ included { configure_model }
18
17
 
19
18
  delegate :rdf_label, to: :update_resource
20
19
 
@@ -27,10 +26,10 @@ module Ladder
27
26
  # @return [ActiveTriples::Resource] resource for the object
28
27
  def update_resource(opts = {})
29
28
  resource_class.properties.each do |field_name, property|
30
- value = update_from_field(field_name) if fields[field_name]
31
- value = update_from_relation(field_name, opts[:related]) if relations[field_name]
29
+ values = update_from_field(field_name) if fields[field_name]
30
+ values = update_from_relation(field_name, opts[:related]) if relations[field_name]
32
31
 
33
- resource.set_value(property.predicate, value) # if value
32
+ resource.set_value(property.predicate, values)
34
33
  end
35
34
 
36
35
  resource
@@ -40,7 +39,7 @@ module Ladder
40
39
  # Push an RDF::Statement into the object
41
40
  #
42
41
  # @param [RDF::Statement, Hash, Array] statement @see RDF::Statement#from
43
- # @return [void]
42
+ # @return [Object, nil] the value inserted into the object
44
43
  def <<(statement)
45
44
  # ActiveTriples::Resource expects: RDF::Statement, Hash, or Array
46
45
  statement = RDF::Statement.from(statement) unless statement.is_a? RDF::Statement
@@ -49,24 +48,10 @@ module Ladder
49
48
  field_name = field_from_predicate(statement.predicate)
50
49
  return unless field_name
51
50
 
52
- # If the object is a URI, see if it is a retrievable model object
53
- value = Ladder::Resource.from_uri(statement.object) if statement.object.is_a? RDF::URI
54
-
55
- # TODO: tidy this code
56
- # subject (RDF::Term) - A symbol is converted to an interned Node.
57
- # predicate (RDF::URI)
58
- # object (RDF::Resource) - if not a Resource, it is coerced to Literal or Node
59
- # depending on if it is a symbol or something other than a Term.
60
- value = yield if block_given?
61
- value ||= statement.object.to_s
51
+ objects = statement.object.is_a?(RDF::Node) && block_given? ? yield : statement.object
62
52
 
63
- enum = send(field_name)
64
-
65
- if enum.is_a?(Enumerable)
66
- enum.send(:push, value) unless enum.include? value
67
- else
68
- send("#{field_name}=", value)
69
- end
53
+ update_field(field_name, *objects) if fields[field_name]
54
+ update_relation(field_name, *objects) if relations[field_name]
70
55
  end
71
56
 
72
57
  ##
@@ -84,8 +69,6 @@ module Ladder
84
69
  relation.class_name.constantize
85
70
  end
86
71
 
87
- private
88
-
89
72
  ##
90
73
  # Retrieve the attribute name for a field or relation,
91
74
  # based on its defined RDF predicate
@@ -99,23 +82,87 @@ module Ladder
99
82
  defined_prop.first
100
83
  end
101
84
 
85
+ private
86
+
102
87
  ##
103
- # Update the delegated ActiveTriples::Resource from a field
88
+ # Set values on a defined relation
104
89
  #
105
90
  # @param [String] field_name ActiveModel attribute name for the field
106
- # @return [void]
107
- def update_from_field(field_name)
91
+ # @param [Array<Object>] obj objects (usually Ladder::Resources) to be set
92
+ # @return [Ladder::Resource, nil]
93
+ def update_relation(field_name, *obj)
94
+ # Should be an Array of RDF::Term objects
95
+ return unless obj
96
+
97
+ obj.map! { |item| item.is_a?(RDF::URI) ? Ladder::Resource.from_uri(item) : item }
98
+ relation = send(field_name)
99
+
100
+ if Mongoid::Relations::Targets::Enumerable == relation.class
101
+ obj.map { |item| relation.send(:push, item) unless relation.include? item }
102
+ else
103
+ send("#{field_name}=", obj.size > 1 ? obj : obj.first)
104
+ end
105
+ end
106
+
107
+ ##
108
+ # Set values on a field; this will cast values
109
+ # from RDF types to persistable Mongoid types
110
+ #
111
+ # @param [String] field_name ActiveModel attribute name for the field
112
+ # @param [Array<Object>] obj objects (usually RDF::Terms) to be set
113
+ # @return [Object, nil]
114
+ def update_field(field_name, *obj)
115
+ # Should be an Array of RDF::Term objects
116
+ return unless obj
117
+
108
118
  if fields[field_name].localized?
109
- localized_hash = read_attribute(field_name)
119
+ trans = {}
110
120
 
111
- unless localized_hash.nil?
112
- localized_hash.map do |lang, value|
113
- cast_uri = RDF::URI.new(value)
114
- cast_uri.valid? ? cast_uri : RDF::Literal.new(value, language: lang)
115
- end
121
+ obj.each do |item|
122
+ lang = item.is_a?(RDF::Literal) && item.has_language? ? item.language.to_s : I18n.locale.to_s
123
+ value = item.is_a?(RDF::URI) ? item.to_s : item.object # TODO: tidy this up
124
+ trans[lang] = trans[lang] ? [*trans[lang]] << value : value
116
125
  end
126
+
127
+ send("#{field_name}_translations=", trans) unless trans.empty?
117
128
  else
118
- send(field_name)
129
+ objects = obj.map { |item| item.is_a?(RDF::URI) ? item.to_s : item.object } # TODO: tidy this up
130
+ send("#{field_name}=", objects.size > 1 ? objects : objects.first)
131
+ end
132
+ end
133
+
134
+ ##
135
+ # Cast values from Mongoid types to RDF types
136
+ #
137
+ # @param [Object] value ActiveModel attribute to be cast
138
+ # @param [Hash] opts options to pass to RDF::Literal
139
+ # @return [RDF::Term]
140
+ def cast_value(value, opts = {})
141
+ case value
142
+ when Array
143
+ value.map { |v| cast_value(v, opts) }
144
+ when String
145
+ cast_uri = RDF::URI.new(value)
146
+ cast_uri.valid? ? cast_uri : RDF::Literal.new(value, opts)
147
+ when Time
148
+ # Cast DateTimes with 00:00:00 or Date stored as Times in Mongoid to xsd:date
149
+ # FIXME: this should NOT be applied for fields that are typed as Time
150
+ value.midnight == value ? RDF::Literal.new(value.to_date) : RDF::Literal.new(value.to_datetime)
151
+ else
152
+ RDF::Literal.new(value, opts)
153
+ end
154
+ end
155
+
156
+ ##
157
+ # Update the delegated ActiveTriples::Resource from a field
158
+ #
159
+ # @param [String] field_name ActiveModel attribute name for the field
160
+ # @return [Object]
161
+ def update_from_field(field_name)
162
+ if fields[field_name].localized?
163
+ read_attribute(field_name).to_a.map { |lang, value| cast_value(value, language: lang) }.flatten
164
+ else
165
+ cast_value send(field_name)
119
166
  end
120
167
  end
121
168
 
@@ -124,7 +171,7 @@ module Ladder
124
171
  #
125
172
  # @param [String] field_name ActiveModel attribute name for the relation
126
173
  # @param [Boolean] related whether to include related objects
127
- # @return [void]
174
+ # @return [Enumerable]
128
175
  def update_from_relation(field_name, related = false)
129
176
  objects = send(field_name).to_a
130
177
 
@@ -133,8 +180,8 @@ module Ladder
133
180
  methods.select { |i| i[/autosave_documents/] }.each { |m| send m }
134
181
 
135
182
  # update inverse relation properties
136
- relation_def = relations[field_name]
137
- objects.each { |object| object.resource.set_value(relation_def.inverse, rdf_subject) } if relation_def.inverse
183
+ relation = relations[field_name]
184
+ objects.each { |object| object.resource.set_value(relation.inverse, rdf_subject) } if relation.inverse
138
185
  objects.map(&:update_resource)
139
186
  else
140
187
  # remove inverse relation properties
@@ -159,9 +206,12 @@ module Ladder
159
206
  def property(field_name, opts = {})
160
207
  if opts[:class_name]
161
208
  mongoid_opts = { autosave: true, index: true }.merge(opts.except(:predicate, :multivalue))
162
- has_and_belongs_to_many(field_name, mongoid_opts) unless relations.keys.include? field_name.to_s
209
+ # TODO: add/fix tests for this behaviour when true
210
+ mongoid_opts[:inverse_of] = nil if Ladder::Config.settings[:one_sided_relations]
211
+
212
+ has_and_belongs_to_many(field_name, mongoid_opts) unless relations[field_name.to_s]
163
213
  else
164
- mongoid_opts = { localize: true }.merge(opts.except(:predicate, :multivalue))
214
+ mongoid_opts = { localize: Ladder::Config.settings[:base_uri] }.merge(opts.except(:predicate, :multivalue))
165
215
  field(field_name, mongoid_opts) unless fields[field_name.to_s]
166
216
  end
167
217
 
@@ -180,7 +230,8 @@ module Ladder
180
230
  #
181
231
  # As nodes are traversed in the graph, the instantiated objects
182
232
  # will be added to a Hash that is passed recursively, in order to
183
- # prevent infinite traversal in the case of cyclic graphs.
233
+ # prevent infinite traversal in the case of cyclic graphs (ie.
234
+ # mark-and-sweep graph traversal).
184
235
  #
185
236
  # @param [RDF::Graph] graph an RDF::Graph to traverse
186
237
  # @param [Hash] objects a keyed Hash of already-created objects in the graph
@@ -200,30 +251,50 @@ module Ladder
200
251
  # Add object to stack for recursion
201
252
  objects[root_subject] = new_object
202
253
 
203
- graph.query([root_subject]).each_statement do |statement|
204
- next if objects[statement.object]
254
+ subgraph = graph.query([root_subject])
205
255
 
206
- # TODO: If the object is a list, process members individually
207
- # list = RDF::List.new statement.object, graph
208
- # binding.pry unless list.empty?
256
+ subgraph.each_statement do |statement|
257
+ # Group statements for this predicate
258
+ stmts = subgraph.query([root_subject, statement.predicate])
209
259
 
210
- # If the object is a BNode, dereference the relation
211
- if statement.object.is_a? RDF::Node
212
- klass = new_object.klass_from_predicate(statement.predicate)
213
- next unless klass
260
+ if stmts.size > 1
261
+ # We have already set this value in a prior pass
262
+ next if new_object.read_attribute new_object.field_from_predicate statement.predicate
214
263
 
215
- object = klass.new_from_graph(graph, objects)
216
- next unless object
264
+ # If there are multiple statements for this predicate, pass an array
265
+ statement.object = RDF::Node.new
266
+ new_object.send(:<<, statement) { stmts.objects.to_a } # TODO: implement #set_value instead
217
267
 
218
- objects[statement.object] = object
219
- new_object.send(:<<, statement) { object }
220
- else
221
- new_object << statement
268
+ elsif statement.object.is_a? RDF::Node
269
+ next if objects[statement.object]
270
+
271
+ # If the object is a BNode, dereference the relation
272
+ objects[statement.object] = new_object.send(:<<, statement) do # TODO: implement #set_value instead
273
+ klass = new_object.klass_from_predicate(statement.predicate)
274
+ klass.new_from_graph(graph, objects, [statement.object]) if klass
275
+ end
276
+
277
+ else new_object << statement
222
278
  end
223
279
  end # end each_statement
224
280
 
225
281
  new_object
226
282
  end
283
+
284
+ protected
285
+
286
+ ##
287
+ # Propagate base URI and properties to subclasses
288
+ #
289
+ # @return [void]
290
+ def inherited(subclass)
291
+ # Copy properties from parent to subclass
292
+ resource_class.properties.each do |_name, config|
293
+ subclass.property config.term, predicate: config.predicate, class_name: config.class_name
294
+ end
295
+
296
+ subclass.configure_model
297
+ end
227
298
  end
228
299
 
229
300
  ##
@@ -236,14 +307,13 @@ module Ladder
236
307
  # @param [RDF::URI] uri RDF subject URI for the resource
237
308
  # @return [Ladder::Resource] a resource instance
238
309
  def self.from_uri(uri)
239
- klasses = ActiveTriples::Resource.descendants.select(&:name)
240
- klass = klasses.find { |k| uri.to_s.include? k.base_uri.to_s }
310
+ klass = Ladder::Config.models.find { |k| uri.to_s.include? k.resource_class.base_uri.to_s }
241
311
 
242
312
  if klass
243
313
  object_id = uri.to_s.match(/[0-9a-fA-F]{24}/).to_s
244
314
 
245
315
  # Retrieve the object if it's persisted, otherwise return a new one (eg. embedded)
246
- return klass.parent.where(id: object_id).exists? ? klass.parent.find(object_id) : klass.parent.new
316
+ return klass.where(id: object_id).exists? ? klass.find(object_id) : klass.new
247
317
  end
248
318
  end
249
319
  end