guacamole 0.0.1 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (50) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/{config/rubocop.yml → .hound.yml} +1 -12
  4. data/.ruby-version +1 -1
  5. data/.travis.yml +2 -0
  6. data/.yardopts +1 -0
  7. data/CONTRIBUTING.md +3 -3
  8. data/Gemfile.devtools +24 -12
  9. data/Guardfile +1 -1
  10. data/README.md +347 -50
  11. data/Rakefile +10 -0
  12. data/config/reek.yml +18 -5
  13. data/guacamole.gemspec +5 -2
  14. data/lib/guacamole.rb +1 -0
  15. data/lib/guacamole/collection.rb +79 -7
  16. data/lib/guacamole/configuration.rb +56 -2
  17. data/lib/guacamole/document_model_mapper.rb +87 -7
  18. data/lib/guacamole/identity_map.rb +124 -0
  19. data/lib/guacamole/proxies/proxy.rb +42 -0
  20. data/lib/guacamole/proxies/referenced_by.rb +15 -0
  21. data/lib/guacamole/proxies/references.rb +15 -0
  22. data/lib/guacamole/query.rb +11 -0
  23. data/lib/guacamole/railtie.rb +6 -1
  24. data/lib/guacamole/railtie/database.rake +57 -3
  25. data/lib/guacamole/tasks/database.rake +23 -0
  26. data/lib/guacamole/version.rb +1 -1
  27. data/lib/rails/generators/guacamole/collection/collection_generator.rb +19 -0
  28. data/lib/rails/generators/guacamole/collection/templates/collection.rb.tt +5 -0
  29. data/lib/rails/generators/guacamole/config/config_generator.rb +25 -0
  30. data/lib/rails/generators/guacamole/config/templates/guacamole.yml +15 -0
  31. data/lib/rails/generators/guacamole/model/model_generator.rb +25 -0
  32. data/lib/rails/generators/guacamole/model/templates/model.rb.tt +11 -0
  33. data/lib/rails/generators/guacamole_generator.rb +28 -0
  34. data/lib/rails/generators/rails/collection/collection_generator.rb +13 -0
  35. data/lib/rails/generators/rspec/collection/collection_generator.rb +13 -0
  36. data/lib/rails/generators/rspec/collection/templates/collection_spec.rb.tt +7 -0
  37. data/spec/acceptance/association_spec.rb +40 -0
  38. data/spec/acceptance/basic_spec.rb +19 -2
  39. data/spec/acceptance/spec_helper.rb +5 -2
  40. data/spec/fabricators/author.rb +11 -0
  41. data/spec/fabricators/author_fabricator.rb +7 -0
  42. data/spec/fabricators/book.rb +11 -0
  43. data/spec/fabricators/book_fabricator.rb +5 -0
  44. data/spec/unit/collection_spec.rb +265 -18
  45. data/spec/unit/configuration_spec.rb +11 -1
  46. data/spec/unit/document_model_mapper_spec.rb +127 -5
  47. data/spec/unit/identiy_map_spec.rb +140 -0
  48. data/spec/unit/query_spec.rb +37 -16
  49. data/tasks/adjustments.rake +0 -1
  50. metadata +78 -8
data/Rakefile CHANGED
@@ -5,3 +5,13 @@ require 'devtools'
5
5
  Devtools.init_rake_tasks
6
6
 
7
7
  import('./tasks/adjustments.rake')
8
+
9
+ desc 'Start a REPL with guacamole loaded (not the Rails part)'
10
+ task :console do
11
+ require 'bundler/setup'
12
+
13
+ require 'pry'
14
+ require 'guacamole'
15
+ ARGV.clear
16
+ Pry.start
17
+ end
data/config/reek.yml CHANGED
@@ -4,7 +4,8 @@ Attribute:
4
4
  exclude: []
5
5
  BooleanParameter:
6
6
  enabled: true
7
- exclude: []
7
+ exclude:
8
+ - respond_to_missing?
8
9
  ClassVariable:
9
10
  enabled: true
10
11
  exclude: []
@@ -18,7 +19,10 @@ DataClump:
18
19
  min_clump_size: 2
19
20
  DuplicateMethodCall:
20
21
  enabled: true
21
- exclude: []
22
+ exclude:
23
+ - Guacamole::DocumentModelMapper#document_to_model
24
+ - Guacamole::DocumentModelMapper#model_to_document
25
+ - Guacamole::Configuration#_add_missing_methods_to_database
22
26
  max_calls: 1
