guacamole 0.0.1 → 0.1.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 (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