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
@@ -0,0 +1,124 @@
1
+ # -*- encoding : utf-8 -*-
2
+
3
+ require 'hamster/hash'
4
+
5
+ module Guacamole
6
+ # This class implements the 'Identity Map' pattern
7
+ # ({http://www.martinfowler.com/eaaCatalog/identityMap.html Fowler, EAA, 195}) to
8
+ # ensure only one copy of the same database document is present in memory within
9
+ # the same session. Internally a `Hamster::Hash` is used. This hash implementation
10
+ # is immutable and thus it is safe to use the current `IdentityMap`
11
+ # implementation in concurrent situations.
12
+ #
13
+ # The `IdentityMap` should be purged after any unit of work (i.e. a request).
14
+ # If used in Rails the {IdentityMap::Session} middleware will automatically
15
+ # be registered. For other Rack-based use cases you must register it yourself.
16
+ # For all other use cases you need to take when to call {Guacamole::IdentityMap.reset} all
17
+ # by yourself.
18
+ class IdentityMap
19
+ # The `IdentityMap::Session` acts as Rack middleware to reset the {IdentityMap}
20
+ # before each request.
21
+ class Session
22
+ # Create a new instance of the `Session` middleware
23
+ #
24
+ # You must pass an object that responds to `call` in the constructor. This
25
+ # will be the called after the `IdentityMap` has been purged.
26
+ #
27
+ # @param [#call] app Any object that responds to `call`
28
+ def initialize(app)
29
+ @app = app
30
+ end
31
+
32
+ # Run the concrete middleware
33
+ #
34
+ # This satisfies the Rack interface and will be called to reset the `IdentityMap`
35
+ # before each request. In the end the `@app` will be called.
36
+ #
37
+ # @param [Hash] env The environment of the Rack request
38
+ # @return [Array] a Rack compliant response array
39
+ def call(env)
40
+ Guacamole.logger.debug '[SESSION] Resetting the IdentityMap'
41
+ IdentityMap.reset
42
+
43
+ @app.call(env)
44
+ end
45
+ end
46
+
47
+ class << self
48
+ # Purges all stored object from the map by setting the internal structure to `nil`
49
+ #
50
+ # @return [nil]
51
+ def reset
52
+ @identity_map_instance = nil
53
+ end
54
+
55
+ # Add an object to the map
56
+ #
57
+ # The object must implement a `key` method. There is *no* check if that method is present.
58
+ #
59
+ # @param [Object#key] object Any object that implements the `key` method (and has a `class`)
60
+ # @return [Object#key] the object which just has been stored
61
+ def store(object)
62
+ @identity_map_instance = identity_map_instance.put(key_for(object), object)
63
+ object
64
+ end
65
+
66
+ # Retrieves a stored object from the map
67
+ #
68
+ # @param [Class] klass The class of the object you want to get
69
+ # @param [Object] key Whatever is the key of that object
70
+ # @return [Object] the stored object
71
+ def retrieve(klass, key)
72
+ identity_map_instance.get key_for(klass, key)
73
+ end
74
+
75
+ # Retrieves a stored object or adds it based on the block if it is not already present
76
+ #
77
+ # This can be used to retrieve and store in one step. See {Guacamole::DocumentModelMapper#document_to_model}
78
+ # for an example.
79
+ #
80
+ # @param [Class] klass The class of the object you want to get
81
+ # @param [Object] key Whatever is the key of that object
82
+ # @yield A block if the object is not already present
83
+ # @yieldreturn [Object#read] the object to store. The `key` and `class` should match with input params
84
+ # @return [Object] the stored object
85
+ def retrieve_or_store(klass, key, &block)
86
+ return retrieve(klass, key) if include?(klass, key)
87
+
88
+ store block.call
89
+ end
90
+
91
+ # Tests if the map contains some object
92
+ #
93
+ # The method accepts either a `class` and `key` or just any `Object` that responds to
94
+ # `key`. Supporting both is made for your convenience.
95
+ #
96
+ # @param [Object#read, Class] object_or_class Either the object to check or the `class`
97
+ # of the object you're looking for
98
+ # @param [Object, nil] key In case you provided a `Class` as first parameter you must
99
+ # provide the key for the object here
100
+ # @return [true, false] does the map contain the object or not
101
+ def include?(object_or_class, key = nil)
102
+ identity_map_instance.key? key_for(object_or_class, key)
103
+ end
104
+
105
+ # Constructs the key used internally by the map
106
+ #
107
+ # @param [Object#read, Class] object_or_class Either an object or a `Class`
108
+ # @param [Object, nil] key In case you provided a `Class` as first parameter you must
109
+ # provide the key for the object here
110
+ # @return [Array(Class, Object)] the created key
111
+ def key_for(object_or_class, key = nil)
112
+ key ? [object_or_class, key] : [object_or_class.class, object_or_class.key]
113
+ end
114
+
115
+ # The internally used map
116
+ #
117
+ # @api private
118
+ # @return [Hamster::Hash] an instance of an immutable hash
119
+ def identity_map_instance
120
+ @identity_map_instance ||= Hamster.hash
121
+ end
122
+ end
123
+ end
124
+ end
@@ -0,0 +1,42 @@
1
+ # -*- encoding : utf-8 -*-
2
+
3
+ module Guacamole
4
+ module Proxies
5
+ # This is the base class for the association proxies. Proxies are only
6
+ # needed for non-embedded relations between objects. Embedded objects are
7
+ # taken care of by Virtus.
8
+ #
9
+ # The `Proxy` class undefines most methods and passes them to the
10
+ # `@target`. The `@target` will be a lambda which will lazy query the
11
+ # requested objects from the database.
12
+ #
13
+ # Concrete proxy classes are:
14
+ #
15
+ # * {Guacamole::Proxies::ReferencedBy}: This will handle one-to-many associations
16
+ # * {Guacamole::Proxies::References}: This will handle many-to-one associations
17
+ class Proxy < BasicObject
18
+ # Despite using BasicObject we still need to remove some method to get a near transparent proxy
19
+ instance_methods.each do |method|
20
+ undef_method(method) unless method =~ /^__/
21
+ end
22
+
23
+ # Convenience method to setup the proxy. The subclasses need to care of creating
24
+ # the `target` correctly.
25
+ #
26
+ # @param [Object] base The class holding the reference. Currently not used.
27
+ # @param [#call] target The lambda for getting the required objects from the database.
28
+ def init(base, target)
29
+ @base = base
30
+ @target = target
31
+ end
32
+
33
+ def method_missing(meth, *args, &blk)
34
+ @target.call.send meth, *args, &blk
35
+ end
36
+
37
+ def respond_to_missing?(name, include_private = false)
38
+ @target.respond_to?(name, include_private)
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,15 @@
1
+ # -*- encoding : utf-8 -*-
2
+
3
+ require 'guacamole/proxies/proxy'
4
+
5
+ module Guacamole
6
+ module Proxies
7
+ # The {ReferencedBy} proxy is used to represent the 'one' in one-to-many relations.
8
+ class ReferencedBy < Proxy
9
+ def initialize(ref, model)
10
+ init model,
11
+ -> { DocumentModelMapper.collection_for(ref).by_example("#{model.class.name.underscore}_id" => model.key) }
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,15 @@
1
+ # -*- encoding : utf-8 -*-
2
+
3
+ require 'guacamole/proxies/proxy'
4
+
5
+ module Guacamole
6
+ module Proxies
7
+ # The {References} proxy is used to represent the 'many' in one-to-many relations.
8
+ class References < Proxy
9
+ def initialize(ref, document)
10
+ init nil,
11
+ -> { DocumentModelMapper.collection_for(ref).by_key(document["#{ref}_id"]) }
12
+ end
13
+ end
14
+ end
15
+ end
@@ -68,5 +68,16 @@ module Guacamole
68
68
  options[:skip] = skip
