fauna 1.3.4 → 2.0.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.
@@ -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,135 @@
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
+ # Performs a +GET+ request for a REST endpoint within the current client context.
45
+ #
46
+ # +path+:: Path to +GET+.
47
+ # +query+:: Query parameters to append to the path.
48
+ #
49
+ # Reference: {FaunaDB REST API}[https://faunadb.com/documentation/rest]
50
+ #
51
+ # :category: Client Methods
52
+ def self.get(path, query = {})
53
+ client.get(path, query)
54
+ end
55
+
56
+ ##
57
+ # Performs a +POST+ request for a REST endpoint within the current client context.
58
+ #
59
+ # +path+:: Path to +POST+.
60
+ # +data+:: Data to post as the body.
61
+ #
62
+ # Reference: {FaunaDB REST API}[https://faunadb.com/documentation/rest]
63
+ #
64
+ # :category: Client Methods
65
+ def self.post(path, data = {})
66
+ client.post(path, data)
67
+ end
68
+
69
+ ##
70
+ # Performs a +PUT+ request for a REST endpoint within the current client context.
71
+ #
72
+ # +path+:: Path to +PUT+.
73
+ # +data+:: Data to post as the body.
74
+ #
75
+ # Reference: {FaunaDB REST API}[https://faunadb.com/documentation/rest]
76
+ #
77
+ # :category: Client Methods
78
+ def self.put(path, data = {})
79
+ client.put(path, data)
80
+ end
81
+
82
+ ##
83
+ # Performs a +PATCH+ request for a REST endpoint within the current client context.
84
+ #
85
+ # +path+:: Path to +PATCH+.
86
+ # +data+:: Data to post as the body.
87
+ #
88
+ # Reference: {FaunaDB REST API}[https://faunadb.com/documentation/rest]
89
+ #
90
+ # :category: Client Methods
91
+ def self.patch(path, data = {})
92
+ client.patch(path, data)
93
+ end
94
+
95
+ ##
96
+ # Performs a +DELETE+ request for a REST endpoint within the current client context.
97
+ #
98
+ # +path+:: Path to +DELETE+.
99
+ # +data+:: Data to post as the body.
100
+ #
101
+ # Reference: {FaunaDB REST API}[https://faunadb.com/documentation/rest]
102
+ #
103
+ # :category: Client Methods
104
+ def self.delete(path)
105
+ client.delete(path)
106
+ end
107
+
108
+ ##
109
+ # Issues a query to FaunaDB with the current client context.
110
+ #
111
+ # Queries are built via the Query helpers. See {FaunaDB Query API}[https://faunadb.com/documentation/queries]
112
+ # for information on constructing queries.
113
+ #
114
+ # +expression+:: A query expression
115
+ #
116
+ # :category: Client Methods
117
+ def self.query(expression = nil, &expr_block)
118
+ client.query(expression, &expr_block)
119
+ end
120
+
121
+ ##
122
+ # Returns the current context's client, or if there is none, raises NoContextError.
123
+ def self.client
124
+ stack.last || fail(NoContextError, 'You must be within a Fauna::Context.block to perform operations.')
125
+ end
126
+
127
+ class << self
128
+ private
129
+
130
+ def stack
131
+ Thread.current[:fauna_context_stack] ||= []
132
+ end
133
+ end
134
+ end
135
+ end
@@ -0,0 +1,181 @@
1
+ module Fauna
2
+ ##
3
+ # Error for when the server returns an unexpected kind of response.
4
+ class UnexpectedError < RuntimeError
5
+ # RequestResult for the request that caused this error.
6
+ attr_reader :request_result
7
+
8
+ def initialize(description, request_result) # :nodoc:
9
+ super(description)
10
+ @request_result = request_result
11
+ end
12
+
13
+ def self.get_or_raise(request_result, hash, key) # :nodoc:
14
+ unless hash.is_a? Hash and hash.key? key
15
+ fail UnexpectedError.new("Response JSON does not contain expected key #{key}", request_result)
16
+ end
17
+ hash[key]
18
+ end
19
+ end
20
+
21
+ ##
22
+ # Error returned by the FaunaDB server.
23
+ # For documentation of error types, see the docs[https://faunadb.com/documentation#errors].
24
+ class FaunaError < RuntimeError
25
+ # List of ErrorData objects returned by the server.
26
+ attr_reader :errors
27
+
28
+ # RequestResult for the request that caused this error.
29
+ attr_reader :request_result
30
+
31
+ ##
32
+ # Raises the associated error from a RequestResult based on the status code.
33
+ #
34
+ # Returns +nil+ for 2xx status codes
35
+ def self.raise_for_status_code(request_result)
36
+ case request_result.status_code
37
+ when 200..299
38
+
39
+ when 400
40
+ fail BadRequest.new(request_result)
41
+ when 401
42
+ fail Unauthorized.new(request_result)
43
+ when 403
44
+ fail PermissionDenied.new(request_result)
45
+ when 404
46
+ fail NotFound.new(request_result)
47
+ when 405
48
+ fail MethodNotAllowed.new(request_result)
49
+ when 500
50
+ fail InternalError.new(request_result)
51
+ when 503
52
+ fail UnavailableError.new(request_result)
53
+ else
54
+ fail UnexpectedError.new('Unexpected status code.', request_result)
55
+ end
56
+ end
57
+
58
+ # Creates a new error from a given RequestResult.
59
+ def initialize(request_result)
60
+ @request_result = request_result
61
+ errors_raw = UnexpectedError.get_or_raise request_result, request_result.response_content, :errors
62
+ @errors = catch :invalid_response do
63
+ throw :invalid_response unless errors_raw.is_a? Array
64
+ errors_raw.map { |error| ErrorData.from_hash(error) }
65
+ end
66
+
67
+ if @errors.nil?
68
+ fail UnexpectedError.new('Error data has an unexpected format.', request_result)
69
+ end
70
+
71
+ super(@errors ? @errors[0].description : '(empty `errors`)')
72
+ end
73
+ end
74
+
75
+ # An exception thrown if FaunaDB cannot evaluate a query.
76
+ class BadRequest < FaunaError; end
77
+
78
+ # An exception thrown if FaunaDB responds with an HTTP 401.
79
+ class Unauthorized < FaunaError; end
80
+
81
+ # An exception thrown if FaunaDB responds with an HTTP 403.
82
+ class PermissionDenied < FaunaError; end
83
+
84
+ # An exception thrown if FaunaDB responds with an HTTP 404 for non-query endpoints.
85
+ class NotFound < FaunaError; end
86
+
87
+ # An exception thrown if FaunaDB responds with an HTTP 405.
88
+ class MethodNotAllowed < FaunaError; end
89
+
90
+ ##
91
+ # An exception thrown if FaunaDB responds with an HTTP 500. Such errors represent an internal
92
+ # failure within the database.
93
+ class InternalError < FaunaError; end
94
+
95
+ ##
96
+ # An exception thrown if FaunaDB responds with an HTTP 503.
97
+ class UnavailableError < FaunaError; end
98
+
99
+ # Data for one error returned by the server.
100
+ class ErrorData
101
+ ##
102
+ # Error code.
103
+ #
104
+ # Reference: {FaunaDB Error codes}[https://faunadb.com/documentation#errors]
105
+ attr_reader :code
106
+ # Error description.
107
+ attr_reader :description
108
+ # Position of the error in a query. May be +nil+.
109
+ attr_reader :position
110
+ # List of Failure objects returned by the server. +nil+ except for <code>validation failed</code> errors.
111
+ attr_reader :failures
112
+
113
+ def self.from_hash(hash) # :nodoc:
114
+ code = ErrorHelpers.get_or_throw hash, :code
115
+ description = ErrorHelpers.get_or_throw hash, :description
116
+ position = ErrorHelpers.map_position hash[:position]
117
+ failures = hash[:failures].map(&Failure.method(:from_hash)) unless hash[:failures].nil?
118
+ ErrorData.new code, description, position, failures
119
+ end
120
+
121
+ def initialize(code, description, position, failures) # :nodoc:
122
+ @code = code
123
+ @description = description
124
+ @position = position
125
+ @failures = failures
126
+ end
127
+
128
+ def inspect # :nodoc:
129
+ "ErrorData(#{code.inspect}, #{description.inspect}, #{position.inspect}, #{failures.inspect})"
130
+ end
131
+ end
132
+
133
+ ##
134
+ # Part of ErrorData.
135
+ # For more information, see the {docs}[https://faunadb.com/documentation#errors-invalid_data].
136
+ class Failure
137
+ # Failure code.
138
+ attr_reader :code
139
+ # Failure description.
140
+ attr_reader :description
141
+ # Field of the failure in the instance.
142
+ attr_reader :field
143
+
144
+ def self.from_hash(hash) # :nodoc:
145
+ Failure.new(
146
+ ErrorHelpers.get_or_throw(hash, :code),
147
+ ErrorHelpers.get_or_throw(hash, :description),
148
+ ErrorHelpers.map_position(hash[:field]),
149
+ )
150
+ end
151
+
152
+ def initialize(code, description, field) # :nodoc:
153
+ @code = code
154
+ @description = description
155
+ @field = field
156
+ end
157
+
158
+ def inspect # :nodoc:
159
+ "Failure(#{code.inspect}, #{description.inspect}, #{field.inspect})"
160
+ end
161
+ end
162
+
163
+ module ErrorHelpers # :nodoc:
164
+ def self.map_position(position)
165
+ unless position.nil?
166
+ position.map do |part|
167
+ if part.is_a? String
168
+ part.to_sym
169
+ else
170
+ part
171
+ end
172
+ end
173
+ end
174
+ end
175
+
176
+ def self.get_or_throw(hash, key)
177
+ throw :invalid_response unless hash.is_a? Hash and hash.key? key
178
+ hash[key]
179
+ end
180
+ end
181
+ end
@@ -0,0 +1,60 @@
1
+ module Fauna
2
+ module FaunaJson # :nodoc:
3
+ def self.to_json(value)
4
+ serialize(value).to_json
5
+ end
6
+
7
+ def self.to_json_pretty(value)
8
+ JSON.pretty_generate serialize(value)
9
+ end
10
+
11
+ def self.deserialize(obj)
12
+ if obj.is_a?(Hash)
13
+ if obj.key? :@ref
14
+ Ref.new obj[:@ref]
15
+ elsif obj.key? :@set
16
+ SetRef.new deserialize(obj[:@set])
17
+ elsif obj.key? :@obj
18
+ deserialize(obj[:@obj])
19
+ elsif obj.key? :@ts
20
+ Time.iso8601 obj[:@ts]
21
+ elsif obj.key? :@date
22
+ Date.iso8601 obj[:@date]
23
+ else
24
+ Hash[obj.collect { |k, v| [k, deserialize(v)] }]
25
+ end
26
+ elsif obj.is_a?(Array)
27
+ obj.collect { |val| deserialize(val) }
28
+ else
29
+ obj
30
+ end
31
+ end
32
+
33
+ def self.json_load(body)
34
+ JSON.load body, nil, max_nesting: false, symbolize_names: true
35
+ end
36
+
37
+ def self.json_load_or_nil(body)
38
+ json_load body
39
+ rescue JSON::ParserError
40
+ nil
41
+ end
42
+
43
+ def self.serialize(value)
44
+ if value.is_a? Time
45
+ # 9 means: include nanoseconds in encoding
46
+ { :@ts => value.iso8601(9) }
47
+ elsif value.is_a? Date
48
+ { :@date => value.iso8601 }
49
+ elsif value.is_a? Hash
50
+ Hash[value.collect { |k, v| [k, serialize(v)] }]
51
+ elsif value.is_a? Array
52
+ value.collect { |val| serialize(val) }
53
+ elsif value.respond_to? :to_hash
54
+ serialize(value.to_hash)
55
+ else
56
+ value
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,96 @@
1
+ module Fauna
2
+ ##
3
+ # A Ref.
4
+ #
5
+ # Reference: {FaunaDB Special Types}[https://faunadb.com/documentation/queries-values-special_types]
6
+ class Ref
7
+ # The raw ref string.
8
+ attr_accessor :value
9
+
10
+ ##
11
+ # Creates a Ref object.
12
+ #
13
+ # :call-seq:
14
+ # Ref.new('databases/prydain')
15
+ # Ref.new('databases', 'prydain')
16
+ # Ref.new(Ref.new('databases'), 'prydain')
17
+ #
18
+ # +parts+: A string, or a list of strings/refs to be joined.
19
+ def initialize(*parts)
20
+ @value = parts.join '/'
21
+ end
22
+
23
+ ##
24
+ # Gets the class part out of the Ref.
25
+ # This is done by removing ref.id().
26
+ # So <code>Fauna::Ref.new('a', 'b/c').to_class</code> will be
27
+ # <code>Fauna::Ref.new('a/b')</code>.
28
+ def to_class
29
+ parts = value.split '/'
30
+ if parts.length == 1
31
+ self
32
+ else
33
+ Fauna::Ref.new(*parts[0...-1])
34
+ end
35
+ end
36
+
37
+ ##
38
+ # Removes the class part of the ref, leaving only the id.
39
+ # This is everything after the last /.
40
+ def id
41
+ parts = value.split '/'
42
+ fail ArgumentError.new 'The Ref does not have an id.' if parts.length == 1
43
+ parts.last
44
+ end
45
+
46
+ # Converts the Ref to a string
47
+ def to_s
48
+ value
49
+ end
50
+
51
+ # Converts the Ref in Hash form.
52
+ def to_hash
53
+ { :@ref => value }
54
+ end
55
+
56
+ # Returns +true+ if +other+ is a Ref and contains the same value.
57
+ def ==(other)
58
+ return false unless other.is_a? Ref
59
+ value == other.value
60
+ end
61
+
62
+ alias_method :eql?, :==
63
+ end
64
+
65
+ ##
66
+ # A SetRef.
67
+ #
68
+ # Reference: {FaunaDB Special Types}[https://faunadb.com/documentation/queries-values-special_types]
69
+ class SetRef
70
+ # The raw set hash.
71
+ attr_accessor :value
72
+
73
+ ##
74
+ # Creates a new SetRef with the given parameters.
75
+ #
76
+ # +params+:: Hash of parameters to build the SetRef with.
77
+ #
78
+ # Reference: {FaunaDB Special Types}[https://faunadb.com/documentation/queries-values-special_types]
79
+ def initialize(params = {})
80
+ self.value = params
81
+ end
82
+
83
+ # Converts the SetRef to Hash form.
84
+ def to_hash
85
+ { :@set => value }
86
+ end
87
+
88
+ # Returns +true+ if +other+ is a SetRef and contains the same value.
89
+ def ==(other)
90
+ return false unless other.is_a? SetRef
91
+ value == other.value
92
+ end
93
+
94
+ alias_method :eql?, :==
95
+ end
96
+ end