httpimagestore 1.8.1 → 1.9.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 (64) hide show
  1. checksums.yaml +15 -0
  2. data/Gemfile +7 -7
  3. data/Gemfile.lock +20 -20
  4. data/README.md +165 -37
  5. data/Rakefile +7 -2
  6. data/VERSION +1 -1
  7. data/bin/httpimagestore +74 -41
  8. data/lib/httpimagestore/configuration/file.rb +20 -11
  9. data/lib/httpimagestore/configuration/handler.rb +96 -257
  10. data/lib/httpimagestore/configuration/handler/source_store_base.rb +37 -0
  11. data/lib/httpimagestore/configuration/handler/statement.rb +114 -0
  12. data/lib/httpimagestore/configuration/identify.rb +17 -9
  13. data/lib/httpimagestore/configuration/output.rb +33 -61
  14. data/lib/httpimagestore/configuration/path.rb +2 -2
  15. data/lib/httpimagestore/configuration/request_state.rb +131 -0
  16. data/lib/httpimagestore/configuration/s3.rb +41 -29
  17. data/lib/httpimagestore/configuration/thumbnailer.rb +189 -96
  18. data/lib/httpimagestore/configuration/validate_hmac.rb +170 -0
  19. data/lib/httpimagestore/error_reporter.rb +6 -1
  20. data/lib/httpimagestore/ruby_string_template.rb +10 -19
  21. metadata +40 -102
  22. data/.rspec +0 -1
  23. data/features/cache-control.feature +0 -41
  24. data/features/compatibility.feature +0 -165
  25. data/features/data-uri.feature +0 -55
  26. data/features/encoding.feature +0 -103
  27. data/features/error-reporting.feature +0 -281
  28. data/features/flexi.feature +0 -259
  29. data/features/health-check.feature +0 -29
  30. data/features/request-matching.feature +0 -211
  31. data/features/rewrite.feature +0 -122
  32. data/features/s3-store-and-thumbnail.feature +0 -82
  33. data/features/source-failover.feature +0 -71
  34. data/features/step_definitions/httpimagestore_steps.rb +0 -203
  35. data/features/storage.feature +0 -198
  36. data/features/support/env.rb +0 -116
  37. data/features/support/test-large.jpg +0 -0
  38. data/features/support/test.empty +0 -0
  39. data/features/support/test.jpg +0 -0
  40. data/features/support/test.png +0 -0
  41. data/features/support/test.txt +0 -1
  42. data/features/support/tiny.png +0 -0
  43. data/features/xid-forwarding.feature +0 -49
  44. data/httpimagestore.gemspec +0 -145
  45. data/load_test/load_test.1k.23a022f6e.m1.small-comp.csv +0 -3
  46. data/load_test/load_test.1k.ec9bde794.m1.small.csv +0 -4
  47. data/load_test/load_test.jmx +0 -317
  48. data/load_test/thumbnail_specs.csv +0 -11
  49. data/load_test/thumbnail_specs_v2.csv +0 -10
  50. data/spec/configuration_file_spec.rb +0 -333
  51. data/spec/configuration_handler_spec.rb +0 -255
  52. data/spec/configuration_identify_spec.rb +0 -67
  53. data/spec/configuration_output_spec.rb +0 -821
  54. data/spec/configuration_path_spec.rb +0 -138
  55. data/spec/configuration_s3_spec.rb +0 -911
  56. data/spec/configuration_source_failover_spec.rb +0 -101
  57. data/spec/configuration_spec.rb +0 -90
  58. data/spec/configuration_thumbnailer_spec.rb +0 -483
  59. data/spec/ruby_string_template_spec.rb +0 -61
  60. data/spec/spec_helper.rb +0 -89
  61. data/spec/support/compute.jpg +0 -0
  62. data/spec/support/cuba_response_env.rb +0 -40
  63. data/spec/support/full.cfg +0 -183
  64. data/spec/support/utf_string.txt +0 -1
data/Rakefile CHANGED
@@ -17,10 +17,15 @@ Jeweler::Tasks.new do |gem|
17
17
  gem.name = "httpimagestore"
18
18
  gem.homepage = "http://github.com/jpastuszek/httpimagestore"
19
19
  gem.license = "MIT"
