fmrest 0.10.0 → 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.
Files changed (50) hide show
  1. checksums.yaml +4 -4
  2. data/.yardopts +2 -0
  3. data/CHANGELOG.md +38 -0
  4. data/README.md +194 -763
  5. metadata +70 -97
  6. data/.gitignore +0 -26
  7. data/.rspec +0 -3
  8. data/.travis.yml +0 -5
  9. data/Gemfile +0 -3
  10. data/Rakefile +0 -6
  11. data/fmrest.gemspec +0 -38
  12. data/lib/fmrest.rb +0 -29
  13. data/lib/fmrest/errors.rb +0 -28
  14. data/lib/fmrest/spyke.rb +0 -21
  15. data/lib/fmrest/spyke/base.rb +0 -23
  16. data/lib/fmrest/spyke/container_field.rb +0 -59
  17. data/lib/fmrest/spyke/model.rb +0 -36
  18. data/lib/fmrest/spyke/model/associations.rb +0 -82
  19. data/lib/fmrest/spyke/model/attributes.rb +0 -171
  20. data/lib/fmrest/spyke/model/auth.rb +0 -35
  21. data/lib/fmrest/spyke/model/connection.rb +0 -74
  22. data/lib/fmrest/spyke/model/container_fields.rb +0 -25
  23. data/lib/fmrest/spyke/model/global_fields.rb +0 -40
  24. data/lib/fmrest/spyke/model/http.rb +0 -37
  25. data/lib/fmrest/spyke/model/orm.rb +0 -212
  26. data/lib/fmrest/spyke/model/serialization.rb +0 -91
  27. data/lib/fmrest/spyke/model/uri.rb +0 -30
  28. data/lib/fmrest/spyke/portal.rb +0 -55
  29. data/lib/fmrest/spyke/relation.rb +0 -359
  30. data/lib/fmrest/spyke/spyke_formatter.rb +0 -273
  31. data/lib/fmrest/spyke/validation_error.rb +0 -25
  32. data/lib/fmrest/string_date.rb +0 -220
  33. data/lib/fmrest/token_store.rb +0 -6
  34. data/lib/fmrest/token_store/active_record.rb +0 -74
  35. data/lib/fmrest/token_store/base.rb +0 -25
  36. data/lib/fmrest/token_store/memory.rb +0 -26
  37. data/lib/fmrest/token_store/moneta.rb +0 -41
  38. data/lib/fmrest/token_store/redis.rb +0 -45
  39. data/lib/fmrest/v1.rb +0 -21
  40. data/lib/fmrest/v1/connection.rb +0 -89
  41. data/lib/fmrest/v1/container_fields.rb +0 -114
  42. data/lib/fmrest/v1/dates.rb +0 -81
  43. data/lib/fmrest/v1/paths.rb +0 -47
  44. data/lib/fmrest/v1/raise_errors.rb +0 -57
  45. data/lib/fmrest/v1/token_session.rb +0 -142
  46. data/lib/fmrest/v1/token_store/active_record.rb +0 -13
  47. data/lib/fmrest/v1/token_store/memory.rb +0 -13
  48. data/lib/fmrest/v1/type_coercer.rb +0 -192
  49. data/lib/fmrest/v1/utils.rb +0 -95
  50. data/lib/fmrest/version.rb +0 -5