69
69
  self
70
70
  end
71
+
72
+ # Is this {Query} equal to another {Query}
73
+ #
74
+ # Two {Query} objects are equal if their examples are equal
75
+ #
76
+ # @param [Query] other The query to compare to
77
+ def ==(other)
78
+ other.instance_of?(self.class) &&
79
+ example == other.example
80
+ end
81
+ alias_method :eql?, :==
71
82
  end
72
83
  end
@@ -9,13 +9,15 @@ module Guacamole
9
9
  # Class to hook into Rails configuration and initializer
10
10
  # @api private
11
11
  class Railtie < Rails::Railtie
12
-
13
12
  rake_tasks do
14
13
  load 'guacamole/railtie/database.rake'
15
14
  end
16
15
 
17
16
  config.guacamole = ::Guacamole::Configuration
18
17
 
18
+ # We're not doing migrations (yet)
19
+ config.send(:app_generators).orm :guacamole, migration: false
20
+
19
21
  initializer 'guacamole.load-config' do
20
22
  config_file = Rails.root.join('config', 'guacamole.yml')
21
23
  if config_file.file?
@@ -23,5 +25,8 @@ module Guacamole
23
25
  end
24
26
  end
25
27
 
28
+ initializer 'guacamole.append-identity-map-middleware' do |app|
29
+ app.middleware.use Guacamole::IdentityMap::Session
30
+ end
26
31
  end
