guacamole 0.3.0 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
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