guacamole 0.3.0 → 0.4.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.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/.hound.yml +1 -1
  3. data/.travis.yml +16 -15
  4. data/CHANGELOG.md +16 -0
  5. data/GOALS.md +8 -0
  6. data/Guardfile +4 -0
  7. data/README.md +21 -81
  8. data/guacamole.gemspec +3 -3
  9. data/lib/guacamole.rb +1 -0
  10. data/lib/guacamole/aql_query.rb +6 -1
  11. data/lib/guacamole/collection.rb +34 -66
  12. data/lib/guacamole/configuration.rb +53 -25
  13. data/lib/guacamole/document_model_mapper.rb +149 -38
  14. data/lib/guacamole/edge.rb +74 -0
  15. data/lib/guacamole/edge_collection.rb +91 -0
  16. data/lib/guacamole/exceptions.rb +0 -5
  17. data/lib/guacamole/graph_query.rb +31 -0
  18. data/lib/guacamole/model.rb +4 -0
  19. data/lib/guacamole/proxies/proxy.rb +7 -3
  20. data/lib/guacamole/proxies/relation.rb +22 -0
  21. data/lib/guacamole/railtie.rb +1 -1
  22. data/lib/guacamole/transaction.rb +177 -0
  23. data/lib/guacamole/version.rb +1 -1
  24. data/shared/transaction.js +66 -0
  25. data/spec/acceptance/aql_spec.rb +32 -40
  26. data/spec/acceptance/relations_spec.rb +239 -0
  27. data/spec/acceptance/spec_helper.rb +2 -2
  28. data/spec/fabricators/author_fabricator.rb +2 -0
  29. data/spec/setup/arangodb.sh +2 -2
  30. data/spec/unit/collection_spec.rb +20 -97
  31. data/spec/unit/configuration_spec.rb +73 -50
  32. data/spec/unit/document_model_mapper_spec.rb +84 -77
  33. data/spec/unit/edge_collection_spec.rb +174 -0
  34. data/spec/unit/edge_spec.rb +57 -0
  35. data/spec/unit/proxies/relation_spec.rb +35 -0
  36. metadata +22 -14
  37. data/lib/guacamole/proxies/referenced_by.rb +0 -15
  38. data/lib/guacamole/proxies/references.rb +0 -15
  39. data/spec/acceptance/association_spec.rb +0 -40
  40. data/spec/unit/example_spec.rb +0 -8
@@ -71,7 +71,7 @@ module Guacamole
71
71
  class Configuration
72
72
  # A wrapper object to handle both configuration from a connection URI and a hash.
73
73
  class ConfigStruct
74
- attr_reader :url, :username, :password, :database
74
+ attr_reader :url, :username, :password, :database, :graph
75
75
 
76
76
  def initialize(config_hash_or_url)
77
77
  case config_hash_or_url
@@ -98,6 +98,7 @@ module Guacamole
98
98
  @username = hash['username']
99
99
  @password = hash['password']
100
100
  @database = hash['database']
101
+ @graph = hash['graph']
101
102
  @url = "#{hash['protocol']}://#{hash['host']}:#{hash['port']}"
102
103
  end
103
104
  end
@@ -105,18 +106,13 @@ module Guacamole
105
106
  # @!visibility protected
106
107
  attr_accessor :database, :default_mapper, :logger
107
108
 
108
- AVAILABLE_EXPERIMENTAL_FEATURES = [
109
- :aql_support
110
- ]
111
-
112
109
  class << self
113
110
  extend Forwardable
114
111
 
115
112
  def_delegators :configuration,
116
113
  :database, :database=,
117
114
  :default_mapper=,
118
- :logger=,
119
- :experimental_features=, :experimental_features
115
+ :logger=
120
116
 
121
117
  def default_mapper
122
118
  configuration.default_mapper || (self.default_mapper = Guacamole::DocumentModelMapper)
@@ -126,14 +122,61 @@ module Guacamole
126
122
  configuration.logger ||= (rails_logger || default_logger)
127
123
  end
128
124
 