23
27
  allow_calls: []
24
28
  FeatureEnvy:
@@ -26,7 +30,8 @@ FeatureEnvy:
26
30
  exclude: []
27
31
  IrresponsibleModule:
28
32
  enabled: true
29
- exclude: []
33
+ exclude:
34
+ - - !ruby/regexp /Generators/
30
35
  LongParameterList:
31
36
  enabled: true
32
37
  exclude: []
@@ -40,7 +45,8 @@ LongYieldList:
40
45
  max_params: 2
41
46
  NestedIterators:
42
47
  enabled: true
43
- exclude: []
48
+ exclude:
49
+ - Guacamole::Configuration#_add_missing_methods_to_database
44
50
  max_allowed_nesting: 2
45
51
  ignore_iterators: []
46
52
  NilCheck:
@@ -52,7 +58,8 @@ RepeatedConditional:
52
58
  max_ifs: 2
53
59
  TooManyInstanceVariables:
54
60
  enabled: true
55
- exclude: []
61
+ exclude:
62
+ - Guacamole::DocumentModelMapper
56
63
  max_instance_variables: 3
57
64
  TooManyMethods:
58
65
  enabled: true
@@ -62,6 +69,12 @@ TooManyStatements:
62
69
  enabled: true
63
70
  exclude:
64
71
  - each
72
+ - Guacamole::DocumentModelMapper#document_to_model
73
+ - Guacamole::DocumentModelMapper#model_to_document
74
+ - Guacamole::Collection::ClassMethods#create_document_from
75
+ - Guacamole::Collection::ClassMethods#create_referenced_by_models_of
76
+ - Guacamole::Configuration#_add_missing_methods_to_database
77
+ - Guacamole::Configuration#create_database_connection_from
65
78
  max_statements: 5
66
79
  UncommunicativeMethodName:
67
80
  enabled: true
data/guacamole.gemspec CHANGED
@@ -18,11 +18,14 @@ Gem::Specification.new do |spec|
18
18
  spec.test_files = spec.files.grep(%r{^(spec)/})
19
19
  spec.require_paths = ['lib']
20
20
 
21
- spec.add_dependency 'ashikawa-core', '~> 0.9.0'
22
- spec.add_dependency 'virtus', '~> 1.0.0.rc2'
21
+ spec.add_dependency 'ashikawa-core', '~> 0.10.0'
22
+ spec.add_dependency 'virtus', '~> 1.0.1'
23
23
  spec.add_dependency 'activesupport', '>= 4.0.0'
24
24
  spec.add_dependency 'activemodel', '>= 4.0.0'
25
+ spec.add_dependency 'hamster', '~> 1.0.1.pre.rc.1'
25
26
 
26
27
  spec.add_development_dependency 'fabrication', '~> 2.8.1'
28
+ spec.add_development_dependency 'faker', '~> 1.2.0'
27
29
  spec.add_development_dependency 'logging', '~> 1.8.1'
30
+ spec.add_development_dependency 'pry', '~> 0.9.12'
28
31
  end
data/lib/guacamole.rb CHANGED
@@ -7,6 +7,7 @@ require 'guacamole/configuration'
7
7
  require 'guacamole/model'
8
8
  require 'guacamole/collection'
9
9
  require 'guacamole/document_model_mapper'
10
+ require 'guacamole/identity_map'
10
11
 
11
12
  if defined?(Rails)
12
13
  require 'guacamole/railtie'
@@ -16,7 +16,6 @@ module Guacamole
16
16
  # the collection. See the `ClassMethods` submodule for details
17
17
  module Collection
18
18
  extend ActiveSupport::Concern
19
-
20
19
  # The class methods added to the class via the mixin
21
20
  #
22
21
  # @!method model_to_document(model)
@@ -51,6 +50,9 @@ module Guacamole
51
50
  # You can use this method for low level communication with the collection.
52
51
  # Details can be found in the Ashikawa::Core documentation.
53
52
  #
53
+ # @note We're well aware that we return a Ashikawa::Core::Collection here
54
+ # but naming it a connection. We think the name `connection` still
55
+ # fits better in this context.
54
56
  # @see http://rubydoc.info/gems/ashikawa-core/Ashikawa/Core/Collection
55
57
  # @return [Ashikawa::Core::Collection]
56
58
  def connection
