fmrest 0.6.0 → 0.10.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -4,12 +4,18 @@ require "fmrest/v1/connection"
4
4
  require "fmrest/v1/paths"
5
5
  require "fmrest/v1/container_fields"
6
6
  require "fmrest/v1/utils"
7
+ require "fmrest/v1/dates"
7
8
 
8
9
  module FmRest
9
10
  module V1
11
+ DEFAULT_DATE_FORMAT = "MM/dd/yyyy"
12
+ DEFAULT_TIME_FORMAT = "HH:mm:ss"
13
+ DEFAULT_TIMESTAMP_FORMAT = "#{DEFAULT_DATE_FORMAT} #{DEFAULT_TIME_FORMAT}"
14
+
10
15
  extend Connection
11
16
  extend Paths
12
17
  extend ContainerFields
13
18
  extend Utils
19
+ extend Dates
14
20
  end
15
21
  end
@@ -5,7 +5,7 @@ require "uri"
5
5
  module FmRest
6
6
  module V1
7
7
  module Connection
8
- BASE_PATH = "/fmi/data/v1/databases".freeze
8
+ BASE_PATH = "/fmi/data/v1/databases"
9
9
 
10
10
  # Builds a complete DAPI Faraday connection with middleware already
11
11
  # configured to handle authentication, JSON parsing, logging and DAPI
@@ -19,7 +19,6 @@ module FmRest
19
19
  # @option (see #base_connection)
20
20
  # @return (see #base_connection)
21
21
  def build_connection(options = FmRest.default_connection_settings, &block)
22
-
23
22
  base_connection(options) do |conn|
24
23
  conn.use RaiseErrors
25
24
  conn.use TokenSession, options
@@ -32,17 +31,18 @@ module FmRest
32
31
  conn.request :multipart
33
32
  conn.request :json
34
33
 
35
- if options[:log]
36
- conn.response :logger, nil, bodies: true, headers: true
37
- end
38
-
39
34
  # Allow overriding the default response middleware
40
35
  if block_given?
41
- yield conn
36
+ yield conn, options
42
37
  else
38
+ conn.use TypeCoercer, options
43
39
  conn.response :json
44
40
  end
45
41
 
42
+ if options[:log]
43
+ conn.response :logger, nil, bodies: true, headers: true
44
+ end
45
+
46
46
  conn.adapter Faraday.default_adapter
47
47
  end
48
48
  end
@@ -86,3 +86,4 @@ end
86
86
 
87
87
  require "fmrest/v1/token_session"
88
88
  require "fmrest/v1/raise_errors"