125
+ def shared_path
126
+ Pathname.new(File.join(__dir__, '..', '..', 'shared'))
127
+ end
128
+
129
+ # Returns the graph associated with this Guacamole application.
130
+ #
131
+ # You can create more graphs by interacting with the database instance directly. This is just the main graph
132
+ # that will be used to realize relations between models. The default name will be generated based on some
133
+ # environment information. To set a custom name use the `graph_name` option when configure your connection.
134
+ #
135
+ # @return [Graph] The graph to be used internally to handle relations
136
+ def graph
137
+ database.graph(graph_name)
138
+ end
139
+
140
+ # Sets a custom name for the internally used graph
141
+ #
142
+ # @param [String] graph_name The name of the graph to be used
143
+ def graph_name=(graph_name)
144
+ @graph_name = graph_name
145
+ end
146
+
147
+ # The name of the graph to be used internally.
148
+ #
149
+ # Determining the name of the graph will go through the following steps:
150
+ #
151
+ # 1. Use the manually set `graph_name`
152
+ # 2. Use the ENV variable `GUACAMOLE_GRAPH` if present.
153
+ # This is useful if you configure the application with ENV variables.
154
+ # 3. If a Rails context was found it will use the name of the application with a `_graph` suffix
155
+ # 4. If none of the above matched it will use the database name with a `_graph` suffix
156
+ #
157
+ # @return [String] The name of the graph to be used
158
+ def graph_name
159
+ return @graph_name if @graph_name
160
+ return ENV['GUACAMOLE_GRAPH'] if ENV['GUACAMOLE_GRAPH']
161
+
162
+ base_name = if Module.const_defined?('Rails')
163
+ Rails.application.class.name.deconstantize.underscore
164
+ else
165
+ database.name
166
+ end
167
+
168
+ [base_name, 'graph'].join('_')
169
+ end
170
+
129
171
  # Load a YAML configuration file to configure Guacamole
130
172
  #
131
173
  # @param [String] file_name The file name of the configuration
132
174
  def load(file_name)
133
- yaml_content = process_file_with_erb(file_name)
134
- config = YAML.load(yaml_content)[current_environment.to_s]
175
+ yaml_content = process_file_with_erb(file_name)
176
+ config = build_config(YAML.load(yaml_content)[current_environment.to_s])
177
+ self.graph_name = config.graph
135
178
 
136
- create_database_connection(build_config(config))
179
+ create_database_connection(config)
137
180
  warn_if_database_was_not_yet_created
138
181
  end
139
182
 
@@ -212,20 +255,5 @@ module Guacamole
212
255
  ERB.new(File.read(file_name)).result
213
256
  end
214
257
  end
215
-
216
- # A list of active experimental features. Refer to `AVAILABLE_EXPERIMENTAL_FEATURES` to see
217
- # what can be activated.
218
- #
219
- # @return [Array<Symbol>] The activated experimental features. Defaults to `[]`
220
- def experimental_features
221
- @experimental_features || []
222
- end
223
-
224
- # Experimental features to activate
225
- #
226
- # @param [Array<Symbol>] features A list of experimental features to activate
227
- def experimental_features=(features)
228
- @experimental_features = features
229
- end
230
258
  end
231
259
  end
@@ -1,7 +1,6 @@
1
1
  # -*- encoding : utf-8 -*-
2
2
 
3
- require 'guacamole/proxies/referenced_by'
4
- require 'guacamole/proxies/references'
3
+ require 'guacamole/proxies/relation'
5
4
 
6
5
  module Guacamole
7
6
  # This is the default mapper class to map between Ashikawa::Core::Document and
@@ -12,6 +11,80 @@ module Guacamole
12
11
  #
13
12
  # @note If you plan to bring your own `DocumentModelMapper` please consider using an {Guacamole::IdentityMap}.
14
13
  class DocumentModelMapper
14
+ # An attribute to encapsulate special mapping
15
+ class Attribute
16
+ # The name of the attribute with in the model
17
+ #
18
+ # @return [Symbol] The name of the attribute
19
+ attr_reader :name
20
+
21
+ # Additional options to be used for the mapping
22
+ #
23
+ # @return [Hash] The mapping options for the attribute
24
+ attr_reader :options
25
+
26
+ # Create a new attribute instance
27
+ #
28
+ # You must at least provide the name of the attribute to be mapped and
29
+ # optionally pass configuration for the mapper when it processes this attribute.
30
+ #
31
+ # @param [Symbol] name The name of the attribute
32
+ # @param [Hash] options Additional options to be passed
33
+ # @option options [Edge] :via The Edge class this attribute relates to
34
+ def initialize(name, options = {})
35
+ @name = name.to_sym
36
+ @options = options
37
+ end
38
+
39
+ # The name of the getter for this attribute
40
+ #
41
+ # @returns [Symbol] The method name to read this attribute
42
+ def getter
43
+ name
44
+ end
45
+
46
+ def get_value(model)
47
+ value = model.send(getter)
48
+
49
+ value.is_a?(Guacamole::Query) ? value.entries : value
50
+ end
51
+
52
+ # The name of the setter for this attribute
53
+ #
54
+ # @return [String] The method name to set this attribute
55
+ def setter
56
+ "#{name}="
57
+ end
58
+
59
+ # Should this attribute be mapped via an Edge in a Graph?
60
+ #
61
+ # @return [Boolean] True if there was an edge class configured
62
+ def map_via_edge?
63
+ !!edge_class
64
+ end
65
+
66
+ # The edge class to be used during the mapping process
67
+ #
68
+ # @return [Edge] The actual edge class
69
+ def edge_class
70
+ options[:via]
71
+ end
72
+
73
+ def inverse?
74
+ !!options[:inverse]
75
+ end
76
+
77
+ # To Attribute instances are equal if their name is equal
78
+ #
79
+ # @param [Attribute] other The Attribute to compare this one to
80
+ # @return [Boolean] True if both have the same name
81
+ def ==(other)
82
+ other.instance_of?(self.class) &&
83
+ other.name == name
84
+ end
85
+ alias_method :eql?, :==
86
+ end
87
+
15
88
  # The class to map to
