fauna 1.3.4 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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