yax-fauna 3.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/CHANGELOG +51 -0
- data/Gemfile +6 -0
- data/LICENSE +12 -0
- data/README.md +148 -0
- data/Rakefile +13 -0
- data/fauna.gemspec +26 -0
- data/lib/fauna.rb +25 -0
- data/lib/fauna/client.rb +253 -0
- data/lib/fauna/client_logger.rb +52 -0
- data/lib/fauna/context.rb +81 -0
- data/lib/fauna/deprecate.rb +29 -0
- data/lib/fauna/errors.rb +235 -0
- data/lib/fauna/json.rb +99 -0
- data/lib/fauna/objects.rb +147 -0
- data/lib/fauna/page.rb +374 -0
- data/lib/fauna/query.rb +899 -0
- data/lib/fauna/request_result.rb +58 -0
- data/lib/fauna/util.rb +50 -0
- data/lib/fauna/version.rb +4 -0
- data/spec/bytes_spec.rb +36 -0
- data/spec/client_logger_spec.rb +73 -0
- data/spec/client_spec.rb +127 -0
- data/spec/context_spec.rb +84 -0
- data/spec/errors_spec.rb +185 -0
- data/spec/fauna_helper.rb +102 -0
- data/spec/json_spec.rb +161 -0
- data/spec/page_spec.rb +357 -0
- data/spec/query_spec.rb +1104 -0
- data/spec/queryv_spec.rb +25 -0
- data/spec/ref_spec.rb +99 -0
- data/spec/setref_spec.rb +23 -0
- data/spec/spec_helper.rb +27 -0
- data/spec/util_spec.rb +19 -0
- metadata +181 -0
@@ -0,0 +1,52 @@
|
|
1
|
+
module Fauna
|
2
|
+
# Example observer that can be used for debugging
|
3
|
+
module ClientLogger
|
4
|
+
##
|
5
|
+
# Lambda that can be the +observer+ for a Client.
|
6
|
+
# Will call the passed block on a string representation of each RequestResult.
|
7
|
+
#
|
8
|
+
# Example:
|
9
|
+
#
|
10
|
+
# logger = ClientLogger.logger do |str|
|
11
|
+
# puts str
|
12
|
+
# end
|
13
|
+
# Client.new observer: logger, ...
|
14
|
+
def self.logger
|
15
|
+
lambda do |request_result|
|
16
|
+
yield show_request_result(request_result)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
# Translates a RequestResult to a string suitable for logging.
|
21
|
+
def self.show_request_result(request_result)
|
22
|
+
rr = request_result
|
23
|
+
logged = ''
|
24
|
+
|
25
|
+
logged << "Fauna #{rr.method.to_s.upcase} /#{rr.path}#{query_string_for_logging(rr.query)}\n"
|
26
|
+
logged << " Credentials: #{rr.auth}\n"
|
27
|
+
if rr.request_content
|
28
|
+
logged << " Request JSON: #{indent(FaunaJson.to_json_pretty(rr.request_content))}\n"
|
29
|
+
end
|
30
|
+
logged << " Response headers: #{indent(FaunaJson.to_json_pretty(rr.response_headers))}\n"
|
31
|
+
logged << " Response JSON: #{indent(FaunaJson.to_json_pretty(rr.response_content))}\n"
|
32
|
+
logged << " Response (#{rr.status_code}): Network latency #{(rr.time_taken * 1000).to_i}ms"
|
33
|
+
|
34
|
+
logged
|
35
|
+
end
|
36
|
+
|
37
|
+
def self.indent(str) # :nodoc:
|
38
|
+
indent_str = ' '
|
39
|
+
str.split("\n").join("\n" + indent_str)
|
40
|
+
end
|
41
|
+
|
42
|
+
def self.query_string_for_logging(query) # :nodoc:
|
43
|
+
return unless query && !query.empty?
|
44
|
+
|
45
|
+
'?' + query.collect do |k, v|
|
46
|
+
"#{k}=#{v}"
|
47
|
+
end.join('&')
|
48
|
+
end
|
49
|
+
|
50
|
+
private_class_method :indent, :query_string_for_logging
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
module Fauna
|
2
|
+
##
|
3
|
+
# Error raised when the context is used without a client being set.
|
4
|
+
class NoContextError < RuntimeError; end
|
5
|
+
|
6
|
+
##
|
7
|
+
# The client context wrapper.
|
8
|
+
#
|
9
|
+
# Used for accessing the client without directly passing around the client instance.
|
10
|
+
# Context is scoped to the current thread.
|
11
|
+
class Context
|
12
|
+
##
|
13
|
+
# Returns a context block with the given client.
|
14
|
+
#
|
15
|
+
# +client+:: Client to use for the context block.
|
16
|
+
def self.block(client)
|
17
|
+
push(client)
|
18
|
+
yield
|
19
|
+
ensure
|
20
|
+
pop
|
21
|
+
end
|
22
|
+
|
23
|
+
##
|
24
|
+
# Adds a client to the current context.
|
25
|
+
#
|
26
|
+
# +client+:: Client to add to the current context.
|
27
|
+
def self.push(client)
|
28
|
+
stack.push(client)
|
29
|
+
end
|
30
|
+
|
31
|
+
##
|
32
|
+
# Removes the last client context from the stack and returns it.
|
33
|
+
def self.pop
|
34
|
+
stack.pop
|
35
|
+
end
|
36
|
+
|
37
|
+
##
|
38
|
+
# Resets the current client context, removing all the clients from the stack.
|
39
|
+
def self.reset
|
40
|
+
stack.clear
|
41
|
+
end
|
42
|
+
|
43
|
+
##
|
44
|
+
# Issues a query to FaunaDB with the current client context.
|
45
|
+
#
|
46
|
+
# Queries are built via the Query helpers. See {FaunaDB Query API}[https://fauna.com/documentation/queries]
|
47
|
+
# for information on constructing queries.
|
48
|
+
#
|
49
|
+
# +expression+:: A query expression
|
50
|
+
#
|
51
|
+
# :category: Client Methods
|
52
|
+
def self.query(expression = nil, &expr_block)
|
53
|
+
client.query(expression, &expr_block)
|
54
|
+
end
|
55
|
+
|
56
|
+
##
|
57
|
+
# Creates a Fauna::Page for paging/iterating over a set with the current client context.
|
58
|
+
#
|
59
|
+
# +set+:: A set query to paginate over.
|
60
|
+
# +params+:: A list of parameters to pass to {paginate}[https://fauna.com/documentation/queries#read_functions-paginate_set].
|
61
|
+
# +fauna_map+:: Optional block to wrap the generated paginate query with. The block will be run in a query context.
|
62
|
+
# The paginate query will be passed into the block as an argument.
|
63
|
+
def self.paginate(set, params = {}, &fauna_map)
|
64
|
+
client.paginate(set, params, &fauna_map)
|
65
|
+
end
|
66
|
+
|
67
|
+
##
|
68
|
+
# Returns the current context's client, or if there is none, raises NoContextError.
|
69
|
+
def self.client
|
70
|
+
stack.last || fail(NoContextError, 'You must be within a Fauna::Context.block to perform operations.')
|
71
|
+
end
|
72
|
+
|
73
|
+
class << self
|
74
|
+
private
|
75
|
+
|
76
|
+
def stack
|
77
|
+
Thread.current[:fauna_context_stack] ||= []
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module Fauna
|
2
|
+
module Deprecate
|
3
|
+
##
|
4
|
+
# Deprecates a method
|
5
|
+
#
|
6
|
+
# class AClass
|
7
|
+
# extend Fauna::Deprecate
|
8
|
+
#
|
9
|
+
# def method
|
10
|
+
# end
|
11
|
+
#
|
12
|
+
# deprecate :method, :new_method
|
13
|
+
#
|
14
|
+
# def new_method
|
15
|
+
# end
|
16
|
+
# end
|
17
|
+
#
|
18
|
+
# +name+:: The method name to be deprecated
|
19
|
+
# +replacement+:: The new method that should be used instead
|
20
|
+
def deprecate(name, replacement)
|
21
|
+
old_name = "deprecated_#{name}"
|
22
|
+
alias_method old_name, name
|
23
|
+
define_method name do |*args, &block|
|
24
|
+
warn "Method #{name} called from #{Gem.location_of_caller.join(':')} is deprecated. Use #{replacement} instead"
|
25
|
+
self.__send__ old_name, *args, &block
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
data/lib/fauna/errors.rb
ADDED
@@ -0,0 +1,235 @@
|
|
1
|
+
module Fauna
|
2
|
+
##
|
3
|
+
# Error for when an object passed into a query cannot be serialized.
|
4
|
+
# Objects that are not native to the query language should implement +to_h+/+to_hash+.
|
5
|
+
class SerializationError < RuntimeError
|
6
|
+
def initialize(obj) # :nodoc:
|
7
|
+
super("Object #{obj.inspect} is not serializable")
|
8
|
+
@object = obj
|
9
|
+
end
|
10
|
+
|
11
|
+
# The object that could not be serialized.
|
12
|
+
attr_reader :object
|
13
|
+
end
|
14
|
+
|
15
|
+
##
|
16
|
+
# Error for when the server returns an unexpected kind of response.
|
17
|
+
class UnexpectedError < RuntimeError
|
18
|
+
# RequestResult for the request that caused this error.
|
19
|
+
attr_reader :request_result
|
20
|
+
|
21
|
+
def initialize(description, request_result) # :nodoc:
|
22
|
+
super(description)
|
23
|
+
@request_result = request_result
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.get_or_raise(request_result, hash, key) # :nodoc:
|
27
|
+
unless hash.is_a? Hash and hash.key? key
|
28
|
+
fail UnexpectedError.new("Response JSON does not contain expected key #{key}", request_result)
|
29
|
+
end
|
30
|
+
hash[key]
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
##
|
35
|
+
# Error returned by the FaunaDB server.
|
36
|
+
# For documentation of error types, see the docs[https://fauna.com/documentation#errors].
|
37
|
+
class FaunaError < RuntimeError
|
38
|
+
# List of ErrorData objects returned by the server.
|
39
|
+
attr_reader :errors
|
40
|
+
|
41
|
+
# RequestResult for the request that caused this error.
|
42
|
+
attr_reader :request_result
|
43
|
+
|
44
|
+
##
|
45
|
+
# Raises the associated error from a RequestResult based on the status code.
|
46
|
+
#
|
47
|
+
# Returns +nil+ for 2xx status codes
|
48
|
+
def self.raise_for_status_code(request_result)
|
49
|
+
case request_result.status_code
|
50
|
+
when 200..299
|
51
|
+
|
52
|
+
when 400
|
53
|
+
fail BadRequest.new(request_result)
|
54
|
+
when 401
|
55
|
+
fail Unauthorized.new(request_result)
|
56
|
+
when 403
|
57
|
+
fail PermissionDenied.new(request_result)
|
58
|
+
when 404
|
59
|
+
fail NotFound.new(request_result)
|
60
|
+
when 405
|
61
|
+
fail MethodNotAllowed.new(request_result)
|
62
|
+
when 500
|
63
|
+
fail InternalError.new(request_result)
|
64
|
+
when 502
|
65
|
+
fail UnavailableError.new(request_result)
|
66
|
+
when 503
|
67
|
+
fail UnavailableError.new(request_result)
|
68
|
+
when 504
|
69
|
+
fail UnavailableError.new(request_result)
|
70
|
+
else
|
71
|
+
fail UnexpectedError.new('Unexpected status code.', request_result)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
# Creates a new error from a given RequestResult or Exception.
|
76
|
+
def initialize(request_result)
|
77
|
+
message = nil
|
78
|
+
|
79
|
+
if request_result.is_a? RequestResult
|
80
|
+
@request_result = request_result
|
81
|
+
|
82
|
+
begin
|
83
|
+
if request_result.response_content.nil?
|
84
|
+
fail UnexpectedError.new('Invalid JSON.', request_result)
|
85
|
+
end
|
86
|
+
|
87
|
+
errors_raw = UnexpectedError.get_or_raise request_result, request_result.response_content, :errors
|
88
|
+
@errors = catch :invalid_response do
|
89
|
+
throw :invalid_response unless errors_raw.is_a? Array
|
90
|
+
errors_raw.map { |error| ErrorData.from_hash(error) }
|
91
|
+
end
|
92
|
+
|
93
|
+
if @errors.nil?
|
94
|
+
fail UnexpectedError.new('Error data has an unexpected format.', request_result)
|
95
|
+
elsif @errors.empty?
|
96
|
+
fail UnexpectedError.new('Error data returned was blank.', request_result)
|
97
|
+
end
|
98
|
+
|
99
|
+
message = @errors.map do |error|
|
100
|
+
msg = 'Error'
|
101
|
+
msg += " at #{error.position}" unless error.position.nil?
|
102
|
+
msg += ": #{error.code} - #{error.description}"
|
103
|
+
|
104
|
+
unless error.failures.nil?
|
105
|
+
msg += ' (' + error.failures.map do |failure|
|
106
|
+
"Failure at #{failure.field}: #{failure.code} - #{failure.description}"
|
107
|
+
end.join(' ') + ')'
|
108
|
+
end
|
109
|
+
|
110
|
+
msg
|
111
|
+
end.join(' ')
|
112
|
+
rescue UnexpectedError => e
|
113
|
+
unless self.is_a?(UnavailableError) && [502, 503, 504].include?(request_result.status_code)
|
114
|
+
raise e
|
115
|
+
end
|
116
|
+
|
117
|
+
message = request_result.response_raw
|
118
|
+
end
|
119
|
+
elsif request_result.is_a? Exception
|
120
|
+
message = request_result.class.name
|
121
|
+
unless request_result.message.nil?
|
122
|
+
message += ": #{request_result.message}"
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
super(message)
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
# An exception thrown if FaunaDB cannot evaluate a query.
|
131
|
+
class BadRequest < FaunaError; end
|
132
|
+
|
133
|
+
# An exception thrown if FaunaDB responds with an HTTP 401.
|
134
|
+
class Unauthorized < FaunaError; end
|
135
|
+
|
136
|
+
# An exception thrown if FaunaDB responds with an HTTP 403.
|
137
|
+
class PermissionDenied < FaunaError; end
|
138
|
+
|
139
|
+
# An exception thrown if FaunaDB responds with an HTTP 404 for non-query endpoints.
|
140
|
+
class NotFound < FaunaError; end
|
141
|
+
|
142
|
+
# An exception thrown if FaunaDB responds with an HTTP 405.
|
143
|
+
class MethodNotAllowed < FaunaError; end
|
144
|
+
|
145
|
+
##
|
146
|
+
# An exception thrown if FaunaDB responds with an HTTP 500. Such errors represent an internal
|
147
|
+
# failure within the database.
|
148
|
+
class InternalError < FaunaError; end
|
149
|
+
|
150
|
+
# An exception thrown if FaunaDB responds with an HTTP 502, 503, or 504.
|
151
|
+
class UnavailableError < FaunaError; end
|
152
|
+
|
153
|
+
# Data for one error returned by the server.
|
154
|
+
class ErrorData
|
155
|
+
##
|
156
|
+
# Error code.
|
157
|
+
#
|
158
|
+
# Reference: {FaunaDB Error codes}[https://fauna.com/documentation#errors]
|
159
|
+
attr_reader :code
|
160
|
+
# Error description.
|
161
|
+
attr_reader :description
|
162
|
+
# Position of the error in a query. May be +nil+.
|
163
|
+
attr_reader :position
|
164
|
+
# List of Failure objects returned by the server. +nil+ except for <code>validation failed</code> errors.
|
165
|
+
attr_reader :failures
|
166
|
+
|
167
|
+
def self.from_hash(hash) # :nodoc:
|
168
|
+
code = ErrorHelpers.get_or_throw hash, :code
|
169
|
+
description = ErrorHelpers.get_or_throw hash, :description
|
170
|
+
position = ErrorHelpers.map_position hash[:position]
|
171
|
+
failures = hash[:failures].map(&Failure.method(:from_hash)) unless hash[:failures].nil?
|
172
|
+
ErrorData.new code, description, position, failures
|
173
|
+
end
|
174
|
+
|
175
|
+
def initialize(code, description, position, failures) # :nodoc:
|
176
|
+
@code = code
|
177
|
+
@description = description
|
178
|
+
@position = position
|
179
|
+
@failures = failures
|
180
|
+
end
|
181
|
+
|
182
|
+
def inspect # :nodoc:
|
183
|
+
"ErrorData(#{code.inspect}, #{description.inspect}, #{position.inspect}, #{failures.inspect})"
|
184
|
+
end
|
185
|
+
end
|
186
|
+
|
187
|
+
##
|
188
|
+
# Part of ErrorData.
|
189
|
+
# For more information, see the {docs}[https://fauna.com/documentation#errors-invalid_data].
|
190
|
+
class Failure
|
191
|
+
# Failure code.
|
192
|
+
attr_reader :code
|
193
|
+
# Failure description.
|
194
|
+
attr_reader :description
|
195
|
+
# Field of the failure in the instance.
|
196
|
+
attr_reader :field
|
197
|
+
|
198
|
+
def self.from_hash(hash) # :nodoc:
|
199
|
+
Failure.new(
|
200
|
+
ErrorHelpers.get_or_throw(hash, :code),
|
201
|
+
ErrorHelpers.get_or_throw(hash, :description),
|
202
|
+
ErrorHelpers.map_position(hash[:field]),
|
203
|
+
)
|
204
|
+
end
|
205
|
+
|
206
|
+
def initialize(code, description, field) # :nodoc:
|
207
|
+
@code = code
|
208
|
+
@description = description
|
209
|
+
@field = field
|
210
|
+
end
|
211
|
+
|
212
|
+
def inspect # :nodoc:
|
213
|
+
"Failure(#{code.inspect}, #{description.inspect}, #{field.inspect})"
|
214
|
+
end
|
215
|
+
end
|
216
|
+
|
217
|
+
module ErrorHelpers # :nodoc:
|
218
|
+
def self.map_position(position)
|
219
|
+
unless position.nil?
|
220
|
+
position.map do |part|
|
221
|
+
if part.is_a? String
|
222
|
+
part.to_sym
|
223
|
+
else
|
224
|
+
part
|
225
|
+
end
|
226
|
+
end
|
227
|
+
end
|
228
|
+
end
|
229
|
+
|
230
|
+
def self.get_or_throw(hash, key)
|
231
|
+
throw :invalid_response unless hash.is_a? Hash and hash.key? key
|
232
|
+
hash[key]
|
233
|
+
end
|
234
|
+
end
|
235
|
+
end
|
data/lib/fauna/json.rb
ADDED
@@ -0,0 +1,99 @@
|
|
1
|
+
module Fauna
|
2
|
+
module FaunaJson # :nodoc:
|
3
|
+
@@serializable_types = [String, Numeric, TrueClass, FalseClass, NilClass, Hash, Array, Symbol, Time, Date, Fauna::Ref, Fauna::SetRef, Fauna::Bytes, Fauna::QueryV, Fauna::Query::Expr]
|
4
|
+
|
5
|
+
def self.serializable_types
|
6
|
+
@@serializable_types
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.to_json(value)
|
10
|
+
serialize(value).to_json
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.to_json_pretty(value)
|
14
|
+
JSON.pretty_generate serialize(value)
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.deserialize(obj)
|
18
|
+
if obj.is_a?(Hash)
|
19
|
+
if obj.key? :@ref
|
20
|
+
ref = obj[:@ref]
|
21
|
+
id = ref[:id]
|
22
|
+
|
23
|
+
if !ref.key?(:class) && !ref.key?(:database)
|
24
|
+
Native.from_name(id)
|
25
|
+
else
|
26
|
+
cls = self.deserialize(ref[:class])
|
27
|
+
db = self.deserialize(ref[:database])
|
28
|
+
Ref.new(id, cls, db)
|
29
|
+
end
|
30
|
+
elsif obj.key? :@set
|
31
|
+
SetRef.new deserialize(obj[:@set])
|
32
|
+
elsif obj.key? :@obj
|
33
|
+
deserialize(obj[:@obj])
|
34
|
+
elsif obj.key? :@ts
|
35
|
+
Time.iso8601 obj[:@ts]
|
36
|
+
elsif obj.key? :@date
|
37
|
+
Date.iso8601 obj[:@date]
|
38
|
+
elsif obj.key? :@bytes
|
39
|
+
Bytes.from_base64 obj[:@bytes]
|
40
|
+
elsif obj.key? :@query
|
41
|
+
QueryV.new deserialize(obj[:@query])
|
42
|
+
else
|
43
|
+
Hash[obj.collect { |k, v| [k, deserialize(v)] }]
|
44
|
+
end
|
45
|
+
elsif obj.is_a?(Array)
|
46
|
+
obj.collect { |val| deserialize(val) }
|
47
|
+
else
|
48
|
+
obj
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def self.json_load(body)
|
53
|
+
JSON.load body, nil, max_nesting: false, symbolize_names: true, create_additions: false
|
54
|
+
end
|
55
|
+
|
56
|
+
def self.json_load_or_nil(body)
|
57
|
+
json_load body
|
58
|
+
rescue JSON::ParserError
|
59
|
+
nil
|
60
|
+
end
|
61
|
+
|
62
|
+
def self.serialize(value)
|
63
|
+
# Handle primitives
|
64
|
+
if [String, Numeric, TrueClass, FalseClass, NilClass].any? { |type| value.is_a? type }
|
65
|
+
value
|
66
|
+
elsif value.is_a? Hash
|
67
|
+
Hash[value.collect { |k, v| [k, serialize(v)] }]
|
68
|
+
elsif value.is_a? Array
|
69
|
+
value.collect { |val| serialize(val) }
|
70
|
+
elsif value.is_a? Symbol
|
71
|
+
value.to_s
|
72
|
+
# Natively supported types
|
73
|
+
elsif value.is_a? Time
|
74
|
+
# 9 means: include nanoseconds in encoding
|
75
|
+
{ :@ts => value.iso8601(9) }
|
76
|
+
elsif value.is_a? Date
|
77
|
+
{ :@date => value.iso8601 }
|
78
|
+
# Fauna native types
|
79
|
+
elsif value.is_a? Ref
|
80
|
+
ref = { id: value.id }
|
81
|
+
ref[:class] = value.class_ unless value.class_.nil?
|
82
|
+
ref[:database] = value.database unless value.database.nil?
|
83
|
+
{ :@ref => serialize(ref) }
|
84
|
+
elsif value.is_a? SetRef
|
85
|
+
{ :@set => serialize(value.value) }
|
86
|
+
elsif value.is_a? Bytes
|
87
|
+
{ :@bytes => value.to_base64 }
|
88
|
+
elsif value.is_a? QueryV
|
89
|
+
{ :@query => serialize(value.value) }
|
90
|
+
# Query expression wrapper
|
91
|
+
elsif value.is_a? Query::Expr
|
92
|
+
serialize(value.raw)
|
93
|
+
# Everything else is rejected
|
94
|
+
else
|
95
|
+
fail SerializationError.new(value)
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|