@@ -1,359 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module FmRest
4
- module Spyke
5
- class Relation < ::Spyke::Relation
6
- SORT_PARAM_MATCHER = /(.*?)(!|__desc(?:end)?)?\Z/.freeze
7
-
8
- # NOTE: We need to keep limit, offset, sort, query and portal accessors
9
- # separate from regular params because FM Data API uses either "limit" or
10
- # "_limit" (or "_offset", etc.) as param keys depending on the type of
11
- # request, so we can't set the params until the last moment
12
-
13
-
14
- attr_accessor :limit_value, :offset_value, :sort_params, :query_params,
15
- :included_portals, :portal_params, :script_params
16
-
17
- def initialize(*_args)
18
- super
19
-
20
- @limit_value = klass.default_limit
21
-
22
- if klass.default_sort.present?
23
- @sort_params = Array.wrap(klass.default_sort).map { |s| normalize_sort_param(s) }
24
- end
25
-
26
- @query_params = []
27
-
28
- @included_portals = nil
29
- @portal_params = {}
30
- @script_params = {}
31
- end
32
-
33
- # @param options [String, Array, Hash, nil, false] sets script params to
34
- # execute in the next get or find request
35
- #
36
- # @example
37
- # # Find records and run the script named "My script"
38
- # Person.script("My script").find_some
39
- #
40
- # # Find records and run the script named "My script" with param "the param"
41
- # Person.script(["My script", "the param"]).find_some
42
- #
43
- # # Find records and run a prerequest, presort and after (normal) script
44
- # Person.script(after: "Script", prerequest: "Prereq script", presort: "Presort script").find_some
45
- #
46
- # # Same as above, but passing parameters too
47
- # Person.script(
48
- # after: ["After script", "the param"],
49
- # prerequest: ["Prereq script", "the param"],
50
- # presort: o ["Presort script", "the param"]
51
- # ).find_some
52
- #
53
- # Person.script(nil).find_some # Disable script execution
54
- # Person.script(false).find_some # Disable script execution
55
- #
56
- # @return [FmRest::Spyke::Relation] a new relation with the script
57
- # options applied
58
- def script(options)
59
- with_clone do |r|
60
- if options.eql?(false) || options.eql?(nil)
61
- r.script_params = {}
62
- else
63
- r.script_params = script_params.merge(FmRest::V1.convert_script_params(options))
64
- end
65
- end
66
- end
67
-
68
- # @param value_or_hash [Integer, Hash] the limit value for this layout,
69
- # or a hash with limits for the layout's portals
70
- # @example
71
- # Person.limit(10) # Set layout limit
72
- # Person.limit(children: 10) # Set portal limit
73
- # @return [FmRest::Spyke::Relation] a new relation with the limits
74
- # applied
75
- def limit(value_or_hash)
76
- with_clone do |r|
77
- if value_or_hash.respond_to?(:each)
78
- r.set_portal_params(value_or_hash, :limit)
79
- else
80
- r.limit_value = value_or_hash
81
- end
82
- end
83
- end
84
-
85
- # @param value_or_hash [Integer, Hash] the offset value for this layout,
86
- # or a hash with offsets for the layout's portals
87
- # @example
88
- # Person.offset(10) # Set layout offset
89
- # Person.offset(children: 10) # Set portal offset
90
- # @return [FmRest::Spyke::Relation] a new relation with the offsets
91
- # applied
92
- def offset(value_or_hash)
93
- with_clone do |r|
94
- if value_or_hash.respond_to?(:each)
95
- r.set_portal_params(value_or_hash, :offset)
96
- else
97
- r.offset_value = value_or_hash
98
- end
99
- end
100
- end
101
-
102
- # Allows sort params given in either hash format (using FM Data API's
103
- # format), or as a symbol, in which case the of the attribute must match
104
- # a known mapped attribute, optionally suffixed with `!` or `__desc` to
105
- # signify it should use descending order.
106
- #
107
- # @param args [Array<Symbol, Hash>] the names of attributes to sort by with
108
- # optional `!` or `__desc` suffix, or a hash of options as expected by
109
- # the FM Data API
110
- # @example
111
- # Person.sort(:first_name, :age!)
112
- # Person.sort(:first_name, :age__desc)
113
- # Person.sort(:first_name, :age__descend)
114
- # Person.sort({ fieldName: "FirstName" }, { fieldName: "Age", sortOrder: "descend" })
115
- # @return [FmRest::Spyke::Relation] a new relation with the sort options
116
- # applied
117
- def sort(*args)
118
- with_clone do |r|
119
- r.sort_params = args.flatten.map { |s| normalize_sort_param(s) }
120
- end
121
- end
122
- alias order sort
123
-
124
- # Sets the portals to include with each record in the response.
125
- #
126
- # @param args [Array<Symbol, String>, true, false] the names of portals to
127
- # include, or `false` to request no portals
128
- # @example
129
- # Person.portal(:relatives, :pets)
130
- # Person.portal(false) # Disables portals
131
- # Person.portal(true) # Enables portals (includes all)
132
- # @return [FmRest::Spyke::Relation] a new relation with the portal
133
- # options applied
134
- def portal(*args)
135
- raise ArgumentError, "Call `portal' with at least one argument" if args.empty?
136
-
137
- with_clone do |r|
138
- if args.length == 1 && args.first.eql?(true) || args.first.eql?(false)
139
- r.included_portals = args.first ? nil : []
140
- else
141
- r.included_portals ||= []
142
- r.included_portals += args.flatten.map { |p| normalize_portal_param(p) }
143
- r.included_portals.uniq!
144
- end
145
- end
146
- end
147
- alias includes portal
148
- alias portals portal
149
-
150
- # Same as calling `portal(true)`
151
- #
152
- # @return (see #portal)
153
- def with_all_portals
154
- portal(true)
155
- end
156
-
157
- # Same as calling `portal(false)`
158
- #
159
- # @return (see #portal)
160
- def without_portals
161
- portal(false)
162
- end
163
-
164
- def query(*params)
165
- with_clone do |r|
166
- r.query_params += params.flatten.map { |p| normalize_query_params(p) }
167
- end
168
- end
169
-
170
- def omit(params)
171
- query(params.merge(omit: true))
172
- end
173
-
174
- # @return [Boolean] whether a query was set on this relation
175
- def has_query?
176
- query_params.present?
177
- end
178
-
179
- # Finds a single instance of the model by forcing limit = 1, or simply
180
- # fetching the record by id if the primary key was set
181
- #
182
- # @return [FmRest::Spyke::Base]
183
- def find_one
184
- @find_one ||=
185
- if primary_key_set?
186
- without_collection_params { super }
187
- else
188
- klass.new_collection_from_result(limit(1).fetch).first
189
- end
190
- rescue ::Spyke::ConnectionError => error
191
- fallback_or_reraise(error, default: nil)
192
- end
193
- alias_method :first, :find_one
194
- alias_method :any, :find_one
195
-
196
- # Yields each batch of records that was found by the find options.
197
- #
198
- # NOTE: By its nature, batch processing is subject to race conditions if
199
- # other processes are modifying the database
200
- #
201
- # @param batch_size [Integer] Specifies the size of the batch.
202
- # @return [Enumerator] if called without a block.
203
- def find_in_batches(batch_size: 1000)
204
- unless block_given?
205
- return to_enum(:find_in_batches, batch_size: batch_size) do
206
- total = limit(1).find_some.metadata.data_info.found_count
207
- (total - 1).div(batch_size) + 1
208
- end
209
- end
210
-
211
- offset = 1 # DAPI offset is 1-based
212
-
213
- loop do
214
- relation = offset(offset).limit(batch_size)
215
-
216
- records = relation.find_some
217
-
218
- yield records if records.length > 0
219
-
220
- break if records.length < batch_size
221
-
222
- # Save one iteration if the total is a multiple of batch_size
223
- if found_count = records.metadata.data_info && records.metadata.data_info.found_count
224
- break if found_count == (offset - 1) + batch_size
225
- end
226
-
227
- offset += batch_size
228
- end
229
- end
230
-
231
- # Looping through a collection of records from the database (using the
232
- # #all method, for example) is very inefficient since it will fetch and
233
- # instantiate all the objects at once.
234
- #
235
- # In that case, batch processing methods allow you to work with the
236
- # records in batches, thereby greatly reducing memory consumption and be
237
- # lighter on the Data API server.
238
- #
239
- # The find_each method uses #find_in_batches with a batch size of 1000
240
- # (or as specified by the :batch_size option).
241
- #
242
- # NOTE: By its nature, batch processing is subject to race conditions if
243
- # other processes are modifying the database
244
- #
245
- # @param (see #find_in_batches)
246
- # @example
247
- # Person.find_each do |person|
248
- # person.greet
249
- # end
250
- #
251
- # Person.query(name: "==Mitch").find_each do |person|
252
- # person.say_hi
253
- # end
254
- # @return (see #find_in_batches)
255
- def find_each(batch_size: 1000)
256
- unless block_given?
257
- return to_enum(:find_each, batch_size: batch_size) do
258
- limit(1).find_some.metadata.data_info.found_count
259
- end
260
- end
261
-
262
- find_in_batches(batch_size: batch_size) do |records|
263
- records.each { |r| yield r }
264
- end
265
- end
266
-
267
- protected
268
-
269
- def set_portal_params(params_hash, param)
270
- # Copy portal_params so we're not modifying the same hash as the parent
271
- # scope
272
- self.portal_params = portal_params.dup
273
-
274
- params_hash.each do |portal_name, value|
275
- # TODO: Use a hash like { portal_name: { param: value } } instead so
276
- # we can intelligently avoid including portal params for excluded
277
- # portals
278
- key = "#{param}.#{normalize_portal_param(portal_name)}"
279
-
280
- # Delete key if value is falsy
281
- if !value && portal_params.has_key?(key)
282
- portal_params.delete(key)
283
- else
284
- self.portal_params[key] = value
285
- end
286
- end
287
- end
288
-
289
- private
290
-
291
- def normalize_sort_param(param)
292
- if param.kind_of?(Symbol) || param.kind_of?(String)
293
- _, attr, descend = param.to_s.match(SORT_PARAM_MATCHER).to_a
294
-
295
- unless field_name = klass.mapped_attributes[attr]
296
- raise ArgumentError, "Unknown attribute `#{attr}' given to sort as #{param.inspect}. If you want to use a custom sort pass a hash in the Data API format"
297
- end
298
-
299
- hash = { fieldName: field_name }
300
- hash[:sortOrder] = "descend" if descend
301
- return hash
302
- end
303
-
304
- # TODO: Sanitize sort hash param for FM Data API conformity?
305
- param
306
- end
307
-
308
- def normalize_portal_param(param)
309
- if param.kind_of?(Symbol)
310
- portal_key, = klass.portal_options.find { |_, opts| opts[:name].to_s == param.to_s }
311
-
312
- unless portal_key
313
- raise ArgumentError, "Unknown portal #{param.inspect}. If you want to include a portal not defined in the model pass it as a string instead"
314
- end
315
-
316
- return portal_key
317
- end
318
-
319
- param
320
- end
321
-
322
- def normalize_query_params(params)
323
- params.each_with_object({}) do |(k, v), normalized|
324
- if k == :omit || k == "omit"
325
- # FM Data API wants omit values as strings, e.g. "true" or "false"
326
- # rather than true/false
327
- normalized["omit"] = v.to_s
328
- next
329
- end
330
-
331
- # TODO: Raise ArgumentError if an attribute given as symbol isn't defiend
332
- if k.kind_of?(Symbol) && klass.mapped_attributes.has_key?(k)
333
- normalized[klass.mapped_attributes[k].to_s] = v
334
- else
335
- normalized[k.to_s] = v
336
- end
337
- end
338
- end
339
-
340
- def primary_key_set?
341
- params[klass.primary_key].present?
342
- end
343
-
344
- def without_collection_params
345
- orig_values = limit_value, offset_value, sort_params, query_params
346
- self.limit_value = self.offset_value = self.sort_params = self.query_params = nil
347
- yield
348
- ensure
349
- self.limit_value, self.offset_value, self.sort_params, self.query_params = orig_values
350
- end
351
-
352
- def with_clone
353
- clone.tap do |relation|
354
- yield relation
355
- end
356
- end
357
- end
358
- end
359
- end
@@ -1,273 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "json"
4
- require "ostruct"
5
-
6
- module FmRest
7
- module Spyke
8
- # Metadata class to be passed to Spyke::Collection#metadata
9
- class Metadata < Struct.new(:messages, :script, :data_info)
10
- alias_method :scripts, :script
11
- end
12
-
13
- class DataInfo < OpenStruct
14
- def total_record_count; totalRecordCount; end
15
- def found_count; foundCount; end
16
- def returned_count; returnedCount; end
17
- end
18
-
19
- # Response Faraday middleware for converting FM API's response JSON into
20
- # Spyke's expected format
21
- class SpykeFormatter < ::Faraday::Response::Middleware
22
- SINGLE_RECORD_RE = %r(/records/\d+\z).freeze
23
- MULTIPLE_RECORDS_RE = %r(/records\z).freeze
24
- CONTAINER_RE = %r(/records/\d+/containers/[^/]+/\d+\z).freeze
25
- FIND_RECORDS_RE = %r(/_find\b).freeze
26
- SCRIPT_REQUEST_RE = %r(/script/[^/]+\z).freeze
27
-
28
- VALIDATION_ERROR_RANGE = 500..599
29
-
30
- # @param app [#call]
31
- # @param model [Class<FmRest::Spyke::Base>]
32
- def initialize(app, model)
33
- super(app)
34
- @model = model
35
- end
36
-
37
- # @param env [Faraday::Env]
38
- def on_complete(env)
39
- return unless env.body.is_a?(Hash)
40
-
41
- json = env.body
42
-
43
- case
44
- when single_record_request?(env)
45
- env.body = prepare_single_record(json)
46
- when multiple_records_request?(env), find_request?(env)
47
- env.body = prepare_collection(json)
48
- when create_request?(env), update_request?(env), delete_request?(env), container_upload_request?(env)
49
- env.body = prepare_save_response(json)
50
- when execute_script_request?(env)
51
- env.body = build_base_hash(json)
52
- else
53
- # Attempt to parse unknown requests too
54
- env.body = build_base_hash(json)
55
- end
56
- end
57
-
58
- private
59
-
60
- # @param json [Hash]
61
- # @return [Hash] the response in Spyke format
62
- def prepare_save_response(json)
63
- response = json[:response]
64
-
65
- data = {}
66
- data[:mod_id] = response[:modId] if response[:modId]
67
- data[:id] = response[:recordId].to_i if response[:recordId]
68
-
69
- build_base_hash(json, true).merge!(data: data)
70
- end
71
-
72
- # (see #prepare_save_response)
73
- def prepare_single_record(json)
74
- data =
75
- json[:response][:data] &&
76
- prepare_record_data(json[:response][:data].first)
77
-
78
- build_base_hash(json).merge!(data: data)
79
- end
80
-
81
- # (see #prepare_save_response)
82
- def prepare_collection(json)
83
- data =
84
- json[:response][:data] &&
85
- json[:response][:data].map { |record_data| prepare_record_data(record_data) }
86
-
87
- build_base_hash(json).merge!(data: data)
88
- end
89
-
90
- # @param json [Hash]
91
- # @param include_errors [Boolean]
92
- # @return [FmRest::Spyke::Metadata] the skeleton structure for a
93
- # Spyke-formatted response
94
- def build_base_hash(json, include_errors = false)
95
- {
96
- metadata: Metadata.new(
97
- prepare_messages(json),
98
- prepare_script_results(json),
99
- prepare_data_info(json)
100
- ).freeze,
101
- errors: include_errors ? prepare_errors(json) : {}
102
- }
103
- end
104
-
105
- # @param json [Hash]
106
- # @return [Array<OpenStruct>] the skeleton structure for a
107
- # Spyke-formatted response
108
- def prepare_messages(json)
109
- return [] unless json[:messages]
110
- json[:messages].map { |m| OpenStruct.new(m).freeze }.freeze
111
- end
112
-
113
- # @param json [Hash]
114
- # @return [OpenStruct] the script(s) execution results for Spyke metadata
115
- # format
116
- def prepare_script_results(json)
117
- results = {}
118
-
119
- [:prerequest, :presort].each do |s|
120
- if json[:response][:"scriptError.#{s}"]
121
- results[s] = OpenStruct.new(
122
- result: json[:response][:"scriptResult.#{s}"],
123
- error: json[:response][:"scriptError.#{s}"]
124
- ).freeze
125
- end
126
- end
127
-
128
- if json[:response][:scriptError]
129
- results[:after] = OpenStruct.new(
130
- result: json[:response][:scriptResult],
131
- error: json[:response][:scriptError]
132
- ).freeze
133
- end
134
-
135
- results.present? ? OpenStruct.new(results).freeze : nil
136
- end
137
-
138
- # @param json [Hash]
139
- # @return [OpenStruct] the script(s) execution results for
140
- # Spyke metadata format
141
- def prepare_data_info(json)
142
- data_info = json[:response] && json[:response][:dataInfo]
143
-
144
- return nil unless data_info.present?
145
-
146
- DataInfo.new(data_info).freeze
147
- end
148
-
149
- # @param json [Hash]
150
- # @return [Hash] the errors hash in Spyke format
151
- def prepare_errors(json)
152
- # Code 0 means "No Error"
153
- # https://fmhelp.filemaker.com/help/17/fmp/en/index.html#page/FMP_Help/error-codes.html
154
- return {} if json[:messages][0][:code].to_i == 0
155
-
156
- json[:messages].each_with_object(base: []) do |message, hash|
157
- # Only include validation errors
158
- next unless VALIDATION_ERROR_RANGE.include?(message[:code].to_i)
159
-
160
- hash[:base] << "#{message[:message]} (#{message[:code]})"
161
- end
162
- end
163
-
164
- # `json_data` is expected in this format:
165
- #
166
- # {
167
- # "fieldData": {
168
- # "fieldName1" : "fieldValue1",
169
- # "fieldName2" : "fieldValue2",
170
- # ...
171
- # },
172
- # "portalData": {
173
- # "portal1" : [
174
- # { <portalRecord1> },
175
- # { <portalRecord2> },
176
- # ...
177
- # ],
178
- # "portal2" : [
179
- # { <portalRecord1> },
180
- # { <portalRecord2> },
181
- # ...
182
- # ]
183
- # },
184
- # "modId": <Id_for_last_modification>,
185
- # "recordId": <Unique_internal_ID_for_this_record>
186
- # }
187
- #
188
- # @param json_data [Hash]
189
- # @return [Hash] the record data in Spyke format
190
- def prepare_record_data(json_data)
191
- out = { id: json_data[:recordId].to_i, mod_id: json_data[:modId] }
192
- out.merge!(json_data[:fieldData])
193
- out.merge!(prepare_portal_data(json_data[:portalData])) if json_data[:portalData]
194
- out
195
- end
196
-
197
- # Extracts `recordId` and strips the `"PortalName::"` field prefix for each
198
- # portal
199
- #
200
- # Sample `json_portal_data`:
201
- #
202
- # "portalData": {
203
- # "Orders":[
204
- # { "Orders::DeliveryDate": "3/7/2017", "recordId": "23" }
205
- # ]
206
- # }
207
- #
208
- # @param json_portal_data [Hash]
209
- # @return [Hash] the portal data in Spyke format
210
- def prepare_portal_data(json_portal_data)
211
- json_portal_data.each_with_object({}) do |(portal_name, portal_records), out|
212
- portal_options = @model.portal_options[portal_name.to_s] || {}
213
-
214
- out[portal_name] =
215
- portal_records.map do |portal_fields|
216
- attributes = { id: portal_fields[:recordId].to_i }
217
- attributes[:mod_id] = portal_fields[:modId] if portal_fields[:modId]
218
-
219
- prefix = portal_options[:attribute_prefix] || portal_name
220
- prefix_matcher = /\A#{prefix}::/
221
-
222
- portal_fields.each do |k, v|
223
- next if :recordId == k || :modId == k
224
- attributes[k.to_s.gsub(prefix_matcher, "").to_sym] = v
225
- end
226
-
227
- attributes
228
- end
229
- end
230
- end
231
-
232
- # @param env [Faraday::Env]
233
- # @return [Boolean]
234
- def single_record_request?(env)
235
- env.method == :get && env.url.path.match(SINGLE_RECORD_RE)
236
- end
237
-
238
- # (see #single_record_request?)
239
- def multiple_records_request?(env)
240
- env.method == :get && env.url.path.match(MULTIPLE_RECORDS_RE)
241
- end
242
-
243
- # (see #single_record_request?)
244
- def find_request?(env)
245
- env.method == :post && env.url.path.match(FIND_RECORDS_RE)
246
- end
247
-
248
- # (see #single_record_request?)
249
- def update_request?(env)
250
- env.method == :patch && env.url.path.match(SINGLE_RECORD_RE)
251
- end
252
-
253
- # (see #single_record_request?)
254
- def create_request?(env)
255
- env.method == :post && env.url.path.match(MULTIPLE_RECORDS_RE)
256
- end
257
-
258
- # (see #single_record_request?)
259
- def container_upload_request?(env)
260
- env.method == :post && env.url.path.match(CONTAINER_RE)
261
- end
262
-
263
- # (see #single_record_request?)
264
- def delete_request?(env)
265
- env.method == :delete && env.url.path.match(SINGLE_RECORD_RE)
266
- end
267
-
268
- def execute_script_request?(env)
269
- env.method == :get && env.url.path.match(SCRIPT_REQUEST_RE)
270
- end
271
- end
272
- end
273
- end