guacamole 0.3.0 → 0.4.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/.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
|