fmrest 0.6.0 → 0.10.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.
@@ -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/