fmrest-core 0.13.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 +7 -0
- data/.yardopts +4 -0
- data/CHANGELOG.md +127 -0
- data/LICENSE.txt +21 -0
- data/README.md +523 -0
- data/lib/fmrest-core.rb +3 -0
- data/lib/fmrest-spyke.rb +3 -0
- data/lib/fmrest.rb +36 -0
- data/lib/fmrest/connection_settings.rb +124 -0
- data/lib/fmrest/errors.rb +30 -0
- data/lib/fmrest/string_date.rb +220 -0
- data/lib/fmrest/token_store.rb +12 -0
- data/lib/fmrest/token_store/active_record.rb +74 -0
- data/lib/fmrest/token_store/base.rb +25 -0
- data/lib/fmrest/token_store/memory.rb +26 -0
- data/lib/fmrest/token_store/moneta.rb +41 -0
- data/lib/fmrest/token_store/null.rb +20 -0
- data/lib/fmrest/token_store/redis.rb +45 -0
- data/lib/fmrest/v1.rb +23 -0
- data/lib/fmrest/v1/auth.rb +30 -0
- data/lib/fmrest/v1/connection.rb +116 -0
- data/lib/fmrest/v1/container_fields.rb +114 -0
- data/lib/fmrest/v1/dates.rb +81 -0
- data/lib/fmrest/v1/paths.rb +42 -0
- data/lib/fmrest/v1/raise_errors.rb +57 -0
- data/lib/fmrest/v1/token_session.rb +133 -0
- data/lib/fmrest/v1/token_store/active_record.rb +13 -0
- data/lib/fmrest/v1/token_store/memory.rb +13 -0
- data/lib/fmrest/v1/type_coercer.rb +192 -0
- data/lib/fmrest/v1/utils.rb +113 -0
- data/lib/fmrest/version.rb +5 -0
- metadata +115 -0
data/lib/fmrest-core.rb
ADDED
data/lib/fmrest-spyke.rb
ADDED
data/lib/fmrest.rb
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "faraday"
|
4
|
+
require "faraday_middleware"
|
5
|
+
|
6
|
+
require "fmrest/version"
|
7
|
+
require "fmrest/connection_settings"
|
8
|
+
require "fmrest/errors"
|
9
|
+
|
10
|
+
module FmRest
|
11
|
+
autoload :V1, "fmrest/v1"
|
12
|
+
autoload :TokenStore, "fmrest/token_store"
|
13
|
+
autoload :Spyke, "fmrest/spyke"
|
14
|
+
|
15
|
+
class << self
|
16
|
+
attr_accessor :token_store
|
17
|
+
|
18
|
+
def default_connection_settings=(settings)
|
19
|
+
@default_connection_settings = ConnectionSettings.wrap(settings, skip_validation: true)
|
20
|
+
end
|
21
|
+
|
22
|
+
def default_connection_settings
|
23
|
+
@default_connection_settings || ConnectionSettings.new({}, skip_validation: true)
|
24
|
+
end
|
25
|
+
|
26
|
+
def config=(connection_hash)
|
27
|
+
warn "[DEPRECATION] `FmRest.config=` is deprecated, use `FmRest.default_connection_settings=` instead"
|
28
|
+
self.default_connection_settings = connection_hash
|
29
|
+
end
|
30
|
+
|
31
|
+
def config
|
32
|
+
warn "[DEPRECATION] `FmRest.config` is deprecated, use `FmRest.default_connection_settings` instead"
|
33
|
+
default_connection_settings
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,124 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module FmRest
|
4
|
+
# Wrapper class for connection settings hash, with a number of purposes:
|
5
|
+
#
|
6
|
+
# * Provide indifferent access (base hash can have either string or symbol
|
7
|
+
# keys)
|
8
|
+
# * Method access
|
9
|
+
# * Default values
|
10
|
+
# * Basic validation
|
11
|
+
# * Normalization (e.g. aliased settings)
|
12
|
+
# * Useful error messages
|
13
|
+
class ConnectionSettings
|
14
|
+
class MissingSetting < ArgumentError; end
|
15
|
+
|
16
|
+
PROPERTIES = %i(
|
17
|
+
host
|
18
|
+
database
|
19
|
+
username
|
20
|
+
password
|
21
|
+
token
|
22
|
+
token_store
|
23
|
+
autologin
|
24
|
+
ssl
|
25
|
+
proxy
|
26
|
+
log
|
27
|
+
coerce_dates
|
28
|
+
date_format
|
29
|
+
timestamp_format
|
30
|
+
time_format
|
31
|
+
timezone
|
32
|
+
).freeze
|
33
|
+
|
34
|
+
# NOTE: password intentionally left non-required since it's only really
|
35
|
+
# needed when no token exists, and should only be required when logging in
|
36
|
+
REQUIRED = %i(
|
37
|
+
host
|
38
|
+
database
|
39
|
+
).freeze
|
40
|
+
|
41
|
+
DEFAULT_DATE_FORMAT = "MM/dd/yyyy"
|
42
|
+
DEFAULT_TIME_FORMAT = "HH:mm:ss"
|
43
|
+
DEFAULT_TIMESTAMP_FORMAT = "#{DEFAULT_DATE_FORMAT} #{DEFAULT_TIME_FORMAT}"
|
44
|
+
|
45
|
+
DEFAULTS = {
|
46
|
+
autologin: true,
|
47
|
+
log: false,
|
48
|
+
date_format: DEFAULT_DATE_FORMAT,
|
49
|
+
time_format: DEFAULT_TIME_FORMAT,
|
50
|
+
timestamp_format: DEFAULT_TIMESTAMP_FORMAT,
|
51
|
+
coerce_dates: false
|
52
|
+
}.freeze
|
53
|
+
|
54
|
+
def self.wrap(settings, skip_validation: false)
|
55
|
+
if settings.kind_of?(self)
|
56
|
+
settings.validate unless skip_validation
|
57
|
+
return settings
|
58
|
+
end
|
59
|
+
new(settings, skip_validation: skip_validation)
|
60
|
+
end
|
61
|
+
|
62
|
+
def initialize(settings, skip_validation: false)
|
63
|
+
@settings = settings.to_h.dup
|
64
|
+
normalize
|
65
|
+
validate unless skip_validation
|
66
|
+
end
|
67
|
+
|
68
|
+
PROPERTIES.each do |p|
|
69
|
+
define_method(p) do
|
70
|
+
get(p)
|
71
|
+
end
|
72
|
+
|
73
|
+
define_method("#{p}!") do
|
74
|
+
r = get(p)
|
75
|
+
raise MissingSetting, "Missing required setting: `#{p}'" if r.nil?
|
76
|
+
r
|
77
|
+
end
|
78
|
+
|
79
|
+
define_method("#{p}?") do
|
80
|
+
!!get(p)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
def [](key)
|
85
|
+
raise ArgumentError, "Unknown setting `#{key}'" unless PROPERTIES.include?(key.to_sym)
|
86
|
+
get(key)
|
87
|
+
end
|
88
|
+
|
89
|
+
def to_h
|
90
|
+
PROPERTIES.each_with_object({}) do |p, h|
|
91
|
+
v = get(p)
|
92
|
+
h[p] = v unless v == DEFAULTS[p]
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
def merge(other, **keyword_args)
|
97
|
+
other = self.class.wrap(other, skip_validation: true)
|
98
|
+
self.class.new(to_h.merge(other.to_h), **keyword_args)
|
99
|
+
end
|
100
|
+
|
101
|
+
def validate
|
102
|
+
missing = REQUIRED.select { |r| get(r).nil? }.map { |m| "`#{m}'" }
|
103
|
+
raise MissingSetting, "Missing required setting(s): #{missing.join(', ')}" unless missing.empty?
|
104
|
+
|
105
|
+
unless username? || token?
|
106
|
+
raise MissingSetting, "A minimum of `username' or `token' are required to be able to establish a connection"
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
private
|
111
|
+
|
112
|
+
def get(key)
|
113
|
+
return @settings[key.to_sym] if @settings.has_key?(key.to_sym)
|
114
|
+
return @settings[key.to_s] if @settings.has_key?(key.to_s)
|
115
|
+
DEFAULTS[key.to_sym]
|
116
|
+
end
|
117
|
+
|
118
|
+
def normalize
|
119
|
+
if !get(:username) && account_name = get(:account_name)
|
120
|
+
@settings[:username] = account_name
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module FmRest
|
4
|
+
class Error < StandardError; end
|
5
|
+
|
6
|
+
class APIError < Error
|
7
|
+
attr_reader :code
|
8
|
+
|
9
|
+
def initialize(code, message = nil)
|
10
|
+
@code = code
|
11
|
+
super("FileMaker Data API responded with error #{code}: #{message}")
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
class APIError::UnknownError < APIError; end # error code -1
|
16
|
+
class APIError::ResourceMissingError < APIError; end # error codes 100..199
|
17
|
+
class APIError::RecordMissingError < APIError::ResourceMissingError; end
|
18
|
+
class APIError::AccountError < APIError; end # error codes 200..299
|
19
|
+
class APIError::LockError < APIError; end # error codes 300..399
|
20
|
+
class APIError::ParameterError < APIError; end # error codes 400..499
|
21
|
+
class APIError::NoMatchingRecordsError < APIError::ParameterError; end
|
22
|
+
class APIError::ValidationError < APIError; end # error codes 500..599
|
23
|
+
class APIError::SystemError < APIError; end # error codes 800..899
|
24
|
+
class APIError::InvalidToken < APIError; end # error code 952
|
25
|
+
class APIError::MaximumDataAPICallsExceeded < APIError; end # error code 953
|
26
|
+
class APIError::ScriptError < APIError; end # error codes 1200..1299
|
27
|
+
class APIError::ODBCError < APIError; end # error codes 1400..1499
|
28
|
+
|
29
|
+
class ContainerFieldError < Error; end
|
30
|
+
end
|
@@ -0,0 +1,220 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "date"
|
4
|
+
|
5
|
+
module FmRest
|
6
|
+
# Gotchas:
|
7
|
+
#
|
8
|
+
# 1.
|
9
|
+
#
|
10
|
+
# Date === <StringDate instance> # => false
|
11
|
+
#
|
12
|
+
# The above can affect case conditions, as trying to match a StringDate
|
13
|
+
# with:
|
14
|
+
#
|
15
|
+
# case obj
|
16
|
+
# when Date
|
17
|
+
# ...
|
18
|
+
#
|
19
|
+
# ...will not work.
|
20
|
+
#
|
21
|
+
# Instead one must specify the FmRest::StringDate class:
|
22
|
+
#
|
23
|
+
# case obj
|
24
|
+
# when Date, FmRest::StringDate
|
25
|
+
# ...
|
26
|
+
#
|
27
|
+
# 2.
|
28
|
+
#
|
29
|
+
# StringDate#eql? only matches other strings, not dates.
|
30
|
+
#
|
31
|
+
# This could affect hash indexing when a StringDate is used as a key.
|
32
|
+
#
|
33
|
+
# TODO: Verify the above
|
34
|
+
#
|
35
|
+
# 3.
|
36
|
+
#
|
37
|
+
# StringDate#succ and StringDate#next return a String, despite Date#succ
|
38
|
+
# and Date#next also existing.
|
39
|
+
#
|
40
|
+
# Workaround: Use StringDate#next_day or strdate + 1
|
41
|
+
#
|
42
|
+
# 4.
|
43
|
+
#
|
44
|
+
# StringDate#to_s returns the original string, not the Date string
|
45
|
+
# representation.
|
46
|
+
#
|
47
|
+
# Workaround: Use strdate.to_date.to_s
|
48
|
+
#
|
49
|
+
# 5.
|
50
|
+
#
|
51
|
+
# StringDate#hash returns the hash for the string (important when using
|
52
|
+
# a StringDate as a hash key)
|
53
|
+
#
|
54
|
+
# 6.
|
55
|
+
#
|
56
|
+
# StringDate#as_json returns the string
|
57
|
+
#
|
58
|
+
# Workaround: Use strdate.to_date.as_json
|
59
|
+
#
|
60
|
+
# 7.
|
61
|
+
#
|
62
|
+
# Equality with Date is not reciprocal:
|
63
|
+
#
|
64
|
+
# str_date == date #=> true
|
65
|
+
# date == str_date #=> false
|
66
|
+
#
|
67
|
+
# NOTE: Potential workaround: Inherit StringDate from Date instead of String
|
68
|
+
#
|
69
|
+
# 8.
|
70
|
+
#
|
71
|
+
# Calling string transforming methods (e.g. .upcase) returns a StringDate
|
72
|
+
# instead of a String.
|
73
|
+
#
|
74
|
+
# NOTE: Potential workaround: Inherit StringDate from Date instead of String
|
75
|
+
#
|
76
|
+
class StringDate < String
|
77
|
+
DELEGATE_CLASS = ::Date
|
78
|
+
|
79
|
+
class InvalidDate < ArgumentError; end
|
80
|
+
|
81
|
+
class << self
|
82
|
+
def strptime(str, date_format, *_)
|
83
|
+
begin
|
84
|
+
date = self::DELEGATE_CLASS.strptime(str, date_format)
|
85
|
+
rescue ArgumentError
|
86
|
+
raise InvalidDate
|
87
|
+
end
|
88
|
+
|
89
|
+
new(str, date)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
def initialize(str, date, **str_args)
|
94
|
+
raise ArgumentError, "str must be of class String" unless str.is_a?(String)
|
95
|
+
raise ArgumentError, "date must be of class #{self.class::DELEGATE_CLASS.name}" unless date.is_a?(self.class::DELEGATE_CLASS)
|
96
|
+
|
97
|
+
super(str, **str_args)
|
98
|
+
|
99
|
+
@delegate = date
|
100
|
+
|
101
|
+
freeze
|
102
|
+
end
|
103
|
+
|
104
|
+
def is_a?(klass)
|
105
|
+
klass == ::Date || super
|
106
|
+
end
|
107
|
+
alias_method :kind_of?, :is_a?
|
108
|
+
|
109
|
+
def to_date
|
110
|
+
@delegate
|
111
|
+
end
|
112
|
+
|
113
|
+
def to_datetime
|
114
|
+
@delegate.to_datetime
|
115
|
+
end
|
116
|
+
|
117
|
+
def to_time
|
118
|
+
@delegate.to_time
|
119
|
+
end
|
120
|
+
|
121
|
+
# ActiveSupport method
|
122
|
+
def in_time_zone(*_)
|
123
|
+
@delegate.in_time_zone(*_)
|
124
|
+
end
|
125
|
+
|
126
|
+
def inspect
|
127
|
+
"#<#{self.class.name} #{@delegate.inspect} - #{super}>"
|
128
|
+
end
|
129
|
+
|
130
|
+
def <=>(oth)
|
131
|
+
return @delegate <=> oth if oth.is_a?(::Date) || oth.is_a?(Numeric)
|
132
|
+
super
|
133
|
+
end
|
134
|
+
|
135
|
+
def +(val)
|
136
|
+
return @delegate + val if val.kind_of?(Numeric)
|
137
|
+
super
|
138
|
+
end
|
139
|
+
|
140
|
+
def <<(val)
|
141
|
+
return @delegate << val if val.kind_of?(Numeric)
|
142
|
+
super
|
143
|
+
end
|
144
|
+
|
145
|
+
def ==(oth)
|
146
|
+
return @delegate == oth if oth.kind_of?(::Date) || oth.kind_of?(Numeric)
|
147
|
+
super
|
148
|
+
end
|
149
|
+
alias_method :===, :==
|
150
|
+
|
151
|
+
def upto(oth, &blk)
|
152
|
+
return @delegate.upto(oth, &blk) if oth.kind_of?(::Date) || oth.kind_of?(Numeric)
|
153
|
+
super
|
154
|
+
end
|
155
|
+
|
156
|
+
def between?(a, b)
|
157
|
+
return @delegate.between?(a, b) if [a, b].any? {|o| o.is_a?(::Date) || o.is_a?(Numeric) }
|
158
|
+
super
|
159
|
+
end
|
160
|
+
|
161
|
+
private
|
162
|
+
|
163
|
+
def respond_to_missing?(name, include_private = false)
|
164
|
+
@delegate.respond_to?(name, include_private)
|
165
|
+
end
|
166
|
+
|
167
|
+
def method_missing(method, *args, &block)
|
168
|
+
@delegate.send(method, *args, &block)
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
class StringDateTime < StringDate
|
173
|
+
DELEGATE_CLASS = ::DateTime
|
174
|
+
|
175
|
+
def is_a?(klass)
|
176
|
+
klass == ::DateTime || super
|
177
|
+
end
|
178
|
+
alias_method :kind_of?, :is_a?
|
179
|
+
|
180
|
+
def to_date
|
181
|
+
@delegate.to_date
|
182
|
+
end
|
183
|
+
|
184
|
+
def to_datetime
|
185
|
+
@delegate
|
186
|
+
end
|
187
|
+
end
|
188
|
+
|
189
|
+
module StringDateAwareness
|
190
|
+
def _parse(v, *_)
|
191
|
+
if v.is_a?(StringDateTime)
|
192
|
+
return { year: v.year, mon: v.month, mday: v.mday, hour: v.hour, min: v.min, sec: v.sec, sec_fraction: v.sec_fraction, offset: v.offset }
|
193
|
+
end
|
194
|
+
if v.is_a?(StringDate)
|
195
|
+
return { year: v.year, mon: v.month, mday: v.mday }
|
196
|
+
end
|
197
|
+
super
|
198
|
+
end
|
199
|
+
|
200
|
+
def parse(v, *_)
|
201
|
+
if v.is_a?(StringDate)
|
202
|
+
return self == ::DateTime ? v.to_datetime : v.to_date
|
203
|
+
end
|
204
|
+
super
|
205
|
+
end
|
206
|
+
|
207
|
+
# Overriding case equality method so that it returns true for
|
208
|
+
# `FmRest::StringDate` instances
|
209
|
+
#
|
210
|
+
# Calls superclass method
|
211
|
+
#
|
212
|
+
def ===(other)
|
213
|
+
super || other.is_a?(StringDate)
|
214
|
+
end
|
215
|
+
|
216
|
+
def self.enable(classes: [Date, DateTime])
|
217
|
+
classes.each { |klass| klass.singleton_class.prepend(self) }
|
218
|
+
end
|
219
|
+
end
|
220
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module FmRest
|
4
|
+
module TokenStore
|
5
|
+
autoload :Base, "fmrest/token_store/base"
|
6
|
+
autoload :Memory, "fmrest/token_store/memory"
|
7
|
+
autoload :Null, "fmrest/token_store/null"
|
8
|
+
autoload :ActiveRecord, "fmrest/token_store/active_record"
|
9
|
+
autoload :Moneta, "fmrest/token_store/moneta"
|
10
|
+
autoload :Redis, "fmrest/token_store/redis"
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "fmrest/token_store/base"
|
4
|
+
require "active_record"
|
5
|
+
|
6
|
+
module FmRest
|
7
|
+
module TokenStore
|
8
|
+
# Heavily inspired by Moneta's ActiveRecord store:
|
9
|
+
#
|
10
|
+
# https://github.com/minad/moneta/blob/master/lib/moneta/adapters/activerecord.rb
|
11
|
+
#
|
12
|
+
class ActiveRecord < Base
|
13
|
+
DEFAULT_TABLE_NAME = "fmrest_session_tokens".freeze
|
14
|
+
|
15
|
+
@connection_lock = ::Mutex.new
|
16
|
+
class << self
|
17
|
+
attr_reader :connection_lock
|
18
|
+
end
|
19
|
+
|
20
|
+
attr_reader :connection_pool, :model
|
21
|
+
|
22
|
+
delegate :with_connection, to: :connection_pool
|
23
|
+
|
24
|
+
def initialize(options = {})
|
25
|
+
super
|
26
|
+
|
27
|
+
@connection_pool = ::ActiveRecord::Base.connection_pool
|
28
|
+
|
29
|
+
create_table
|
30
|
+
|
31
|
+
@model = Class.new(::ActiveRecord::Base)
|
32
|
+
@model.table_name = table_name
|
33
|
+
end
|
34
|
+
|
35
|
+
def delete(key)
|
36
|
+
model.where(scope: key).delete_all
|
37
|
+
end
|
38
|
+
|
39
|
+
def load(key)
|
40
|
+
model.where(scope: key).pluck(:token).first
|
41
|
+
end
|
42
|
+
|
43
|
+
def store(key, value)
|
44
|
+
record = model.find_or_initialize_by(scope: key)
|
45
|
+
record.token = value
|
46
|
+
record.save!
|
47
|
+
value
|
48
|
+
end
|
49
|
+
|
50
|
+
private
|
51
|
+
|
52
|
+
def create_table
|
53
|
+
with_connection do |conn|
|
54
|
+
return if conn.table_exists?(table_name)
|
55
|
+
|
56
|
+
# Prevent multiple connections from attempting to create the table simultaneously.
|
57
|
+
self.class.connection_lock.synchronize do
|
58
|
+
conn.create_table(table_name, id: false) do |t|
|
59
|
+
t.string :scope, null: false
|
60
|
+
t.string :token, null: false
|
61
|
+
t.datetime :updated_at
|
62
|
+
end
|
63
|
+
conn.add_index(table_name, :scope, unique: true)
|
64
|
+
conn.add_index(table_name, [:scope, :token])
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def table_name
|
70
|
+
options[:table_name] || DEFAULT_TABLE_NAME
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|