@@ -96,6 +98,31 @@ module Guacamole
96
98
  mapper.document_to_model connection.fetch(key)
97
99
  end
98
100
 
101
+ # Persist a model in the collection or replace it in the database, depending if it is already persisted
102
+ #
103
+ # * If {Model#persisted? model#persisted?} is `false`, the model will be saved in the collection.
104
+ # Timestamps, revision and key will be set on the model.
105
+ # * If {Model#persisted? model#persisted?} is `true`, it replaces the currently saved version of the model with
106
+ # its new version. It searches for the entry in the database
107
+ # by key. This will change the updated_at timestamp and revision
108
+ # of the provided model.
109
+ #
110
+ # See also {#create create} and {#replace replace} for explicit usage.
111
+ #
112
+ # @param [Model] model The model to be saved
113
+ # @return [Model] The provided model
114
+ # @example Save a podcast to the database
115
+ # podcast = Podcast.new(title: 'Best Show', guest: 'Dirk Breuer')
116
+ # PodcastsCollection.save(podcast)
117
+ # podcast.key #=> '27214247'
118
+ # @example Get a podcast, update its title, replace it
119
+ # podcast = PodcastsCollection.by_key('27214247')
120
+ # podcast.title = 'Even better'
121
+ # PodcastsCollection.save(podcast)
122
+ def save(model)
123
+ model.persisted? ? replace(model) : create(model)
124
+ end
125
+
99
126
  # Persist a model in the collection
100
127
  #
101
128
  # The model will be saved in the collection. Timestamps, revision
@@ -107,7 +134,7 @@ module Guacamole
107
134
  # podcast = Podcast.new(title: 'Best Show', guest: 'Dirk Breuer')
108
135
  # PodcastsCollection.save(podcast)
109
136
  # podcast.key #=> '27214247'
110
- def save(model)
137
+ def create(model)
111
138
  return false unless model.valid?
112
139
 
113
140
  add_timestamps_to_model(model)
@@ -125,10 +152,10 @@ module Guacamole
125
152
  # PodcastsCollection.delete(podcast)
126
153
  def delete(model_or_key)
127
154
  key = if model_or_key.respond_to? :key
128
- model_or_key.key
129
- else
130
- model_or_key
131
- end
155
+ model_or_key.key
156
+ else
157
+ model_or_key
158
+ end
132
159
  fetch_document(key).delete
133
160
  key
134
161
  end
@@ -220,18 +247,64 @@ module Guacamole
220
247
  # Create a document from a model
221
248
  #
222
249
  # @api private
250
+ # @todo Currently we only save the associated models if those never have been
251
+ # persisted. In future versions we should add something like `:autosave`
252
+ # to always save associated models.
223
253
  def create_document_from(model)
254
+ create_referenced_models_of model
255
+
224
256
  document = connection.create_document(model_to_document(model))
225
257
 
226
258
  model.key = document.key
227
259
  model.rev = document.revision
228
260
 
261
+ create_referenced_by_models_of model
262
+
229
263
  document
230
264
  end
231
265
 
266
+ # Creates all not yet persisted referenced models of `model`
267
+ #
268
+ # Referenced models needs to be created before the parent model, because it needs their `key`
269
+ #
270
+ # @api private
271
+ # @todo This method should be considered 'work in progress'. We already know we need to change this.
272
+ # @return [void]
273
+ def create_referenced_models_of(model)
274
+ mapper.referenced_models.each do |ref_model_name|
275
+ ref_collection = mapper.collection_for(ref_model_name)
276
+
277
+ ref_model = model.send(ref_model_name)
278
+ next unless ref_model
279
+
280
+ ref_collection.save ref_model unless ref_model.persisted?
281
+ end
282
+ end
283
+
284
+ # Creates all not yet persisted models which are referenced by `model`
285
+ #
286
+ # Referenced by models needs to created after the parent model, because they need its `key`
287
+ #
288
+ # @api private
289
+ # @todo This method should be considered 'work in progress'. We already know we need to change this.
290
+ # @return [void]
291
+ def create_referenced_by_models_of(model)
292
+ mapper.referenced_by_models.each do |ref_model_name|
293
+ ref_collection = mapper.collection_for(ref_model_name)
294
+
295
+ ref_models = model.send(ref_model_name)
296
+
297
+ ref_models.each do |ref_model|
298
+ ref_model.send("#{model.class.name.demodulize.underscore}=", model)
299
+ ref_collection.save ref_model unless ref_model.persisted?
300
+ end
301
+ end
302
+ end
303
+
232
304
  # Replace a document in the database with this model