20
- gem.summary = %Q{HTTP based image storage and thumbnailer}
21
- gem.description = %Q{Thumbnails images using httpthumbnailer and stored data on HTTP server (S3)}
20
+ gem.summary = %Q{HTTP API server for image thumbnailing and storage}
21
+ gem.description = %Q{Configurable S3 or file system image storage and processing HTTP API server. It is using HTTP Thumbnailer as image processing backend.}
22
22
  gem.email = "jpastuszek@gmail.com"
23
23
  gem.authors = ["Jakub Pastuszek"]
24
+ gem.files.exclude "features/**/*"
25
+ gem.files.exclude "gatling/**/*"
26
+ gem.files.exclude "spec/**/*"
27
+ gem.files.exclude "*.gemspec"
28
+ gem.files.exclude ".rspec"
24
29
  # dependencies defined in Gemfile
25
30
  end
26
31
  Jeweler::RubygemsDotOrgTasks.new
data/VERSION CHANGED
@@ -1 +1 @@
1
- 1.8.1
1
+ 1.9.0
@@ -10,7 +10,7 @@ Application.new('httpimagestore', port: 3000, processor_count_factor: 2) do
10
10
  argument :config,
11
11
  cast: Pathname,
12
12
  description: 'configuration file path'
13
- version (Pathname.new(__FILE__).dirname + '..' + 'VERSION').read
13
+ version((Pathname.new(__FILE__).dirname + '..' + 'VERSION').read)
14
14
  end
15
15
 
16
16
  settings do |settings|
@@ -21,6 +21,7 @@ Application.new('httpimagestore', port: 3000, processor_count_factor: 2) do
21
21
  require 'httpimagestore/error_reporter'
22
22
 
23
23
  class HTTPImageStore < Controller
24
+ include PerfStats
24
25
  extend Stats