16
89
  #
17
90
  # @return [class] The class to map to
@@ -21,8 +94,11 @@ module Guacamole
21
94
  #
22
95
  # @return [Array] An array of embedded models
23
96
  attr_reader :models_to_embed
24
- attr_reader :referenced_by_models
25
- attr_reader :referenced_models
97
+
98
+ # The list of Attributes to treat specially during the mapping process
99
+ #
100
+ # @return [Array<Attribute>] The list of special attributes
101
+ attr_reader :attributes
26
102
 
27
103
  # Create a new instance of the mapper
28
104
  #
@@ -34,8 +110,7 @@ module Guacamole
34
110
  @model_class = model_class
35
111
  @identity_map = identity_map
36
112
  @models_to_embed = []
37
- @referenced_by_models = []
38
- @referenced_models = []
113
+ @attributes = []
39
114
  end
40
115
 
41
116
  class << self
@@ -64,7 +139,7 @@ module Guacamole
64
139
  # seems to be a good place for this functionality.
65
140
  # @param [symbol, string] model_name the name of the model
66
141
  # @return [class] the {collection} class for the given model name
67
- def collection_for(model_name)
142
+ def collection_for(model_name = model_class.name)
68
143
  self.class.collection_for model_name
69
144
  end
70
145
 
@@ -74,26 +149,18 @@ module Guacamole
74
149
  #
75
150
  # @param [Ashikawa::Core::Document] document
76
151
  # @return [Model] the resulting model with the given Model class
77
- # rubocop:disable MethodLength
78
152
  def document_to_model(document)
79
153
  identity_map.retrieve_or_store model_class, document.key do
80
154
  model = model_class.new(document.to_h)
81
155
 
82
- referenced_by_models.each do |ref_model_name|
83
- model.send("#{ref_model_name}=", Proxies::ReferencedBy.new(ref_model_name, model))
84
- end
85
-
86
- referenced_models.each do |ref_model_name|
87
- model.send("#{ref_model_name}=", Proxies::References.new(ref_model_name, document))
88
- end
89
-
90
156
  model.key = document.key
91
157
  model.rev = document.revision
92
158
 
159
+ handle_related_documents(model)
160
+
93
161
  model
94
162
  end
95
163
  end
96
- # rubocop:enable MethodLength
97
164
 
98
165
  # Map a model to a document
99
166
  #
@@ -101,29 +168,14 @@ module Guacamole
101
168
  #
102
169
  # @param [Model] model
103
170
  # @return [Ashikawa::Core::Document] the resulting document
104
- # rubocop:disable MethodLength
105
171
  def model_to_document(model)
106
172
  document = model.attributes.dup.except(:key, :rev)
107
- models_to_embed.each do |attribute_name|
108
- document[attribute_name] = model.send(attribute_name).map do |embedded_model|
109
- embedded_model.attributes.except(:key, :rev)
110
- end
111
- end
112
173
 
113
- referenced_models.each do |ref_model_name|
114
- ref_key = [ref_model_name.to_s, 'id'].join('_').to_sym
115
- ref_model = model.send ref_model_name
116
- document[ref_key] = ref_model.key if ref_model
117
- document.delete(ref_model_name)
118
- end
119
-
120
- referenced_by_models.each do |ref_model_name|
121
- document.delete(ref_model_name)
122
- end
174
+ handle_embedded_models(model, document)
175
+ handle_related_models(document)
123
176
 
124
177
  document
125
178
  end
126
- # rubocop:enable MethodLength
127
179
 
128
180
  # Declare a model to be embedded
129
181
  #
@@ -158,12 +210,43 @@ module Guacamole
158
210
  @models_to_embed << model_name
159
211
  end
160
212
 
