guacamole 0.3.0 → 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.hound.yml +1 -1
- data/.travis.yml +16 -15
- data/CHANGELOG.md +16 -0
- data/GOALS.md +8 -0
- data/Guardfile +4 -0
- data/README.md +21 -81
- data/guacamole.gemspec +3 -3
- data/lib/guacamole.rb +1 -0
- data/lib/guacamole/aql_query.rb +6 -1
- data/lib/guacamole/collection.rb +34 -66
- data/lib/guacamole/configuration.rb +53 -25
- data/lib/guacamole/document_model_mapper.rb +149 -38
- data/lib/guacamole/edge.rb +74 -0
- data/lib/guacamole/edge_collection.rb +91 -0
- data/lib/guacamole/exceptions.rb +0 -5
- data/lib/guacamole/graph_query.rb +31 -0
- data/lib/guacamole/model.rb +4 -0
- data/lib/guacamole/proxies/proxy.rb +7 -3
- data/lib/guacamole/proxies/relation.rb +22 -0
- data/lib/guacamole/railtie.rb +1 -1
- data/lib/guacamole/transaction.rb +177 -0
- data/lib/guacamole/version.rb +1 -1
- data/shared/transaction.js +66 -0
- data/spec/acceptance/aql_spec.rb +32 -40
- data/spec/acceptance/relations_spec.rb +239 -0
- data/spec/acceptance/spec_helper.rb +2 -2
- data/spec/fabricators/author_fabricator.rb +2 -0
- data/spec/setup/arangodb.sh +2 -2
- data/spec/unit/collection_spec.rb +20 -97
- data/spec/unit/configuration_spec.rb +73 -50
- data/spec/unit/document_model_mapper_spec.rb +84 -77
- data/spec/unit/edge_collection_spec.rb +174 -0
- data/spec/unit/edge_spec.rb +57 -0
- data/spec/unit/proxies/relation_spec.rb +35 -0
- metadata +22 -14
- data/lib/guacamole/proxies/referenced_by.rb +0 -15
- data/lib/guacamole/proxies/references.rb +0 -15
- data/spec/acceptance/association_spec.rb +0 -40
- data/spec/unit/example_spec.rb +0 -8
@@ -71,7 +71,7 @@ module Guacamole
|
|
71
71
|
class Configuration
|
72
72
|
# A wrapper object to handle both configuration from a connection URI and a hash.
|
73
73
|
class ConfigStruct
|
74
|
-
attr_reader :url, :username, :password, :database
|
74
|
+
attr_reader :url, :username, :password, :database, :graph
|
75
75
|
|
76
76
|
def initialize(config_hash_or_url)
|
77
77
|
case config_hash_or_url
|
@@ -98,6 +98,7 @@ module Guacamole
|
|
98
98
|
@username = hash['username']
|
99
99
|
@password = hash['password']
|
100
100
|
@database = hash['database']
|
101
|
+
@graph = hash['graph']
|
101
102
|
@url = "#{hash['protocol']}://#{hash['host']}:#{hash['port']}"
|
102
103
|
end
|
103
104
|
end
|
@@ -105,18 +106,13 @@ module Guacamole
|
|
105
106
|
# @!visibility protected
|
106
107
|
attr_accessor :database, :default_mapper, :logger
|
107
108
|
|
108
|
-
AVAILABLE_EXPERIMENTAL_FEATURES = [
|
109
|
-
:aql_support
|
110
|
-
]
|
111
|
-
|
112
109
|
class << self
|
113
110
|
extend Forwardable
|
114
111
|
|
115
112
|
def_delegators :configuration,
|
116
113
|
:database, :database=,
|
117
114
|
:default_mapper=,
|
118
|
-
:logger
|
119
|
-
:experimental_features=, :experimental_features
|
115
|
+
:logger=
|
120
116
|
|
121
117
|
def default_mapper
|
122
118
|
configuration.default_mapper || (self.default_mapper = Guacamole::DocumentModelMapper)
|
@@ -126,14 +122,61 @@ module Guacamole
|
|
126
122
|
configuration.logger ||= (rails_logger || default_logger)
|
127
123
|
end
|
128
124
|
|
125
|
+
def shared_path
|
126
|
+
Pathname.new(File.join(__dir__, '..', '..', 'shared'))
|
127
|
+
end
|
128
|
+
|
129
|
+
# Returns the graph associated with this Guacamole application.
|
130
|
+
#
|
131
|
+
# You can create more graphs by interacting with the database instance directly. This is just the main graph
|
132
|
+
# that will be used to realize relations between models. The default name will be generated based on some
|
133
|
+
# environment information. To set a custom name use the `graph_name` option when configure your connection.
|
134
|
+
#
|
135
|
+
# @return [Graph] The graph to be used internally to handle relations
|
136
|
+
def graph
|
137
|
+
database.graph(graph_name)
|
138
|
+
end
|
139
|
+
|
140
|
+
# Sets a custom name for the internally used graph
|
141
|
+
#
|
142
|
+
# @param [String] graph_name The name of the graph to be used
|
143
|
+
def graph_name=(graph_name)
|
144
|
+
@graph_name = graph_name
|
145
|
+
end
|
146
|
+
|
147
|
+
# The name of the graph to be used internally.
|
148
|
+
#
|
149
|
+
# Determining the name of the graph will go through the following steps:
|
150
|
+
#
|
151
|
+
# 1. Use the manually set `graph_name`
|
152
|
+
# 2. Use the ENV variable `GUACAMOLE_GRAPH` if present.
|
153
|
+
# This is useful if you configure the application with ENV variables.
|
154
|
+
# 3. If a Rails context was found it will use the name of the application with a `_graph` suffix
|
155
|
+
# 4. If none of the above matched it will use the database name with a `_graph` suffix
|
156
|
+
#
|
157
|
+
# @return [String] The name of the graph to be used
|
158
|
+
def graph_name
|
159
|
+
return @graph_name if @graph_name
|
160
|
+
return ENV['GUACAMOLE_GRAPH'] if ENV['GUACAMOLE_GRAPH']
|
161
|
+
|
162
|
+
base_name = if Module.const_defined?('Rails')
|
163
|
+
Rails.application.class.name.deconstantize.underscore
|
164
|
+
else
|
165
|
+
database.name
|
166
|
+
end
|
167
|
+
|
168
|
+
[base_name, 'graph'].join('_')
|
169
|
+
end
|
170
|
+
|
129
171
|
# Load a YAML configuration file to configure Guacamole
|
130
172
|
#
|
131
173
|
# @param [String] file_name The file name of the configuration
|
132
174
|
def load(file_name)
|
133
|
-
yaml_content
|
134
|
-
config
|
175
|
+
yaml_content = process_file_with_erb(file_name)
|
176
|
+
config = build_config(YAML.load(yaml_content)[current_environment.to_s])
|
177
|
+
self.graph_name = config.graph
|
135
178
|
|
136
|
-
create_database_connection(
|
179
|
+
create_database_connection(config)
|
137
180
|
warn_if_database_was_not_yet_created
|
138
181
|
end
|
139
182
|
|
@@ -212,20 +255,5 @@ module Guacamole
|
|
212
255
|
ERB.new(File.read(file_name)).result
|
213
256
|
end
|
214
257
|
end
|
215
|
-
|
216
|
-
# A list of active experimental features. Refer to `AVAILABLE_EXPERIMENTAL_FEATURES` to see
|
217
|
-
# what can be activated.
|
218
|
-
#
|
219
|
-
# @return [Array<Symbol>] The activated experimental features. Defaults to `[]`
|
220
|
-
def experimental_features
|
221
|
-
@experimental_features || []
|
222
|
-
end
|
223
|
-
|
224
|
-
# Experimental features to activate
|
225
|
-
#
|
226
|
-
# @param [Array<Symbol>] features A list of experimental features to activate
|
227
|
-
def experimental_features=(features)
|
228
|
-
@experimental_features = features
|
229
|
-
end
|
230
258
|
end
|
231
259
|
end
|
@@ -1,7 +1,6 @@
|
|
1
1
|
# -*- encoding : utf-8 -*-
|
2
2
|
|
3
|
-
require 'guacamole/proxies/
|
4
|
-
require 'guacamole/proxies/references'
|
3
|
+
require 'guacamole/proxies/relation'
|
5
4
|
|
6
5
|
module Guacamole
|
7
6
|
# This is the default mapper class to map between Ashikawa::Core::Document and
|
@@ -12,6 +11,80 @@ module Guacamole
|
|
12
11
|
#
|
13
12
|
# @note If you plan to bring your own `DocumentModelMapper` please consider using an {Guacamole::IdentityMap}.
|
14
13
|
class DocumentModelMapper
|
14
|
+
# An attribute to encapsulate special mapping
|
15
|
+
class Attribute
|
16
|
+
# The name of the attribute with in the model
|
17
|
+
#
|
18
|
+
# @return [Symbol] The name of the attribute
|
19
|
+
attr_reader :name
|
20
|
+
|
21
|
+
# Additional options to be used for the mapping
|
22
|
+
#
|
23
|
+
# @return [Hash] The mapping options for the attribute
|
24
|
+
attr_reader :options
|
25
|
+
|
26
|
+
# Create a new attribute instance
|
27
|
+
#
|
28
|
+
# You must at least provide the name of the attribute to be mapped and
|
29
|
+
# optionally pass configuration for the mapper when it processes this attribute.
|
30
|
+
#
|
31
|
+
# @param [Symbol] name The name of the attribute
|
32
|
+
# @param [Hash] options Additional options to be passed
|
33
|
+
# @option options [Edge] :via The Edge class this attribute relates to
|
34
|
+
def initialize(name, options = {})
|
35
|
+
@name = name.to_sym
|
36
|
+
@options = options
|
37
|
+
end
|
38
|
+
|
39
|
+
# The name of the getter for this attribute
|
40
|
+
#
|
41
|
+
# @returns [Symbol] The method name to read this attribute
|
42
|
+
def getter
|
43
|
+
name
|
44
|
+
end
|
45
|
+
|
46
|
+
def get_value(model)
|
47
|
+
value = model.send(getter)
|
48
|
+
|
49
|
+
value.is_a?(Guacamole::Query) ? value.entries : value
|
50
|
+
end
|
51
|
+
|
52
|
+
# The name of the setter for this attribute
|
53
|
+
#
|
54
|
+
# @return [String] The method name to set this attribute
|
55
|
+
def setter
|
56
|
+
"#{name}="
|
57
|
+
end
|
58
|
+
|
59
|
+
# Should this attribute be mapped via an Edge in a Graph?
|
60
|
+
#
|
61
|
+
# @return [Boolean] True if there was an edge class configured
|
62
|
+
def map_via_edge?
|
63
|
+
!!edge_class
|
64
|
+
end
|
65
|
+
|
66
|
+
# The edge class to be used during the mapping process
|
67
|
+
#
|
68
|
+
# @return [Edge] The actual edge class
|
69
|
+
def edge_class
|
70
|
+
options[:via]
|
71
|
+
end
|
72
|
+
|
73
|
+
def inverse?
|
74
|
+
!!options[:inverse]
|
75
|
+
end
|
76
|
+
|
77
|
+
# To Attribute instances are equal if their name is equal
|
78
|
+
#
|
79
|
+
# @param [Attribute] other The Attribute to compare this one to
|
80
|
+
# @return [Boolean] True if both have the same name
|
81
|
+
def ==(other)
|
82
|
+
other.instance_of?(self.class) &&
|
83
|
+
other.name == name
|
84
|
+
end
|
85
|
+
alias_method :eql?, :==
|
86
|
+
end
|
87
|
+
|
15
88
|
# The class to map to
|
16
89
|
#
|
17
90
|
# @return [class] The class to map to
|
@@ -21,8 +94,11 @@ module Guacamole
|
|
21
94
|
#
|
22
95
|
# @return [Array] An array of embedded models
|
23
96
|
attr_reader :models_to_embed
|
24
|
-
|
25
|
-
|
97
|
+
|
98
|
+
# The list of Attributes to treat specially during the mapping process
|
99
|
+
#
|
100
|
+
# @return [Array<Attribute>] The list of special attributes
|
101
|
+
attr_reader :attributes
|
26
102
|
|
27
103
|
# Create a new instance of the mapper
|
28
104
|
#
|
@@ -34,8 +110,7 @@ module Guacamole
|
|
34
110
|
@model_class = model_class
|
35
111
|
@identity_map = identity_map
|
36
112
|
@models_to_embed = []
|
37
|
-
@
|
38
|
-
@referenced_models = []
|
113
|
+
@attributes = []
|
39
114
|
end
|
40
115
|
|
41
116
|
class << self
|
@@ -64,7 +139,7 @@ module Guacamole
|
|
64
139
|
# seems to be a good place for this functionality.
|
65
140
|
# @param [symbol, string] model_name the name of the model
|
66
141
|
# @return [class] the {collection} class for the given model name
|
67
|
-
def collection_for(model_name)
|
142
|
+
def collection_for(model_name = model_class.name)
|
68
143
|
self.class.collection_for model_name
|
69
144
|
end
|
70
145
|
|
@@ -74,26 +149,18 @@ module Guacamole
|
|
74
149
|
#
|
75
150
|
# @param [Ashikawa::Core::Document] document
|
76
151
|
# @return [Model] the resulting model with the given Model class
|
77
|
-
# rubocop:disable MethodLength
|
78
152
|
def document_to_model(document)
|
79
153
|
identity_map.retrieve_or_store model_class, document.key do
|
80
154
|
model = model_class.new(document.to_h)
|
81
155
|
|
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
|
89
|
-
|
90
156
|
model.key = document.key
|
91
157
|
model.rev = document.revision
|
92
158
|
|
159
|
+
handle_related_documents(model)
|
160
|
+
|
93
161
|
model
|
94
162
|
end
|
95
163
|
end
|
96
|
-
# rubocop:enable MethodLength
|
97
164
|
|
98
165
|
# Map a model to a document
|
99
166
|
#
|
@@ -101,29 +168,14 @@ module Guacamole
|
|
101
168
|
#
|
102
169
|
# @param [Model] model
|
103
170
|
# @return [Ashikawa::Core::Document] the resulting document
|
104
|
-
# rubocop:disable MethodLength
|
105
171
|
def model_to_document(model)
|
106
172
|
document = model.attributes.dup.except(:key, :rev)
|
107
|
-
models_to_embed.each do |attribute_name|
|
108
|
-
document[attribute_name] = model.send(attribute_name).map do |embedded_model|
|
109
|
-
embedded_model.attributes.except(:key, :rev)
|
110
|
-
end
|
111
|
-
end
|
112
173
|
|
113
|
-
|
114
|
-
|
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
|
174
|
+
handle_embedded_models(model, document)
|
175
|
+
handle_related_models(document)
|
123
176
|
|
124
177
|
document
|
125
178
|
end
|
126
|
-
# rubocop:enable MethodLength
|
127
179
|
|
128
180
|
# Declare a model to be embedded
|
129
181
|
#
|
@@ -158,12 +210,43 @@ module Guacamole
|
|
158
210
|
@models_to_embed << model_name
|
159
211
|
end
|
160
212
|
|
161
|
-
|
162
|
-
|
213
|
+
# Mark an attribute of the model to be specially treated during mapping
|
214
|
+
#
|
215
|
+
# @param [Symbol] attribute_name The name of the model attribute
|
216
|
+
# @param [Hash] options Additional options to configure the mapping process
|
217
|
+
# @option options [Edge] :via The Edge class this attribute relates to
|
218
|
+
# @example Define a relation via an Edge in a Graph
|
219
|
+
# class Authorship
|
220
|
+
# include Guacamole::Edge
|
221
|
+
#
|
222
|
+
# from :users
|
223
|
+
# to :posts
|
224
|
+
# end
|
225
|
+
#
|
226
|
+
# class BlogpostsCollection
|
227
|
+
# include Guacamole::Collection
|
228
|
+
#
|
229
|
+
# map do
|
230
|
+
# attribute :author, via: Authorship
|
231
|
+
# end
|
232
|
+
# end
|
233
|
+
def attribute(attribute_name, options = {})
|
234
|
+
@attributes << Attribute.new(attribute_name, options)
|
235
|
+
end
|
236
|
+
|
237
|
+
# Returns a list of attributes that have an Edge class configured
|
238
|
+
#
|
239
|
+
# @return [Array<Attribute>] A list of attributes which all have an Edge class
|
240
|
+
def edge_attributes
|
241
|
+
attributes.select(&:map_via_edge?)
|
163
242
|
end
|
164
243
|
|
165
|
-
|
166
|
-
|
244
|
+
# Is this Mapper instance responsible for mapping the given model
|
245
|
+
#
|
246
|
+
# @param [Model] model The model to check against
|
247
|
+
# @return [Boolean] True if the given model is an instance of #model_class. False if not.
|
248
|
+
def responsible_for?(model)
|
249
|
+
model.instance_of?(model_class)
|
167
250
|
end
|
168
251
|
|
169
252
|
private
|
@@ -171,5 +254,33 @@ module Guacamole
|
|
171
254
|
def identity_map
|
172
255
|
@identity_map
|
173
256
|
end
|
257
|
+
|
258
|
+
def handle_embedded_models(model, document)
|
259
|
+
models_to_embed.each do |attribute_name|
|
260
|
+
document[attribute_name] = model.send(attribute_name).map do |embedded_model|
|
261
|
+
embedded_model.attributes.except(:key, :rev)
|
262
|
+
end
|
263
|
+
end
|
264
|
+
end
|
265
|
+
|
266
|
+
def handle_related_models(document)
|
267
|
+
edge_attributes.each do |edge_attribute|
|
268
|
+
document.delete(edge_attribute.name)
|
269
|
+
end
|
270
|
+
end
|
271
|
+
|
272
|
+
def handle_related_documents(model)
|
273
|
+
edge_attributes.each do |edge_attribute|
|
274
|
+
just_one = case model.class.attribute_set[edge_attribute.name].type
|
275
|
+
when Virtus::Attribute::Collection::Type then false
|
276
|
+
else
|
277
|
+
true
|
278
|
+
end
|
279
|
+
|
280
|
+
opts = { just_one: just_one, inverse: edge_attribute.inverse? }
|
281
|
+
|
282
|
+
model.send(edge_attribute.setter, Proxies::Relation.new(model, edge_attribute.edge_class, opts))
|
283
|
+
end
|
284
|
+
end
|
174
285
|
end
|
175
286
|
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
# -*- encoding : utf-8 -*-
|
2
|
+
|
3
|
+
require 'guacamole/model'
|
4
|
+
|
5
|
+
require 'active_support'
|
6
|
+
require 'active_support/concern'
|
7
|
+
|
8
|
+
module Guacamole
|
9
|
+
# An Edge representing a relation between two models within a Graph
|
10
|
+
#
|
11
|
+
# A Guacamole::Edge is specialized model with two predefined attributes (`from` and `to`)
|
12
|
+
# and a class level DSL to define the relation between models inside a Graph. Like normal
|
13
|
+
# models, edge models don't know the database. But unlike the collection classes you define
|
14
|
+
# yourself for your models Guacamole will create a default collection class to be used with
|
15
|
+
# your edge models.
|
16
|
+
#
|
17
|
+
# @!attribute [r] from
|
18
|
+
# The model on the from side of the edge
|
19
|
+
#
|
20
|
+
# @return [Guacamole::Model] The document from which the relation originates
|
21
|
+
#
|
22
|
+
# @!attribute [r] to
|
23
|
+
# The model on the to side of the edge
|
24
|
+
#
|
25
|
+
# @return [Guacamole::Model] The document to which the relation directs
|
26
|
+
#
|
27
|
+
# @!method self.from(collection_name)
|
28
|
+
# Define the collection from which all these edges will originate
|
29
|
+
#
|
30
|
+
# @api public
|
31
|
+
# @param [Symbol] collection_name The name of the originating collection
|
32
|
+
#
|
33
|
+
# @!method self.to(collection_name)
|
34
|
+
# Define the collection to which all these edges will direct
|
35
|
+
#
|
36
|
+
# @api public
|
37
|
+
# @param [Symbol] collection_name The name of the target collection
|
38
|
+
module Edge
|
39
|
+
extend ActiveSupport::Concern
|
40
|
+
|
41
|
+
included do
|
42
|
+
include Guacamole::Model
|
43
|
+
|
44
|
+
attribute :from, Object
|
45
|
+
attribute :to, Object
|
46
|
+
end
|
47
|
+
|
48
|
+
module ClassMethods
|
49
|
+
def from(collection_name = nil)
|
50
|
+
if collection_name.nil?
|
51
|
+
@from
|
52
|
+
else
|
53
|
+
@from = collection_name
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def to(collection_name = nil)
|
58
|
+
if collection_name.nil?
|
59
|
+
@to
|
60
|
+
else
|
61
|
+
@to = collection_name
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def to_collection
|
66
|
+
[to.to_s.camelcase, 'Collection'].join('').constantize
|
67
|
+
end
|
68
|
+
|
69
|
+
def from_collection
|
70
|
+
[from.to_s.camelcase, 'Collection'].join('').constantize
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|