89
+ require "fmrest/v1/type_coercer"
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FmRest
4
+ module V1
5
+ module Dates
6
+ FM_DATETIME_FORMAT_MATCHER = /MM|mm|dd|HH|ss|yyyy/.freeze
7
+
8
+ FM_DATE_TO_STRPTIME_SUBSTITUTIONS = {
9
+ "MM" => "%m",
10
+ "dd" => "%d",
11
+ "yyyy" => "%Y",
12
+ "HH" => "%H",
13
+ "mm" => "%M",
14
+ "ss" => "%S"
15
+ }.freeze
16
+
17
+ FM_DATE_TO_REGEXP_SUBSTITUTIONS = {
18
+ "MM" => '(?:0[1-9]|1[012])',
19
+ "dd" => '(?:0[1-9]|[12][0-9]|3[01])',
20
+ "yyyy" => '\d{4}',
21
+ "HH" => '(?:[01]\d|2[0123])',
22
+ "mm" => '[0-5]\d',
23
+ "ss" => '[0-5]\d'
24
+ }.freeze
25
+
26
+ def self.extended(mod)
27
+ mod.instance_eval do
28
+ @date_strptime = {}
29
+ @date_regexp = {}
30
+ end
31
+ end
32
+
33
+ # Converts a FM date-time format to `DateTime.strptime` format
34
+ #
35
+ # @param fm_format [String] The FileMaker date-time format
36
+ # @return [String] The `DateTime.strpdate` equivalent of the given FM
37
+ # date-time format
38
+ def fm_date_to_strptime_format(fm_format)
39
+ @date_strptime[fm_format] ||=
40
+ fm_format.gsub(FM_DATETIME_FORMAT_MATCHER, FM_DATE_TO_STRPTIME_SUBSTITUTIONS).freeze
41
+ end
42
+
43
+ # Converts a FM date-time format to a Regexp. This is mostly used a
44
+ # quicker way of checking whether a FM field is a date field than
45
+ # Date|DateTime.strptime
46
+ #
47
+ # @param fm_format [String] The FileMaker date-time format
48
+ # @return [Regexp] A reegular expression matching strings in the given FM
49
+ # date-time format
50
+ def fm_date_to_regexp(fm_format)
51
+ @date_regexp[fm_format] ||=
52
+ Regexp.new('\A' + fm_format.gsub(FM_DATETIME_FORMAT_MATCHER, FM_DATE_TO_REGEXP_SUBSTITUTIONS) + '\Z').freeze
53
+ end
54
+
55
+ # Takes a DateTime dt, and returns the correct local offset for that dt,
56
+ # daylight savings included, in fraction of a day.
57
+ #
58
+ # By default, if ActiveSupport's Time.zone is set it will be used instead
59
+ # of the system timezone.
60
+ #
61
+ # @param dt [DateTime] The DateTime to get the offset for
62
+ # @param zone [nil, String, TimeZone] The timezone to use to calculate
63
+ # the offset (defaults to system timezone, or ActiveSupport's Time.zone
64
+ # if set)
65
+ # @return [Rational] The offset in fraction of a day
66
+ def local_offset_for_datetime(dt, zone = nil)
67
+ dt = dt.new_offset(0)
68
+ time = ::Time.utc(dt.year, dt.month, dt.day, dt.hour, dt.min, dt.sec)
69
+
70
+ # Do we have ActiveSupport's TimeZone?
71
+ time = if time.respond_to?(:in_time_zone)
72
+ time.in_time_zone(zone || ::Time.zone)
73
+ else
74
+ time.localtime
75
+ end
76
+
77
+ Rational(time.utc_offset, 86400) # seconds in one day (24*60*60)
78
+ end
79
+ end
80
+ end
81
+ end
@@ -14,6 +14,8 @@ module FmRest
14
14
  TOKEN_STORE_INTERFACE = [:load, :store, :delete].freeze