161
- def referenced_by(model_name)
162
- @referenced_by_models << model_name
213
+ # Mark an attribute of the model to be specially treated during mapping
214
+ #
215
+ # @param [Symbol] attribute_name The name of the model attribute
216
+ # @param [Hash] options Additional options to configure the mapping process
217
+ # @option options [Edge] :via The Edge class this attribute relates to
218
+ # @example Define a relation via an Edge in a Graph
219
+ # class Authorship
220
+ # include Guacamole::Edge
221
+ #
222
+ # from :users
223
+ # to :posts
224
+ # end
225
+ #
226
+ # class BlogpostsCollection
227
+ # include Guacamole::Collection
228
+ #
229
+ # map do
230
+ # attribute :author, via: Authorship
231
+ # end
232
+ # end
233
+ def attribute(attribute_name, options = {})
234
+ @attributes << Attribute.new(attribute_name, options)
235
+ end
236
+
237
+ # Returns a list of attributes that have an Edge class configured
238
+ #
239
+ # @return [Array<Attribute>] A list of attributes which all have an Edge class
240
+ def edge_attributes
241
+ attributes.select(&:map_via_edge?)
163
242
  end
164
243
 
165
- def references(model_name)
166
- @referenced_models << model_name
244
+ # Is this Mapper instance responsible for mapping the given model
245
+ #
246
+ # @param [Model] model The model to check against
247
+ # @return [Boolean] True if the given model is an instance of #model_class. False if not.
248
+ def responsible_for?(model)
249
+ model.instance_of?(model_class)
167
250
  end
168
251
 
169
252
  private
@@ -171,5 +254,33 @@ module Guacamole
171
254
  def identity_map
172
255
  @identity_map
173
256
  end
257
+
258
+ def handle_embedded_models(model, document)
259
+ models_to_embed.each do |attribute_name|
260
+ document[attribute_name] = model.send(attribute_name).map do |embedded_model|
261
+ embedded_model.attributes.except(:key, :rev)
262
+ end
263
+ end
264
+ end
265
+
266
+ def handle_related_models(document)
267
+ edge_attributes.each do |edge_attribute|
268
+ document.delete(edge_attribute.name)
269
+ end
270
+ end
271
+
272
+ def handle_related_documents(model)
273
+ edge_attributes.each do |edge_attribute|
274
+ just_one = case model.class.attribute_set[edge_attribute.name].type
275
+ when Virtus::Attribute::Collection::Type then false
276
+ else
277
+ true
278
+ end
279
+
280
+ opts = { just_one: just_one, inverse: edge_attribute.inverse? }
281
+
282
+ model.send(edge_attribute.setter, Proxies::Relation.new(model, edge_attribute.edge_class, opts))
283
+ end
284
+ end
174
285
  end
175
286
  end
@@ -0,0 +1,74 @@
1
+ # -*- encoding : utf-8 -*-
2
+
3
+ require 'guacamole/model'
4
+
5
+ require 'active_support'
6
+ require 'active_support/concern'
7
+
8
+ module Guacamole
9
+ # An Edge representing a relation between two models within a Graph
10
+ #
11
+ # A Guacamole::Edge is specialized model with two predefined attributes (`from` and `to`)
12
+ # and a class level DSL to define the relation between models inside a Graph. Like normal
13
+ # models, edge models don't know the database. But unlike the collection classes you define
14
+ # yourself for your models Guacamole will create a default collection class to be used with
15
+ # your edge models.
16
+ #
17
+ # @!attribute [r] from
18
+ # The model on the from side of the edge
19
+ #
20
+ # @return [Guacamole::Model] The document from which the relation originates
21
+ #
22
+ # @!attribute [r] to
23
+ # The model on the to side of the edge
24
+ #
25
+ # @return [Guacamole::Model] The document to which the relation directs
26
+ #
27
+ # @!method self.from(collection_name)
28
+ # Define the collection from which all these edges will originate
29
+ #
30
+ # @api public
31
+ # @param [Symbol] collection_name The name of the originating collection
32
+ #
33
+ # @!method self.to(collection_name)
34
+ # Define the collection to which all these edges will direct
35
+ #
36
+ # @api public
37
+ # @param [Symbol] collection_name The name of the target collection
38
+ module Edge
39
+ extend ActiveSupport::Concern
40
+
41
+ included do
42
+ include Guacamole::Model
43
+
44
+ attribute :from, Object
45
+ attribute :to, Object
46
+ end
47
+
48
+ module ClassMethods
49
+ def from(collection_name = nil)
50
+ if collection_name.nil?
51
+ @from
52
+ else
53
+ @from = collection_name
54
+ end
55
+ end
56
+
57
+ def to(collection_name = nil)
58
+ if collection_name.nil?
59
+ @to
60
+ else
61
+ @to = collection_name
62
+ end
63
+ end
64
+
65
+ def to_collection
66
+ [to.to_s.camelcase, 'Collection'].join('').constantize
67
+ end
68
+
69
+ def from_collection
70
+ [from.to_s.camelcase, 'Collection'].join('').constantize
71
+ end
72
+ end
73
+ end
74
+ end