27
32
  end
@@ -1,5 +1,59 @@
1
+ # -*- encoding : utf-8 -*-
2
+
3
+ load 'guacamole/tasks/database.rake'
4
+
1
5
  namespace :db do
2
- # TODO: Add all the Rails-tasks.
3
- # If we cannot do something meaningful provide a noop.
4
- # Foremost the creation of databases should be provided.
6
+ unless Rake::Task.task_defined?("db:drop")
7
+ desc "Drops all the collections of the database for the current Rails.env"
8
+ task :drop => "guacamole:drop"
9
+ end
10
+
11
+ unless Rake::Task.task_defined?("db:purge")
12
+ desc "Purges all the collections of the database for the current Rails.env"
13
+ task :purge => "guacamole:purge"
14
+ end
15
+
16
+ unless Rake::Task.task_defined?("db:seed")
17
+ desc "Load the seed data from db/seeds.rb"
18
+ task :seed => :environment do
19
+ seed_file = File.join(Rails.root, "db", "seeds.rb")
20
+ load(seed_file) if File.exist?(seed_file)
21
+ end
22
+ end
23
+
24
+ unless Rake::Task.task_defined?("db:setup")
25
+ desc "Create the database, and initialize with the seed data"
26
+ task :setup => [ "db:create", "db:seed" ]
27
+ end
28
+
29
+ unless Rake::Task.task_defined?("db:reset")
30
+ desc "Delete data and loads the seeds"
31
+ task :reset => [ "db:drop", "db:seed" ]
32
+ end
33
+
34
+ unless Rake::Task.task_defined?("db:create")
35
+ desc "Create the database"
36
+ task :create => "guacamole:create"
37
+ end
38
+
39
+ unless Rake::Task.task_defined?("db:migrate")
40
+ desc "Run the migrations for the current Rails.env (not yet implemented)"
41
+ task :migrate => :environment do
42
+ raise "ArangoDB doesn't support migrations. If you need to migrate your data you must roll your own solution."
43
+ end
44
+ end
45
+
46
+ unless Rake::Task.task_defined?("db:schema:load")
47
+ namespace :schema do
48
+ task :load do
49
+ raise "ArangoDB is schema-less, thus we cannot load any schema."
50
+ end
51
+ end
52
+ end
53
+
54
+ unless Rake::Task.task_defined?("db:test:prepare")
55
+ namespace :test do
56
+ task :prepare => "guacamole:purge"
57
+ end
58
+ end
5
59
  end
@@ -0,0 +1,23 @@
1
+ # -*- encoding : utf-8 -*-
2
+
3
+ namespace :db do
4
+ namespace :guacamole do
5
+ desc "Purges all the collections of the database for the current environment"
6
+ task :purge => :environment do
7
+ puts "[ARANGODB] Purging all data from database '#{Guacamole.configuration.database.name}' ..."
8
+ Guacamole.configuration.database.truncate
9
+ end
10
+
11
+ desc "Drops all the collections of the database for the current environment"
12
+ task :drop => :environment do
13
+ puts "[ARANGODB] Dropping the database '#{Guacamole.configuration.database.name}' ..."
14
+ Guacamole.configuration.database.drop
15
+ end
16
+
17
+ desc "Create the database for the current environment"
18
+ task :create => :environment do
19
+ puts "[ARANGODB] Creating the database '#{Guacamole.configuration.database.name}' ..."
20
+ Guacamole.configuration.database.create
21
+ end
22
+ end
23
+ end
@@ -1,5 +1,5 @@
1
1
  # -*- encoding : utf-8 -*-
