guacamole 0.0.1 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +1 -0
- data/{config/rubocop.yml → .hound.yml} +1 -12
- data/.ruby-version +1 -1
- data/.travis.yml +2 -0
- data/.yardopts +1 -0
- data/CONTRIBUTING.md +3 -3
- data/Gemfile.devtools +24 -12
- data/Guardfile +1 -1
- data/README.md +347 -50
- data/Rakefile +10 -0
- data/config/reek.yml +18 -5
- data/guacamole.gemspec +5 -2
- data/lib/guacamole.rb +1 -0
- data/lib/guacamole/collection.rb +79 -7
- data/lib/guacamole/configuration.rb +56 -2
- data/lib/guacamole/document_model_mapper.rb +87 -7
- data/lib/guacamole/identity_map.rb +124 -0
- data/lib/guacamole/proxies/proxy.rb +42 -0
- data/lib/guacamole/proxies/referenced_by.rb +15 -0
- data/lib/guacamole/proxies/references.rb +15 -0
- data/lib/guacamole/query.rb +11 -0
- data/lib/guacamole/railtie.rb +6 -1
- data/lib/guacamole/railtie/database.rake +57 -3
- data/lib/guacamole/tasks/database.rake +23 -0
- data/lib/guacamole/version.rb +1 -1
- data/lib/rails/generators/guacamole/collection/collection_generator.rb +19 -0
- data/lib/rails/generators/guacamole/collection/templates/collection.rb.tt +5 -0
- data/lib/rails/generators/guacamole/config/config_generator.rb +25 -0
- data/lib/rails/generators/guacamole/config/templates/guacamole.yml +15 -0
- data/lib/rails/generators/guacamole/model/model_generator.rb +25 -0
- data/lib/rails/generators/guacamole/model/templates/model.rb.tt +11 -0
- data/lib/rails/generators/guacamole_generator.rb +28 -0
- data/lib/rails/generators/rails/collection/collection_generator.rb +13 -0
- data/lib/rails/generators/rspec/collection/collection_generator.rb +13 -0
- data/lib/rails/generators/rspec/collection/templates/collection_spec.rb.tt +7 -0
- data/spec/acceptance/association_spec.rb +40 -0
- data/spec/acceptance/basic_spec.rb +19 -2
- data/spec/acceptance/spec_helper.rb +5 -2
- data/spec/fabricators/author.rb +11 -0
- data/spec/fabricators/author_fabricator.rb +7 -0
- data/spec/fabricators/book.rb +11 -0
- data/spec/fabricators/book_fabricator.rb +5 -0
- data/spec/unit/collection_spec.rb +265 -18
- data/spec/unit/configuration_spec.rb +11 -1
- data/spec/unit/document_model_mapper_spec.rb +127 -5
- data/spec/unit/identiy_map_spec.rb +140 -0
- data/spec/unit/query_spec.rb +37 -16
- data/tasks/adjustments.rake +0 -1
- 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
|
data/lib/guacamole/query.rb
CHANGED
@@ -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
|
data/lib/guacamole/railtie.rb
CHANGED
@@ -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
|
-
|
3
|
-
|
4
|
-
|
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
|
data/lib/guacamole/version.rb
CHANGED
@@ -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,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 -%>
|