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