2
2
  module Guacamole
3
3
  # Current version of the gem
4
- VERSION = '0.0.1'
4
+ VERSION = '0.1.0'
5
5
  end
@@ -0,0 +1,19 @@
1
+ # -*- encoding : utf-8 -*-
2
+
3
+ require 'rails/generators/guacamole_generator'
4
+
5
+ module Guacamole
6
+ module Generators
7
+ class CollectionGenerator < Base
8
+ desc 'Creates a Guacamole collection'
9
+
10
+ check_class_collision suffix: 'Collection'
11
+
12
+ def create_collection_file
13
+ template 'collection.rb.tt', File.join('app/collections', class_path, "#{file_name}_collection.rb")
14
+ end
15
+
16
+ hook_for :test_framework
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,5 @@
1
+ <% module_namespacing do -%>
2
+ class <%= class_name %>Collection
3
+ include Guacamole::Collection
4
+ end
5
+ <% end -%>
@@ -0,0 +1,25 @@
1
+ # -*- encoding : utf-8 -*-
2
+
3
+ require 'rails/generators/guacamole_generator'
4
+
5
+ module Guacamole
6
+ module Generators
7
+ class ConfigGenerator < Rails::Generators::Base
8
+ desc 'Creates a Guacamole configuration file at config/guacamole.yml'
9
+
10
+ argument :database_name, type: :string, optional: true
11
+
12
+ def self.source_root
13
+ @_guacamole_source_root ||= File.expand_path('../templates', __FILE__)
14
+ end
15
+
16
+ def app_name
17
+ Rails.application.class.parent.to_s.underscore
18
+ end
19
+
20
+ def create_config_file
21
+ template 'guacamole.yml', File.join('config', 'guacamole.yml')
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,15 @@
1
+ development:
2
+ protocol: 'http'
3
+ host: 'localhost'
4
+ port: 8529
5
+ password: ''
6
+ username: ''
7
+ database: '<%= database_name || app_name %>_development'
8
+
9
+ test:
10
+ protocol: 'http'
11
+ host: 'localhost'
12
+ port: 8529
13
+ password: ''
14
+ username: ''
15
+ database: '<%= database_name || app_name %>_test'
@@ -0,0 +1,25 @@
1
+ # -*- encoding : utf-8 -*-
2
+
3
+ require 'rails/generators/guacamole_generator'
4
+
5
+ module Guacamole
6
+ module Generators
7
+ class ModelGenerator < Base
8
+ desc 'Creates a Guacamole model'
9
+ argument :attributes, type: :array, default: [], banner: 'field:type field:type'
10
+
11
+ check_class_collision
12
+
13
+ class_option :parent, type: :string, desc: 'The parent class for the generated model'
14
+
15
+ def create_model_file
16
+ template 'model.rb.tt', File.join('app/models', class_path, "#{file_name}.rb")
17
+ end
18
+
19
+ hook_for :test_framework
20
+ hook_for :collection, aliases: '-c', type: :boolean, default: true do |instance, collection|
21
+ instance.invoke collection, [instance.name.pluralize]
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,11 @@
1
+ <% module_namespacing do -%>
2
+ class <%= class_name %><%= " < #{options[:parent].classify}" if options[:parent] %>
3
+ <% unless options[:parent] -%>
4
+ include Guacamole::Model
5
+ <% end -%>
6
+
7
+ <% attributes.reject{|attr| attr.reference?}.each do |attribute| -%>
8
+ attribute :<%= attribute.name %>, <%= attribute.type_class %>
9
+ <% end -%>
10
+ end
11
+ <% end -%>