yax-fauna 3.0.1
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 +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
|