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
@@ -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 -%>