233
305
  #
234
306
  # @api private
307
+ # @note This will **not** update associated models (see {#create})
235
308
  def replace_document_from(model)
236
309
  document = model_to_document(model)
237
310
  response = connection.replace(model.key, document)
@@ -240,7 +313,6 @@ module Guacamole
240
313
 
241
314
  document
242
315
  end
243
-
244
316
  end
245
317
  end
246
318
  end
@@ -4,11 +4,11 @@ require 'logger'
4
4
  require 'forwardable'
5
5
  require 'ashikawa-core'
6
6
  require 'active_support/core_ext'
7
+ require 'yaml'
7
8
 
8
9
  require 'guacamole/document_model_mapper'
9
10
 
10
11
  module Guacamole
11
-
12
12
  class << self
13
13
  # Configure Guacamole
14
14
  #
@@ -26,6 +26,13 @@ module Guacamole
26
26
  def configuration
27
27
  @configuration ||= Configuration
28
28
  end
29
+
30
+ # Just an alias to Configuration#logger
31
+ #
32
+ # @return [Configuration#logger]
33
+ def logger
34
+ configuration.logger
35
+ end
29
36
  end
30
37
 
31
38
  # Current configuration
@@ -97,12 +104,16 @@ module Guacamole
97
104
  end
98
105
 
99
106
  def create_database_connection_from(config)
100
- Ashikawa::Core::Database.new do |arango_config|
107
+ database = Ashikawa::Core::Database.new do |arango_config|
101
108
  arango_config.url = db_url_from(config)
102
109
  arango_config.username = config['username']
103
110
  arango_config.password = config['password']
104
111
  arango_config.logger = logger
105
112
  end
113
+
114
+ _add_missing_methods_to_database(database)
115
+
116
+ database
106
117
  end
107
118
 
108
119
  def db_url_from(config)
@@ -118,6 +129,49 @@ module Guacamole
118
129
  default_logger.level = Logger::INFO
119
130
  default_logger
120
131
  end
132
+
133
+ # FIXME: This is not here to stay! Kill it with fire!
134
+ #
135
+ # As soon Ashikawa::Core provides those features
136
+ # (https://github.com/triAGENS/ashikawa-core/issues/83) immediately
137
+ # remove this hack. But while this is ugly as hell it ensures we don't
138
+ # need to change any other related code. Just remove this and we're good.
139
+ def _add_missing_methods_to_database(database)
140
+ database.singleton_class.instance_eval do
141
+ # The raw Faraday connection
142
+ define_method(:raw_connection) do
143
+ @connection.connection
144
+ end
145
+
146
+ # The base URI to the ArangoDB server
147
+ define_method(:arangodb_uri) do |additional_path = ''|
148
+ uri = raw_connection.url_prefix
149
+ base_uri = [uri.scheme, '://', uri.host, ':', uri.port].join
150
+ URI.join(base_uri, additional_path)
151
+ end
152
+
153
+ # Database name query method
154
+ define_method(:name) do
155
+ database_regexp = %r{_db/(?<db_name>\w+)/_api}
156
+ raw_connection.url_prefix.to_s.match(database_regexp)['db_name']
157
+ end
158
+
159
+ # Creates the database
160
+ define_method(:create) do
161
+ raw_connection.post(arangodb_uri('/_api/database'), name: name)
162
+ end
163
+
164
+ # Drops the database
165
+ define_method(:drop) do
166
+ raw_connection.delete(arangodb_uri("/_api/database/#{name}"))
167
+ end
168
+
169
+ # Truncate the database
170
+ define_method(:truncate) do
171
+ collections.each { |collection| collection.truncate! }
172
+ end
173
+ end
174
+ end
121
175
  end
122
176
  end
123
177
  end
@@ -1,11 +1,16 @@
1
1
  # -*- encoding : utf-8 -*-
2
2
 
3
+ require 'guacamole/proxies/referenced_by'
4
+ require 'guacamole/proxies/references'
5
+
3
6
  module Guacamole
4
7
  # This is the default mapper class to map between Ashikawa::Core::Document and
5
8
  # Guacamole::Model instances.
6
9
  #
7
10
  # If you want to build your own mapper, you have to build at least the
