httpimagestore 1.8.1 → 1.9.0

Sign up to get free protection for your applications and to get access to all the features.
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