25
26
  def_stats(
26
27
  :workers,
@@ -68,48 +69,79 @@ Application.new('httpimagestore', port: 3000, processor_count_factor: 2) do
68
69
 
69
70
  log.debug{"got request: #{env["REQUEST_METHOD"]} #{env["REQUEST_URI"]}"}
70
71
  env['app.configuration'].handlers.each do |handler|
71
- log.debug{"trying handler: #{handler.http_method}, #{handler.uri_matchers.join(', ')}"}
72
+ log.debug{"trying handler: #{handler}"}
72
73
  on eval(handler.http_method), *handler.uri_matchers.map{|m| instance_eval(&m.matcher)} do |*args|
73
- log.debug{"matched handler: #{handler.http_method}, #{handler.uri_matchers.join(', ')}"}
74
- # map and decode matched URI segments
75
- matches = {}
76
- names = handler.uri_matchers
77
- .map do |matcher|
78
- matcher.names
79
- end
80
- .flatten
81
-
82
- fail "matched more arguments than named (#{args.length} for #{names.length})" if args.length > names.length
83
- fail "matched less arguments than named (#{args.length} for #{names.length})" if args.length < names.length
84
-
85
- names.zip(args)
86
- .each do |name, value|
87
- fail "name should be a symbol" unless name.is_a? Symbol
88
- matches[name] = URI.utf_decode(value)
89
- end
90
-
91
- # decode remaining URI components
92
- path = (env["PATH_INFO"][1..-1] || '').split('/').map do |part|
93
- URI.utf_decode(part)
94
- end.join('/')
95
-
96
- # query string already decoded by Rack
97
- query_string = req.GET
98
-
99
- state = Configuration::RequestState.new(req.body.read, matches, path, query_string, memory_limit, env['xid'] || {})
100
-
101
- handler.sources.each do |source|
102
- source.realize(state) unless source.respond_to? :excluded? and source.excluded?(state)
103
- end
104
- handler.processors.each do |processor|
105
- processor.realize(state) unless processor.respond_to? :excluded? and processor.excluded?(state)
106
- end
107
- handler.stores.each do |store|
108
- store.realize(state) unless store.respond_to? :excluded? and store.excluded?(state)
74
+ log.debug{"matched handler: #{handler}"}
75
+ log.with_meta_context api_method: handler.http_method.upcase, api_handler: handler.to_s do
76
+ measure "handling request", handler.to_s do
77
+ # map and decode matched URI segments
78
+ matches = {}
79
+ names = handler.uri_matchers
80
+ .map do |matcher|
81
+ matcher.names
82
+ end
83
+ .flatten
84
+
85
+ fail "matched more arguments than named (#{args.length} for #{names.length})" if args.length > names.length
86
+ fail "matched less arguments than named (#{args.length} for #{names.length})" if args.length < names.length
87
+
88
+ names.zip(args)
89
+ .each do |name, value|
90
+ fail "name should be a symbol" unless name.is_a? Symbol
91
+ matches[name] = URI.utf_decode(value)
92
+ end
93
+
94
+ # decode remaining URI components
95
+ path = (env['PATH_INFO'][1..-1] || '').split('/').map do |part|
96
+ URI.utf_decode(part)
97
+ end.join('/')
98
+
99
+ # query string already decoded by Rack
100
+ query_string = req.GET
101
+
102
+ # actual request URI
103
+ request_uri = env['REQUEST_URI']
104
+ request_headers = env.select{|k,v| k.start_with? 'HTTP_'}.map do |pair|
105
+ [
106
+ pair[0].sub(/^HTTP_/, '').gsub('_', '-'),
107
+ pair[1]
108
+ ]
109
+ end
110
+ request_headers = Hash[request_headers]
111
+ request_headers.delete('VERSION')
112
+
113
+ body = measure "reading request body" do
114
+ req.body.read
115
+ end
116
+
117
+ state = Configuration::RequestState.new(body, matches, path, query_string, request_uri, request_headers, memory_limit, env['xid'] || {})
118
+
119
+ measure "validating request" do
120
+ handler.validators.each do |validator|
121
+ validator.realize(state) unless validator.respond_to? :excluded? and validator.excluded?(state)
122
+ end
123
+ end unless handler.validators.empty?
124
+ measure "sourcing images" do
125
+ handler.sources.each do |source|
126
+ source.realize(state) unless source.respond_to? :excluded? and source.excluded?(state)
127
+ end
128
+ end
129
+ measure "processing images" do
130
+ handler.processors.each do |processor|
131
+ processor.realize(state) unless processor.respond_to? :excluded? and processor.excluded?(state)
132
+ end
133
+ end unless handler.processors.empty?
134
+ measure "storing images" do
135
+ handler.stores.each do |store|
136
+ store.realize(state) unless store.respond_to? :excluded? and store.excluded?(state)
137
+ end
138
+ end unless handler.stores.empty?
139
+ measure "sending response" do
140
+ handler.output.realize(state)
141
+ instance_eval(&state.output_callback)
142
+ end
143
+ end
109
144
  end
110
- handler.output.realize(state)
111
-
112
- instance_eval &state.output_callback
113
145
  end
114
146
  end
115
147
 
@@ -144,6 +176,7 @@ Application.new('httpimagestore', port: 3000, processor_count_factor: 2) do
144
176
  require 'httpimagestore/configuration/file'
145
177
  require 'httpimagestore/configuration/output'
146
178
  require 'httpimagestore/configuration/s3'
179
+ require 'httpimagestore/configuration/validate_hmac'
147
180
 
148
181
  HTTPImageStore.use Configurator, Configuration.from_file(settings.config)
149
182
  HTTPImageStore
@@ -1,5 +1,5 @@
1
1
  require 'httpimagestore/configuration/path'
2
- require 'httpimagestore/configuration/handler'
2
+ require 'httpimagestore/configuration/handler/source_store_base'
3
3
  require 'httpimagestore/configuration/source_failover'
4
4
  require 'pathname'
5
5
  require 'addressable/uri'
@@ -18,6 +18,7 @@ module Configuration
18
18
  end
19
19
 
20
20
  class FileSourceStoreBase < SourceStoreBase
21
+ include PerfStats
21
22
  extend Stats
22
23
  def_stats(
23
24
  :total_file_store,
@@ -29,20 +30,24 @@ module Configuration
29
30
  def self.parse(configuration, node)
30
31
  image_name = node.grab_values('image name').first
31
32
  node.required_attributes('root', 'path')
32
- root_dir, path_spec, if_image_name_on = *node.grab_attributes('root', 'path', 'if-image-name-on')
33
- matcher = InclusionMatcher.new(image_name, if_image_name_on)
34
33
 
35
- self.new(
34
+ # TODO: it should be possible to compact that
35
+ root_dir, path_spec, remaining = *node.grab_attributes_with_remaining('root', 'path')
36
+ conditions, remaining = *ConditionalInclusion.grab_conditions_with_remaining(remaining)
37
+ remaining.empty? or raise UnexpectedAttributesError.new(node, remaining)
38
+
39
+ file = self.new(
36
40
  configuration.global,
37
41
  image_name,
38
- matcher,
39
42
  root_dir,
40
43
  path_spec
41
44
  )
45
+ file.with_conditions(conditions)
46
+ file
42
47
  end
43
48
 
44
- def initialize(global, image_name, matcher, root_dir, path_spec)
45
- super global, image_name, matcher, path_spec
49
+ def initialize(global, image_name, root_dir, path_spec)
50
+ super(global, image_name, path_spec)
46
51
  @root_dir = Pathname.new(root_dir).cleanpath
47
52
  end
48
53
 
@@ -83,9 +88,11 @@ module Configuration
83
88
 
84
89
  log.info "sourcing '#{image_name}' from file '#{storage_path}'"
85
90
  begin
86
- data = storage_path.open('rb') do |io|
87
- request_state.memory_limit.io io
88
- io.read
91
+ data = measure "sourcing image from file", image_name do
92
+ storage_path.open('rb') do |io|
93
+ request_state.memory_limit.io io
94
+ io.read
95
+ end
89
96
  end
90
97
  FileSourceStoreBase.stats.incr_total_file_source
91
98
  FileSourceStoreBase.stats.incr_total_file_source_bytes(data.bytesize)
@@ -120,7 +127,9 @@ module Configuration
120
127
  image.store_url = file_url(rendered_path)
121
128
 
122
129
  log.info "storing '#{image_name}' in file '#{storage_path}' (#{image.data.length} bytes)"
123
- storage_path.open('wb'){|io| io.write image.data}
130
+ measure "storing image in file", image_name do
131
+ storage_path.open('wb'){|io| io.write image.data}
132
+ end
124
133
  FileSourceStoreBase.stats.incr_total_file_store
125
134
  FileSourceStoreBase.stats.incr_total_file_store_bytes(image.data.bytesize)
126
135
  end
@@ -1,6 +1,5 @@
1
- require 'mime/types'
2
- require 'digest/sha2'
3
- require 'securerandom'
1
+ require 'httpimagestore/configuration/request_state'
2
+ require 'httpimagestore/ruby_string_template'
4
3
 
5
4
  module Configuration
6
5
  class ImageNotLoadedError < ConfigurationError
@@ -39,123 +38,6 @@ module Configuration
39
38
  end
40
39
  end
41
40
 
42
- class RequestState < Hash
43
- include ClassLogging
44
-
45
- class Images < Hash
46
- def initialize(memory_limit)
47
- @memory_limit = memory_limit
48
- super
49
- end
50
-
51
- def []=(name, image)
52
- if member?(name)
53
- @memory_limit.return fetch(name).data.bytesize
54
- end
55
- super
56
- end
57
-
58
- def [](name)
59
- fetch(name){|image_name| raise ImageNotLoadedError.new(image_name)}
60
- end
61
- end
62
-
63
- def initialize(body = '', matches = {}, path = '', query_string = {}, memory_limit = MemoryLimit.new, headers = {})
64
- super() do |request_state, name|
65
- # note that request_state may be different object when useing with_locals that creates duplicate
66
- request_state[name] = request_state.generate_meta_variable(name) or raise VariableNotDefinedError.new(name)
67
- end
68
-
69
- self[:path] = path
70
- merge! matches
71
- self[:query_string_options] = query_string.sort.map{|kv| kv.join(':')}.join(',')
72
-
73
- log.debug "processing request with body length: #{body.bytesize} bytes and variables: #{map{|k,v| "#{k}: '#{v}'"}.join(', ')}"
74
-
75
- @body = body
76
- @images = Images.new(memory_limit)
77
- @memory_limit = memory_limit
78
- @output_callback = nil
79
-
80
- @headers = headers
81
- end
82
-
83
- attr_reader :body
84
- attr_reader :images
85
- attr_reader :memory_limit
86
- attr_reader :headers
87
-
88
- def with_locals(*locals)
89
- locals = locals.reduce{|a, b| a.merge(b)}
90
- log.debug "using additional local variables: #{locals}"
91
- self.dup.merge!(locals)
92
- end
93
-
94
- def output(&callback)
95
- @output_callback = callback
96
- end
97
-
98
- def output_callback
99
- @output_callback or fail 'no output callback'
100
- end
101
-
102
- def fetch_base_variable(name, base_name)
103
- fetch(base_name, nil) or generate_meta_variable(base_name) or raise NoVariableToGenerateMetaVariableError.new(base_name, name)
104
- end
105
-
106
- def generate_meta_variable(name)
107
- log.debug "generating meta variable: #{name}"
108
- val = case name
109
- when :basename
110
- path = Pathname.new(fetch_base_variable(name, :path))
111
- path.basename(path.extname).to_s
112
- when :dirname
113
- Pathname.new(fetch_base_variable(name, :path)).dirname.to_s
114
- when :extension
115
- Pathname.new(fetch_base_variable(name, :path)).extname.delete('.')
116
- when :digest # deprecated
117
- @body.empty? and raise NoRequestBodyToGenerateMetaVariableError.new(name)
118
- Digest::SHA2.new.update(@body).to_s[0,16]
119
- when :input_digest
120
- @body.empty? and raise NoRequestBodyToGenerateMetaVariableError.new(name)
121
- Digest::SHA2.new.update(@body).to_s[0,16]
122
- when :input_sha256
123
- @body.empty? and raise NoRequestBodyToGenerateMetaVariableError.new(name)
124
- Digest::SHA2.new.update(@body).to_s
125
- when :input_image_width
126
- @images['input'].width or raise NoImageDataForVariableError.new('input', name)
127
- when :input_image_height
128
- @images['input'].height or raise NoImageDataForVariableError.new('input', name)
129
- when :input_image_mime_extension
130
- @images['input'].mime_extension or raise NoImageDataForVariableError.new('input', name)
131
- when :image_digest
132
- Digest::SHA2.new.update(@images[fetch_base_variable(name, :image_name)].data).to_s[0,16]
133
- when :image_sha256
134
- Digest::SHA2.new.update(@images[fetch_base_variable(name, :image_name)].data).to_s
135
- when :mimeextension # deprecated
136
- image_name = fetch_base_variable(name, :image_name)
137
- @images[image_name].mime_extension or raise NoImageDataForVariableError.new(image_name, name)
138
- when :image_mime_extension
139
- image_name = fetch_base_variable(name, :image_name)
140
- @images[image_name].mime_extension or raise NoImageDataForVariableError.new(image_name, name)
141
- when :image_width
142
- image_name = fetch_base_variable(name, :image_name)
143
- @images[image_name].width or raise NoImageDataForVariableError.new(image_name, name)
144
- when :image_height
145
- image_name = fetch_base_variable(name, :image_name)
146
- @images[image_name].height or raise NoImageDataForVariableError.new(image_name, name)
147
- when :uuid
148
- SecureRandom.uuid
149
- end
150
- if val
151
- log.debug "generated meta variable '#{name}': #{val}"
152
- else
153
- log.debug "could not generated meta variable '#{name}'"
154
- end
155
- val
156
- end
157
- end
158
-
159
41
  module ImageMetaData
160
42
  attr_accessor :source_path
161
43
  attr_accessor :source_url
@@ -163,9 +45,16 @@ module Configuration
163
45
  attr_accessor :store_url
164
46
 
165
47
  def mime_extension
166
- return nil unless mime_type
167
- mime = MIME::Types[mime_type].first
168
- mime.extensions.select{|e| e.length == 3}.first or mime.extensions.first
48
+ case mime_type
49
+ when nil then nil
50
+ when 'image/jpeg' then 'jpg'
51
+ when 'image/png' then 'png'
52
+ when 'image/gif' then 'gif'
53
+ else
54
+ # TODO: this does not work well; the resoult may not the most common extension (like 'jpeg')
55
+ mime = MIME::Types[mime_type].first or return nil
56
+ mime.preferred_extension
57
+ end
169
58
  end
170
59
  end
171
60
 
@@ -180,114 +69,6 @@ module Configuration
180
69
  end
181
70
  end
182
71
 
183
- class InclusionMatcher
184
- def initialize(value, template)
185
- @value = value
186
- @template = RubyStringTemplate.new(template) if template
187
- end
188
-
189
- def included?(request_state)
190
- return true if not @template
191
- @template.render(request_state).split(',').include? @value
192
- end
193
- end
194
-
195
- class HandlerStatement
196
- module ImageName
197
- attr_reader :image_name
198
-
199
- def initialize(global, *args)
200
- @image_name = args.pop
201
-
202
- super(global, *args)
203
-
204
- config_local :imagename, @image_name # deprecated
205
- config_local :image_name, @image_name
206
- end
207
- end
208
-
209
- module PathSpec
210
- attr_reader :path_spec
211
-
212
- def initialize(global, *args)
213
- @path_spec = args.pop
214
- super(global, *args)
215
- end
216
-
217
- def path_template
218
- @global.paths[@path_spec]
219
- end
220
- end
221
-
222
- module ConditionalInclusion
223
- def initialize(global, *args)
224
- @matchers = []
225
- matcher = args.pop
226
- @matchers << matcher if matcher
227
- super(global, *args)
228
- end
229
-
230
- def inclusion_matcher(matcher)
231
- @matchers << matcher
232
- end
233
-
234
- def included?(request_state)
235
- return true if @matchers.empty?
236
- @matchers.any? do |matcher|
237
- matcher.included?(request_state)
238
- end
239
- end
240
-
241
- def excluded?(request_state)
242
- not included? request_state
243
- end
244
- end
245
-
246
- def initialize(global, *args)
247
- @global = global
248
- @config_locals = {}
249
- @module_args = args
250
- end
251
-
252
- attr_reader :config_locals
253
- def config_local(name, value)
254
- @config_locals[name] = value
255
- end
256
-
257
- def path_template(path_spec)
258
- @global.paths[path_spec]
259
- end
260
- end
261
-
262
- class SourceStoreBase < HandlerStatement
263
- include ImageName
264
- include PathSpec
265
- include ConditionalInclusion
266
-
267
- def initialize(global, image_name, matcher, path_spec)
268
- super(global, image_name, path_spec, matcher)
269
- end
270
-
271
- private
272
-
273
- def put_sourced_named_image(request_state)
274
- rendered_path = path_template.render(request_state.with_locals(config_locals))
275
-
276
- image = yield @image_name, rendered_path
277
-
278
- image.source_path = rendered_path
279
- request_state.images[@image_name] = image
280
- end
281
-
282
- def get_named_image_for_storage(request_state)
283
- image = request_state.images[@image_name]
284
- rendered_path = path_template.render(request_state.with_locals(config_locals))
285
- image.store_path = rendered_path
286
-
287
- yield @image_name, image, rendered_path
288
- end
289
- end
290
-
291
72
  class Matcher
292
73
  def initialize(names, debug_type = '', debug_value = '', &matcher)
293
74
  @names = names
@@ -296,8 +77,10 @@ module Configuration
296
77
  @debug_value = case debug_value
297
78
  when Regexp
298
79
  "/#{debug_value.source}/"
80
+ when nil
81
+ nil
299
82
  else
300
- debug_value.inspect
83
+ debug_value.to_s
301
84
  end
302
85
  end
303
86
 
@@ -305,15 +88,38 @@ module Configuration
305
88
  attr_reader :matcher
306
89
 
307
90
  def to_s
308
- if @names.empty?
309
- "#{@debug_type}(#{@debug_value})"
91
+ if @debug_value
92
+ if @names.empty?
93
+ "#{@debug_type}(#{@debug_value})"
94
+ else
95
+ "#{@debug_type}(#{@names.join(',')} => #{@debug_value})"
96
+ end
310
97
  else
311
- "#{@debug_type}(#{@names.join(',')} => #{@debug_value})"
98
+ @debug_type
312
99
  end
313
100
  end
314
101
  end
315
102
 
316
103
  class Handler < Scope
104
+ class HandlerConfiguration
105
+ def initialize(global, http_method, uri_matchers)
106
+ @global = global
107
+ @http_method = http_method
108
+ @uri_matchers = uri_matchers
109
+ @validators = []
110
+ @sources = []
111
+ @processors = []
112
+ @stores = []
113
+ @output = nil
114
+ end
115
+
116
+ attr_accessor :global, :http_method, :uri_matchers, :validators, :sources, :processors, :stores, :output
117
+
118
+ def to_s
119
+ "#{@http_method} #{@uri_matchers.join(', ')}"
120
+ end
121
+ end
122
+
317
123
  def self.match(node)
318
124
  node.name == 'put' or
319
125
  node.name == 'post' or
@@ -325,20 +131,7 @@ module Configuration
325
131
  end
326
132
 
327
133
  def self.parse(configuration, node)
328
- handler_configuration =
329
- Struct.new(
330
- :global,
331
- :http_method,
332
- :uri_matchers,
333
- :sources,
334
- :processors,
335
- :stores,
336
- :output
337
- ).new
338
-
339
- handler_configuration.global = configuration
340
- handler_configuration.http_method = node.name
341
- handler_configuration.uri_matchers = node.values.map do |matcher|
134
+ uri_matchers = node.values.map do |matcher|
342
135
  case matcher
343
136
  # URI matchers
344
137
  when %r{^:([^/]+)/(.*)/$} # :foobar/.*/
@@ -381,31 +174,29 @@ module Configuration
381
174
  when /^\&([^=]+)=(.+)$/# ?foo=bar
382
175
  name = $1.to_sym
383
176
  value = $2
384
- Matcher.new([name], 'QueryKeyValue', "#{name}=#{value}") do
177
+ Matcher.new([name], 'QueryKeyValue', "#{value}") do
385
178
  ->{req.GET[name.to_s] && req.GET[name.to_s] == value && captures.push(req.GET[name.to_s])}
386
179
  end
387
180
  when /^\&:(.+)\?(.*)$/# &:foo?bar
388
181
  name = $1.to_sym
389
182
  default = $2
390
- Matcher.new([name], 'QueryKeyDefault', "#{name}=<key>|#{default}") do
183
+ Matcher.new([name], 'QueryKeyDefault', "<key>|#{default}") do
391
184
  ->{captures.push(req.GET[name.to_s] || default)}
392
185
  end
393
186
  when /^\&:(.+)$/# &:foo
394
187
  name = $1.to_sym
395
- Matcher.new([name], 'QueryKey', "#{name}=<key>") do
188
+ Matcher.new([name], 'QueryKey', "<key>") do
396
189
  ->{req.GET[name.to_s] && captures.push(req.GET[name.to_s])}
397
190
  end
398
191
  # Literal URI segment matcher
399
192
  else # foobar
400
- Matcher.new([], "Literal", matcher) do
193
+ Matcher.new([], matcher, nil) do
401
194
  Regexp.escape(matcher)
402
195
  end
403
196
  end
404
197
  end
405
- handler_configuration.sources = []
406
- handler_configuration.processors = []
407
- handler_configuration.stores = []
408
- handler_configuration.output = nil
198
+
199
+ handler_configuration = HandlerConfiguration.new(configuration, node.name, uri_matchers)
409
200
 
410
201
  node.grab_attributes
411
202
 
@@ -426,6 +217,54 @@ module Configuration
426
217
  end
427
218
  RequestState.logger = Global.logger_for(RequestState)
428
219
 
220
+ class OutputText < Scope
221
+ def self.match(node)
222
+ node.name == 'output_text'
223
+ end
224
+
225
+ def self.parse(configuration, node)
226
+ configuration.output and raise StatementCollisionError.new(node, 'output')
227
+ text = node.grab_values('text').first
228
+ status, cache_control = *node.grab_attributes('status', 'cache-control')
229
+ configuration.output = OutputText.new(text, status || 200, cache_control)
230
+ end
231
+
232
+ def initialize(text, status, cache_control)
233
+ @text = RubyStringTemplate.new(text || fail("no text?!"))
234
+ @status = status || 200
235
+ @cache_control = cache_control
236
+ end
237
+
238
+ def realize(request_state)
239
+ # make sure variables are available in request context
240
+ status = @status
241
+ text = @text.render(request_state)
242
+ cache_control = @cache_control
243
+ request_state.output do
244
+ res['Cache-Control'] = cache_control if cache_control
245
+ write_plain status.to_i, text.to_s
246
+ end
247
+ end
248
+ end
249
+
250
+ class OutputOK < OutputText
251
+ def self.match(node)
252
+ node.name == 'output_ok'
253
+ end
254
+
255
+ def self.parse(configuration, node)
256
+ configuration.output and raise StatementCollisionError.new(node, 'output')
257
+ cache_control = node.grab_attributes('cache-control').first
258
+ configuration.output = OutputOK.new(cache_control)
259
+ end
260
+
261
+ def initialize(cache_control = nil)
262
+ super 'OK', 200, cache_control
263
+ end
264
+ end
265
+
429
266
  Global.register_node_parser Handler
267
+ Handler::register_node_parser OutputText
268
+ Handler::register_node_parser OutputOK
430
269
  end
431
270