8
11
  # `document_to_model` and `model_to_document` methods.
12
+ #
13
+ # @note If you plan to bring your own `DocumentModelMapper` please consider using an {Guacamole::IdentityMap}.
9
14
  class DocumentModelMapper
10
15
  # The class to map to
11
16
  #
@@ -16,6 +21,8 @@ module Guacamole
16
21
  #
17
22
  # @return [Array] An array of embedded models
18
23
  attr_reader :models_to_embed
24
+ attr_reader :referenced_by_models
25
+ attr_reader :referenced_models
19
26
 
20
27
  # Create a new instance of the mapper
21
28
  #
@@ -23,9 +30,42 @@ module Guacamole
23
30
  # The Document class is always Ashikawa::Core::Document
24
31
  #
25
32
  # @param [Class] model_class
26
- def initialize(model_class)
27
- @model_class = model_class
28
- @models_to_embed = []
33
+ def initialize(model_class, identity_map = IdentityMap)
34
+ @model_class = model_class
35
+ @identity_map = identity_map
36
+ @models_to_embed = []
37
+ @referenced_by_models = []
38
+ @referenced_models = []
39
+ end
40
+
41
+ class << self
42
+ # construct the {collection} class for a given model name.
43
+ #
44
+ # @example
45
+ # collection_class = collection_for(:user)
46
+ # collection_class == userscollection # would be true
47
+ #
48
+ # @note This is an class level alias for {DocumentModelMapper#collection_for}
49
+ # @param [symbol, string] model_name the name of the model
50
+ # @return [class] the {collection} class for the given model name
51
+ def collection_for(model_name)
52
+ "#{model_name.to_s.classify.pluralize}Collection".constantize
53
+ end
54
+ end
55
+
56
+ # construct the {collection} class for a given model name.
57
+ #
58
+ # @example
59
+ # collection_class = collection_for(:user)
60
+ # collection_class == userscollection # would be true
61
+ #
62
+ # @todo As of now this is some kind of placeholder method. As soon as we implement
63
+ # the configuration of the mapping (#12) this will change. Still the {DocumentModelMapper}
64
+ # seems to be a good place for this functionality.
65
+ # @param [symbol, string] model_name the name of the model
66
+ # @return [class] the {collection} class for the given model name
67
+ def collection_for(model_name)
68
+ self.class.collection_for model_name
29
69
  end
30
70
 
31
71
  # Map a document to a model
@@ -34,14 +74,26 @@ module Guacamole
34
74
  #
35
75
  # @param [Ashikawa::Core::Document] document
36
76
  # @return [Model] the resulting model with the given Model class
77
+ # rubocop:disable MethodLength
37
78
  def document_to_model(document)
38
- model = model_class.new(document.hash)
79
+ identity_map.retrieve_or_store model_class, document.key do
80
+ model = model_class.new(document.to_h)
81
+
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
39
89
 
40
- model.key = document.key
41
- model.rev = document.revision
90
+ model.key = document.key
91
+ model.rev = document.revision
42
92
 
43
- model
93
+ model
94
+ end
44
95
  end
96
+ # rubocop:enable MethodLength
45
97
 
46
98
  # Map a model to a document
47
99
  #
@@ -49,6 +101,7 @@ module Guacamole
49
101
  #
50
102
  # @param [Model] model
51
103
  # @return [Ashikawa::Core::Document] the resulting document
104
+ # rubocop:disable MethodLength
52
105
  def model_to_document(model)
53
106
  document = model.attributes.dup.except(:key, :rev)
54
107
  models_to_embed.each do |attribute_name|
@@ -56,8 +109,21 @@ module Guacamole
56
109
  embedded_model.attributes.except(:key, :rev)
57
110
  end
58
111
  end
112
+
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
123
+
59
124
  document
60
125
  end
126
+ # rubocop:enable MethodLength
61
127
 
62
128
  # Declare a model to be embedded
63
129
  #
@@ -91,5 +157,19 @@ module Guacamole
91
157
  def embeds(model_name)
92
158
  @models_to_embed << model_name
93
159
  end
160
+
161
+ def referenced_by(model_name)
162
+ @referenced_by_models << model_name
163
+ end
164
+
165
+ def references(model_name)
166
+ @referenced_models << model_name
167
+ end
168
+
169
+ private
170
+
171
+ def identity_map
172
+ @identity_map
173
+ end
94
174
  end
95
175
  end