15
15
  LOGOUT_PATH_MATCHER = %r{\A(#{FmRest::V1::Connection::BASE_PATH}/[^/]+/sessions/)[^/]+\Z}.freeze
16
16
 
17
+ # @param app [#call]
18
+ # @param options [Hash]
17
19
  def initialize(app, options = FmRest.default_connection_settings)
18
20
  super(app)
19
21
  @options = options
@@ -0,0 +1,192 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fmrest/string_date"
4
+
5
+ module FmRest
6
+ module V1
7
+ class TypeCoercer < Faraday::Response::Middleware
8
+ # We use this date to represent a FileMaker time for consistency with
9
+ # ginjo-rfm
10
+ JULIAN_ZERO_DAY = "-4712/1/1"
11
+
12
+ COERCE_HYBRID = [:hybrid, "hybrid", true].freeze
13
+ COERCE_FULL = [:full, "full"].freeze
14
+
15
+ # @param app [#call]
16
+ # @param options [Hash]
17
+ def initialize(app, options = FmRest.default_connection_settings)
18
+ super(app)
19
+ @options = options
20
+ end
21
+
22
+ def on_complete(env)
23
+ return unless enabled?
24
+ return unless env.body.kind_of?(Hash)
25
+
26
+ data = env.body.dig("response", "data") || env.body.dig(:response, :data)
27
+
28
+ return unless data
29
+
30
+ data.each do |record|
31
+ field_data = record["fieldData"] || record[:fieldData]
32
+ portal_data = record["portalData"] || record[:portalData]
33
+
34
+ coerce_fields(field_data)
35
+
36
+ portal_data.try(:each_value) do |portal_records|
37
+ portal_records.each do |pr|
38
+ coerce_fields(pr)
39
+ end
40
+ end
41
+ end
42
+ end
43
+
44
+ private
45
+
46
+ def coerce_fields(hash)
47
+ hash.each do |k, v|
48
+ next unless v.is_a?(String)
49
+ next if k == "recordId" || k == :recordId || k == "modId" || k == :modId
50
+
51
+ if quick_check_timestamp(v)
52
+ begin
53
+ hash[k] = coerce_timestamp(v)
54
+ next
55
+ rescue ArgumentError
56
+ end
57
+ end
58
+
59
+ if quick_check_date(v)
60
+ begin
61
+ hash[k] = date_class.strptime(v, date_strptime_format)
62
+ next
63
+ rescue ArgumentError
64
+ end
65
+ end
66
+
67
+ if quick_check_time(v)
68
+ begin
69
+ hash[k] = datetime_class.strptime("#{JULIAN_ZERO_DAY} #{v}", time_strptime_format)
70
+ next
71
+ rescue ArgumentError
72
+ end
73
+ end
74
+ end
75
+ end
76
+
77
+ def coerce_timestamp(str)
78
+ str_timestamp = DateTime.strptime(str, datetime_strptime_format)
79
+
80
+ if local_timezone?
81
+ # Change the DateTime to the local timezone, keeping the same
82
+ # time and just modifying the timezone
83
+ offset = FmRest::V1.local_offset_for_datetime(str_timestamp)
84
+ str_timestamp = str_timestamp.new_offset(offset) - offset
85
+ end
86
+
87
+ if datetime_class == StringDateTime
88
+ str_timestamp = StringDateTime.new(str, str_timestamp)
89
+ end
90
+
91
+ str_timestamp
92
+ end
93
+
94
+ def date_class
95
+ @date_class ||=
96
+ case coerce_dates
97
+ when *COERCE_HYBRID
98
+ StringDate
99
+ when *COERCE_FULL
100
+ Date
101
+ end
102
+ end
103
+
104
+ def datetime_class
105
+ @datetime_class ||=
106
+ case coerce_dates
107
+ when *COERCE_HYBRID
108
+ StringDateTime
109
+ when *COERCE_FULL
110
+ DateTime
111
+ end
112
+ end
113
+
114
+ def date_fm_format
115
+ @options[:date_format] || DEFAULT_DATE_FORMAT
116
+ end
117
+
118
+ def timestamp_fm_format
119
+ @options[:timestamp_format] || DEFAULT_TIMESTAMP_FORMAT
120
+ end
121
+
122
+ def time_fm_format
123
+ @options[:time_format] || DEFAULT_TIME_FORMAT
124
+ end
125
+
126
+ def date_strptime_format
127
+ FmRest::V1.fm_date_to_strptime_format(date_fm_format)
128
+ end
129
+
130
+ def datetime_strptime_format
131
+ FmRest::V1.fm_date_to_strptime_format(timestamp_fm_format)
132
+ end
133
+
134
+ def time_strptime_format
135
+ @time_strptime_format ||=
136
+ "%Y/%m/%d " + FmRest::V1.fm_date_to_strptime_format(time_fm_format)
137
+ end
138
+
139
+ # We use a string length test, followed by regexp match test to try to
140
+ # identify date fields. Benchmarking shows this should be between 1 and 3
141
+ # orders of magnitude faster for fails (i.e. non-dates) than just using
142
+ # Date.strptime.
143
+ #
144
+ # user system total real
145
+ # strptime: 0.268496 0.000962 0.269458 ( 0.270865)
146
+ # re=~: 0.024872 0.000070 0.024942 ( 0.025057)
147
+ # re.match?: 0.019745 0.000095 0.019840 ( 0.020058)
148
+ # strptime fail: 0.141309 0.000354 0.141663 ( 0.142266)
149
+ # re=~ fail: 0.031637 0.000095 0.031732 ( 0.031872)
150
+ # re.match? fail: 0.011249 0.000056 0.011305 ( 0.011375)
151
+ # length fail: 0.007177 0.000024 0.007201 ( 0.007222)
152
+ #
153
+ # NOTE: The faster Regexp#match? was introduced in Ruby 2.4.0, so we
154
+ # can't really rely on it being available
155
+ if //.respond_to?(:match?)
156
+ def quick_check_timestamp(v)
157
+ v.length == timestamp_fm_format.length && FmRest::V1::fm_date_to_regexp(timestamp_fm_format).match?(v)
158
+ end
159
+
160
+ def quick_check_date(v)
161
+ v.length == date_fm_format.length && FmRest::V1::fm_date_to_regexp(date_fm_format).match?(v)
162
+ end
163
+
164
+ def quick_check_time(v)
165
+ v.length == time_fm_format.length && FmRest::V1::fm_date_to_regexp(time_fm_format).match?(v)
166
+ end
167
+ else
168
+ def quick_check_timestamp(v)
169
+ v.length == timestamp_fm_format.length && FmRest::V1::fm_date_to_regexp(timestamp_fm_format) =~ v
170
+ end
171
+
172
+ def quick_check_date(v)
173
+ v.length == date_fm_format.length && FmRest::V1::fm_date_to_regexp(date_fm_format) =~ v
174
+ end
175
+
176
+ def quick_check_time(v)
177
+ v.length == time_fm_format.length && FmRest::V1::fm_date_to_regexp(time_fm_format) =~ v
178
+ end
179
+ end
180
+
181
+ def local_timezone?
182
+ @local_timezone ||= @options.fetch(:timezone, nil).try(:to_sym) == :local
183
+ end
184
+
185
+ def coerce_dates
186
+ @options.fetch(:coerce_dates, false)
187
+ end
188
+
189
+ alias_method :enabled?, :coerce_dates
190
+ end
191
+ end
192
+ end
@@ -72,6 +72,7 @@ module FmRest
72
72
  params
73
73
  end
74
74
 
75
+
75
76
  private
76
77
 
77
78
  def convert_script_arguments(script_arguments, suffix = nil)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module FmRest
4
- VERSION = "0.6.0"
4
+ VERSION = "0.10.0"
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: fmrest
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.0
4
+ version: 0.10.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Pedro Carbajal
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2020-05-05 00:00:00.000000000 Z
11
+ date: 2020-05-31 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: faraday
@@ -231,7 +231,6 @@ files:
231
231
  - ".travis.yml"
232
232
  - ".yardopts"
233
233
  - CHANGELOG.md
234
- - CODE_OF_CONDUCT.md
235
234
  - Gemfile
236
235
  - LICENSE.txt
237
236
  - README.md
@@ -242,20 +241,22 @@ files:
242
241
  - lib/fmrest/spyke.rb
243
242
  - lib/fmrest/spyke/base.rb
244
243
  - lib/fmrest/spyke/container_field.rb
245
- - lib/fmrest/spyke/json_parser.rb
246
244
  - lib/fmrest/spyke/model.rb
247
245
  - lib/fmrest/spyke/model/associations.rb
248
246
  - lib/fmrest/spyke/model/attributes.rb
249
247
  - lib/fmrest/spyke/model/auth.rb
250
248
  - lib/fmrest/spyke/model/connection.rb
251
249
  - lib/fmrest/spyke/model/container_fields.rb
250
+ - lib/fmrest/spyke/model/global_fields.rb
252
251
  - lib/fmrest/spyke/model/http.rb
253
252
  - lib/fmrest/spyke/model/orm.rb
254
253
  - lib/fmrest/spyke/model/serialization.rb
255
254
  - lib/fmrest/spyke/model/uri.rb
256
255
  - lib/fmrest/spyke/portal.rb
257
256
  - lib/fmrest/spyke/relation.rb
257
+ - lib/fmrest/spyke/spyke_formatter.rb
258
258
  - lib/fmrest/spyke/validation_error.rb
259
+ - lib/fmrest/string_date.rb
259
260
  - lib/fmrest/token_store.rb
260
261
  - lib/fmrest/token_store/active_record.rb
261
262
  - lib/fmrest/token_store/base.rb
@@ -265,11 +266,13 @@ files:
265
266
  - lib/fmrest/v1.rb
266
267
  - lib/fmrest/v1/connection.rb
267
268
  - lib/fmrest/v1/container_fields.rb
269
+ - lib/fmrest/v1/dates.rb
268
270
  - lib/fmrest/v1/paths.rb
269
271
  - lib/fmrest/v1/raise_errors.rb
270
272
  - lib/fmrest/v1/token_session.rb
271
273
  - lib/fmrest/v1/token_store/active_record.rb
272
274
  - lib/fmrest/v1/token_store/memory.rb
275
+ - lib/fmrest/v1/type_coercer.rb
273
276
  - lib/fmrest/v1/utils.rb
274
277
  - lib/fmrest/version.rb
275
278
  homepage: https://github.com/beezwax/fmrest-ruby
@@ -1,74 +0,0 @@
1
- # Contributor Covenant Code of Conduct
2
-
3
- ## Our Pledge
4
-
5
- In the interest of fostering an open and welcoming environment, we as
6
- contributors and maintainers pledge to making participation in our project and
7
- our community a harassment-free experience for everyone, regardless of age, body
8
- size, disability, ethnicity, gender identity and expression, level of experience,
9
- nationality, personal appearance, race, religion, or sexual identity and
10
- orientation.
11
-
12
- ## Our Standards
13
-
14
- Examples of behavior that contributes to creating a positive environment
15
- include:
16
-
17
- * Using welcoming and inclusive language
18
- * Being respectful of differing viewpoints and experiences
19
- * Gracefully accepting constructive criticism
20
- * Focusing on what is best for the community
21
- * Showing empathy towards other community members
22
-
23
- Examples of unacceptable behavior by participants include:
24
-
25
- * The use of sexualized language or imagery and unwelcome sexual attention or
26
- advances
27
- * Trolling, insulting/derogatory comments, and personal or political attacks
28
- * Public or private harassment
29
- * Publishing others' private information, such as a physical or electronic
30
- address, without explicit permission
31
- * Other conduct which could reasonably be considered inappropriate in a
32
- professional setting
33
-
34
- ## Our Responsibilities
35
-
36
- Project maintainers are responsible for clarifying the standards of acceptable
37
- behavior and are expected to take appropriate and fair corrective action in
38
- response to any instances of unacceptable behavior.
39
-
40
- Project maintainers have the right and responsibility to remove, edit, or
41
- reject comments, commits, code, wiki edits, issues, and other contributions
42
- that are not aligned to this Code of Conduct, or to ban temporarily or
43
- permanently any contributor for other behaviors that they deem inappropriate,
44
- threatening, offensive, or harmful.
45
-
46
- ## Scope
47
-
48
- This Code of Conduct applies both within project spaces and in public spaces
49
- when an individual is representing the project or its community. Examples of
50
- representing a project or community include using an official project e-mail
51
- address, posting via an official social media account, or acting as an appointed
52
- representative at an online or offline event. Representation of a project may be
53
- further defined and clarified by project maintainers.
54
-
55
- ## Enforcement
56
-
57
- Instances of abusive, harassing, or otherwise unacceptable behavior may be
58
- reported by contacting the project team at pedro_c@beezwax.net. All
59
- complaints will be reviewed and investigated and will result in a response that
60
- is deemed necessary and appropriate to the circumstances. The project team is
61
- obligated to maintain confidentiality with regard to the reporter of an incident.
62
- Further details of specific enforcement policies may be posted separately.
63
-
64
- Project maintainers who do not follow or enforce the Code of Conduct in good
65
- faith may face temporary or permanent repercussions as determined by other
66
- members of the project's leadership.
67
-
68
- ## Attribution
69
-
70
- This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
71
- available at [http://contributor-covenant.org/version/1/4][version]
72
-
73
- [homepage]: http://contributor-covenant.org
74
- [version]: http://contributor-covenant.org/version/1/4/