runo 0.1.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 (149) hide show
  1. data/LICENSE +19 -0
  2. data/README.rdoc +120 -0
  3. data/bin/runo +35 -0
  4. data/lib/_error.rb +14 -0
  5. data/lib/_field.rb +260 -0
  6. data/lib/_i18n.rb +141 -0
  7. data/lib/_parser.rb +243 -0
  8. data/lib/_path.rb +86 -0
  9. data/lib/_storage/_storage.rb +213 -0
  10. data/lib/_storage/file.rb +200 -0
  11. data/lib/_storage/sequel.rb +174 -0
  12. data/lib/_storage/temp.rb +73 -0
  13. data/lib/_widget/action_create.rb +23 -0
  14. data/lib/_widget/action_login.rb +22 -0
  15. data/lib/_widget/action_signup.rb +16 -0
  16. data/lib/_widget/action_update.rb +16 -0
  17. data/lib/_widget/crumb.rb +24 -0
  18. data/lib/_widget/done.rb +16 -0
  19. data/lib/_widget/login.rb +25 -0
  20. data/lib/_widget/me.rb +31 -0
  21. data/lib/_widget/message.rb +51 -0
  22. data/lib/_widget/navi.rb +88 -0
  23. data/lib/_widget/submit.rb +49 -0
  24. data/lib/_widget/view_ym.rb +77 -0
  25. data/lib/_workflow/_workflow.rb +89 -0
  26. data/lib/_workflow/attachment.rb +50 -0
  27. data/lib/_workflow/blog.rb +28 -0
  28. data/lib/_workflow/contact.rb +23 -0
  29. data/lib/_workflow/forum.rb +26 -0
  30. data/lib/_workflow/register.rb +39 -0
  31. data/lib/meta/_meta.rb +20 -0
  32. data/lib/meta/group.rb +19 -0
  33. data/lib/meta/id.rb +59 -0
  34. data/lib/meta/owner.rb +21 -0
  35. data/lib/meta/timestamp.rb +118 -0
  36. data/lib/runo.rb +396 -0
  37. data/lib/scalar/checkbox.rb +68 -0
  38. data/lib/scalar/file.rb +144 -0
  39. data/lib/scalar/img.rb +112 -0
  40. data/lib/scalar/password.rb +58 -0
  41. data/lib/scalar/radio.rb +47 -0
  42. data/lib/scalar/select.rb +47 -0
  43. data/lib/scalar/text.rb +38 -0
  44. data/lib/scalar/textarea.rb +35 -0
  45. data/lib/scalar/textarea_pre.rb +14 -0
  46. data/lib/scalar/textarea_wiki.rb +173 -0
  47. data/lib/set/_set.rb +195 -0
  48. data/lib/set/dynamic.rb +177 -0
  49. data/lib/set/static.rb +102 -0
  50. data/lib/set/static_folder.rb +96 -0
  51. data/locale/en/index.po +242 -0
  52. data/locale/index.pot +243 -0
  53. data/locale/ja/index.po +242 -0
  54. data/locale/lazy_parser.rb +54 -0
  55. data/skel/config.ru +27 -0
  56. data/skel/skin/_users/00000000_frank-avatar.jpg +0 -0
  57. data/skel/skin/_users/00000000_frank-avatar_small.jpg +0 -0
  58. data/skel/skin/_users/00000000_frank.yaml +12 -0
  59. data/skel/skin/_users/00000000_root-avatar.jpg +0 -0
  60. data/skel/skin/_users/00000000_root-avatar_small.jpg +0 -0
  61. data/skel/skin/_users/00000000_root.yaml +11 -0
  62. data/skel/skin/_users/css/users.css +21 -0
  63. data/skel/skin/_users/css/users.less +25 -0
  64. data/skel/skin/_users/done.html +42 -0
  65. data/skel/skin/_users/index.html +46 -0
  66. data/skel/skin/_users/index.yaml +3 -0
  67. data/skel/skin/_users/summary.html +40 -0
  68. data/skel/skin/css/base.css +93 -0
  69. data/skel/skin/css/base.less +139 -0
  70. data/skel/skin/css/coax.css +199 -0
  71. data/skel/skin/css/coax.less +244 -0
  72. data/skel/skin/examples/blog/20091214_0001.yaml +8 -0
  73. data/skel/skin/examples/blog/20100630_0001.yaml +8 -0
  74. data/skel/skin/examples/blog/20100630_0002.yaml +14 -0
  75. data/skel/skin/examples/blog/20100701_0001.yaml +8 -0
  76. data/skel/skin/examples/blog/20100701_0002-a-20100701_0001-f.jpg +0 -0
  77. data/skel/skin/examples/blog/20100701_0002-a-20100701_0001-f_small.jpg +0 -0
  78. data/skel/skin/examples/blog/20100701_0002.yaml +19 -0
  79. data/skel/skin/examples/blog/frank/20100701_0001.yaml +10 -0
  80. data/skel/skin/examples/blog/frank/index.yaml +4 -0
  81. data/skel/skin/examples/blog/index.html +51 -0
  82. data/skel/skin/examples/blog/rss.xml +18 -0
  83. data/skel/skin/examples/contact/20100701_0001-file.txt +1 -0
  84. data/skel/skin/examples/contact/20100701_0001.yaml +15 -0
  85. data/skel/skin/examples/contact/20100701_0002.yaml +8 -0
  86. data/skel/skin/examples/contact/20100701_0003.yaml +9 -0
  87. data/skel/skin/examples/contact/index.html +47 -0
  88. data/skel/skin/examples/contact/js/contact.js +13 -0
  89. data/skel/skin/examples/contact/summary.html +54 -0
  90. data/skel/skin/examples/forum/20100701_0001.yaml +41 -0
  91. data/skel/skin/examples/forum/20100701_0002.yaml +25 -0
  92. data/skel/skin/examples/forum/index.html +68 -0
  93. data/skel/skin/examples/forum/summary.html +47 -0
  94. data/skel/skin/examples/index.html +75 -0
  95. data/skel/skin/index.html +41 -0
  96. data/skel/skin/js/base.js +50 -0
  97. data/t/locale/de/index.po +19 -0
  98. data/t/locale/en-GB/index.po +25 -0
  99. data/t/locale/ja/index.po +30 -0
  100. data/t/skin/_users/00000000_test.yaml +3 -0
  101. data/t/skin/_users/index.html +13 -0
  102. data/t/skin/foo/20091120_0001.yaml +7 -0
  103. data/t/skin/foo/bar/20091120_0001.yaml +5 -0
  104. data/t/skin/foo/bar/index.yaml +5 -0
  105. data/t/skin/foo/baz/css/baz.css +1 -0
  106. data/t/skin/foo/css/foo.css +1 -0
  107. data/t/skin/foo/index.html +14 -0
  108. data/t/skin/foo/index.yaml +7 -0
  109. data/t/skin/foo/not_css/foo.css +1 -0
  110. data/t/skin/foo/sub-20100306_0001.yaml +3 -0
  111. data/t/skin/index.yaml +3 -0
  112. data/t/skin/t_attachment/index.html +13 -0
  113. data/t/skin/t_contact/done.html +6 -0
  114. data/t/skin/t_contact/index.html +9 -0
  115. data/t/skin/t_file/index.html +16 -0
  116. data/t/skin/t_img/index.html +14 -0
  117. data/t/skin/t_img/test.jpg +0 -0
  118. data/t/skin/t_select/index.html +9 -0
  119. data/t/skin/t_store/index.html +9 -0
  120. data/t/skin/t_summary/20100326_0001.yaml +3 -0
  121. data/t/skin/t_summary/create.html +9 -0
  122. data/t/skin/t_summary/index.html +9 -0
  123. data/t/skin/t_summary/summary.html +9 -0
  124. data/t/t.rb +27 -0
  125. data/t/test_checkbox.rb +273 -0
  126. data/t/test_field.rb +330 -0
  127. data/t/test_file.rb +900 -0
  128. data/t/test_id.rb +215 -0
  129. data/t/test_img.rb +328 -0
  130. data/t/test_meta.rb +57 -0
  131. data/t/test_parser.rb +1266 -0
  132. data/t/test_password.rb +188 -0
  133. data/t/test_radio.rb +226 -0
  134. data/t/test_role.rb +249 -0
  135. data/t/test_runo.rb +742 -0
  136. data/t/test_runo_call.rb +1286 -0
  137. data/t/test_runo_i18n.rb +318 -0
  138. data/t/test_select.rb +182 -0
  139. data/t/test_set_complex.rb +527 -0
  140. data/t/test_set_dynamic.rb +1504 -0
  141. data/t/test_set_folder.rb +515 -0
  142. data/t/test_set_permit.rb +246 -0
  143. data/t/test_set_static.rb +445 -0
  144. data/t/test_storage.rb +915 -0
  145. data/t/test_text.rb +125 -0
  146. data/t/test_textarea.rb +138 -0
  147. data/t/test_timestamp.rb +473 -0
  148. data/t/test_workflow.rb +367 -0
  149. metadata +345 -0
@@ -0,0 +1,118 @@
1
+ # encoding: UTF-8
2
+
3
+ # Author:: Akira FUNAI
4
+ # Copyright:: Copyright (c) 2009 Akira FUNAI
5
+
6
+ require 'time'
7
+
8
+ class Runo::Meta::Timestamp < Runo::Field
9
+
10
+ include Runo::Meta
11
+
12
+ REX_DATE = /\A(\d+).(\d+).(\d+)(?:[T\s](\d+):(\d+)(?::(\d+))?)?\z/
13
+
14
+ def initialize(meta = {})
15
+ meta[:size] = $&.to_i if meta[:tokens] && meta[:tokens].find {|t| t =~ /^\d+$/ }
16
+ meta[:can_edit] = true if meta[:tokens].to_a.include? 'can_edit'
17
+ meta[:can_update] = true if meta[:tokens].to_a.include? 'can_update'
18
+ super
19
+ end
20
+
21
+ def errors
22
+ if @date_str.nil?
23
+ []
24
+ elsif @date_str =~ REX_DATE
25
+ (Time.local($1, $2, $3, $4, $5, $6) rescue nil) ? [] : ['out of range']
26
+ else
27
+ ['wrong format']
28
+ end
29
+ end
30
+
31
+ private
32
+
33
+ def _g_default(arg)
34
+ _date val['published']
35
+ end
36
+ alias :_g_published :_g_default
37
+
38
+ def _g_rfc2822(arg)
39
+ val['published'].rfc2822
40
+ end
41
+
42
+ def _g_created(arg)
43
+ _date val['created']
44
+ end
45
+
46
+ def _g_updated(arg)
47
+ _date val['updated']
48
+ end
49
+
50
+ def _date(time)
51
+ time.is_a?(::Time) ? time.strftime(_ '%Y-%m-%dT%H:%M:%S') : 'n/a'
52
+ end
53
+
54
+ def _g_create(arg)
55
+ <<_html.chomp if my[:can_edit]
56
+ <span class="#{_g_class arg}"><input type="text" name="#{my[:short_name]}" value="" size="#{my[:size]}" /></span>
57
+ _html
58
+ end
59
+
60
+ def _g_update(arg)
61
+ if my[:can_edit]
62
+ v = @date_str
63
+ v ||= val['published'].is_a?(::Time) ? val['published'].strftime('%Y-%m-%d %H:%M:%S') : ''
64
+ <<_html.chomp
65
+ <span class="#{_g_class arg}"><input type="text" name="#{my[:short_name]}" value="#{Runo::Field.h v}" size="#{my[:size]}" />#{_g_errors arg}</span>
66
+ _html
67
+ elsif my[:can_update] && !find_ancestor {|f| f[:id] =~ Runo::REX::ID_NEW }
68
+ <<_html
69
+ <span class="#{_g_class arg}">
70
+ <input type="checkbox" id="timestamp_#{my[:short_name]}" name="#{my[:short_name]}" value="true" />
71
+ <label for="timestamp_#{my[:short_name]}">#{_ 'update the timestamp'}</label>
72
+ #{_g_errors arg}</span>
73
+ _html
74
+ end
75
+ end
76
+
77
+ def _post(action, v)
78
+ case action
79
+ when :load
80
+ @val = val_cast v
81
+ when :create
82
+ now = Time.now
83
+ @val = {
84
+ 'created' => now,
85
+ 'updated' => now,
86
+ 'published' => now,
87
+ }
88
+ if my[:can_edit] && v['published'].is_a?(::Time)
89
+ @val['published'] = v['published']
90
+ else
91
+ nil # do not set @action
92
+ end
93
+ when :update
94
+ @val['updated'] = Time.now
95
+ if my[:can_edit] && v['published'].is_a?(::Time)
96
+ @val['published'] = v['published']
97
+ elsif my[:can_update] && v['published'] == :same_as_updated
98
+ @val['published'] = @val['updated']
99
+ else
100
+ nil # do not set @action
101
+ end
102
+ end
103
+ end
104
+
105
+ def val_cast(v)
106
+ if v.is_a? ::Hash
107
+ v
108
+ elsif v == 'true'
109
+ {'published' => :same_as_updated}
110
+ elsif v.is_a?(::String) && !v.empty?
111
+ @date_str = v
112
+ (v =~ REX_DATE && t = (Time.local($1, $2, $3, $4, $5, $6) rescue nil)) ? {'published' => t} : {}
113
+ else
114
+ {}
115
+ end
116
+ end
117
+
118
+ end
@@ -0,0 +1,396 @@
1
+ # encoding: UTF-8
2
+
3
+ # Author:: Akira FUNAI
4
+ # Copyright:: Copyright (c) 2009 Akira FUNAI
5
+
6
+ class Runo
7
+
8
+ lib_dir = ::File.dirname __FILE__
9
+
10
+ require "#{lib_dir}/_i18n.rb"
11
+ I18n.bindtextdomain('index', ::File.expand_path('../locale', lib_dir))
12
+
13
+ Dir["#{lib_dir}/_*.rb"].sort.each {|file| require file }
14
+ Dir["#{lib_dir}/[a-z]*/*.rb"].sort.each {|file| require file }
15
+ Dir["#{lib_dir}/_*/*.rb"].sort.each {|file| require file }
16
+
17
+ module REX
18
+ ID_SHORT = /[a-z][a-z0-9]*/
19
+ ID = /^(\d{8})_(\d{4,}|#{ID_SHORT})/
20
+ ID_NEW = /^_\d/
21
+ COND = /^(.+?)=(.+)$/
22
+ COND_D = /^(19\d\d|2\d\d\d|9999)\d{0,4}$/
23
+ PATH_ID = /\/((?:19|2\d)\d{6})\/(\d+)/
24
+ TID = /\d{10}\.\d+/
25
+ DIR_STATIC = /css|js|img|imgs|image|images/
26
+ end
27
+
28
+ def self.config(config)
29
+ @config = config
30
+ end
31
+
32
+ def self.[](name)
33
+ @config ||= {}
34
+ @config[name]
35
+ end
36
+
37
+ def self.current
38
+ Thread.current
39
+ end
40
+
41
+ def self.session
42
+ self.current[:session] || ($fake_session ||= {})
43
+ end
44
+
45
+ def self.transaction
46
+ self.session[:transaction] ||= {}
47
+ end
48
+
49
+ def self.client
50
+ self.session[:client] ||= 'nobody'
51
+ end
52
+
53
+ def self.client=(id)
54
+ self.session[:client] = id
55
+ end
56
+
57
+ def self.token
58
+ self.session[:token] ||= rand(36 ** 32).to_s(36)
59
+ end
60
+
61
+ def self.base
62
+ self.current[:base]
63
+ end
64
+
65
+ def self.uri
66
+ self.current[:uri]
67
+ end
68
+
69
+ def self.libdir
70
+ ::File.dirname __FILE__
71
+ end
72
+
73
+ def self.static(env)
74
+ @static ||= Rack::Directory.new Runo['skin_dir']
75
+ response = @static.call env
76
+
77
+ if response.first == 404
78
+ until ::File.readable? ::File.join(
79
+ Runo['skin_dir'],
80
+ env['PATH_INFO'].sub(%r{(/#{Runo::REX::DIR_STATIC}/).*}, '\\1')
81
+ )
82
+ env['PATH_INFO'].sub!(%r{/[^/]+(?=/#{Runo::REX::DIR_STATIC}/)}, '') || break
83
+ end
84
+ @static.call env
85
+ else
86
+ response
87
+ end
88
+ end
89
+
90
+ def call(env)
91
+ uri = "#{env['rack.url_scheme']}://#{env['HTTP_HOST']}#{env['SCRIPT_NAME']}"
92
+ req = Rack::Request.new env
93
+ method = req.request_method.downcase
94
+ params = params_from_request req
95
+ path = req.path_info
96
+ tid = Runo::Path.tid_of path
97
+
98
+ static_path = ::File.expand_path path
99
+ return Runo.static(env) if (
100
+ static_path =~ %r{/#{Runo::REX::DIR_STATIC}/} &&
101
+ static_path !~ %r{/#{Runo::REX::TID}/} &&
102
+ static_path !~ %r{/#{Runo::REX::ID.to_s.sub('^','')}/}
103
+ )
104
+
105
+ Runo::I18n.lang = env['HTTP_ACCEPT_LANGUAGE']
106
+
107
+ Runo.current[:env] = env
108
+ Runo.current[:uri] = uri
109
+ Runo.current[:req] = req
110
+ Runo.current[:session] = env['rack.session']
111
+
112
+ if Runo.transaction[tid].is_a? Runo::Field
113
+ base = Runo.transaction[tid].item(Runo::Path.steps_of path.sub(/\A.*#{Runo::REX::TID}/, ''))
114
+ else
115
+ base = Runo::Path.base_of path
116
+ end
117
+ return response_not_found unless base
118
+
119
+ base[:tid] = tid
120
+ Runo.current[:base] = base
121
+
122
+ begin
123
+ if params[:action] == :logout && params[:token] == Runo.token
124
+ logout(base, params)
125
+ elsif method == 'get'
126
+ get(base, params)
127
+ elsif params[:action] == :login
128
+ login(base, params)
129
+ elsif params[:action] == :preview
130
+ preview(base, params)
131
+ elsif params[:token] != Runo.token
132
+ response_forbidden(:body => 'invalid token')
133
+ elsif Runo.transaction[tid] && !Runo.transaction[tid].is_a?(Runo::Field)
134
+ response_unprocessable_entity(:body => 'transaction expired')
135
+ else
136
+ begin
137
+ post(base, params)
138
+ rescue Runo::Error::Forbidden
139
+ response_forbidden
140
+ end
141
+ end
142
+ rescue Runo::Error::Forbidden
143
+ if params[:action] && Runo.client == 'nobody'
144
+ params[:dest_action] = (method == 'post') ? :index : params[:action]
145
+ params[:action] = :login
146
+ end
147
+ response_unprocessable_entity(:body => _get(base, params)) rescue response_forbidden
148
+ # TODO: rescue Error::System etc.
149
+ end
150
+ end
151
+
152
+ private
153
+
154
+ def login(base, params)
155
+ user = Runo::Set::Static::Folder.root.item('_users', 'main', params['id'].to_s)
156
+ if user && params['pw'].to_s.crypt(user.val('password')) == user.val('password')
157
+ Runo.client = params['id']
158
+ else
159
+ Runo.client = nil
160
+ raise Runo::Error::Forbidden
161
+ end
162
+ path = Runo::Path.path_of params[:conds]
163
+ action = (params['dest_action'] =~ /\A\w+\z/) ? params['dest_action'] : 'index'
164
+ response_see_other(
165
+ :location => "#{Runo.uri}#{base[:path]}/#{path}#{action}.html"
166
+ )
167
+ end
168
+
169
+ def logout(base, params)
170
+ Runo.client = nil
171
+ path = Runo::Path.path_of params[:conds]
172
+ response_see_other(
173
+ :location => "#{Runo.uri}#{base[:path]}/#{path}index.html"
174
+ )
175
+ end
176
+
177
+ def get(base, params)
178
+ if base.is_a? Runo::File
179
+ body = (params[:sub_action] == :small) ? base.thumbnail : base.body
180
+ response_ok(
181
+ :headers => {
182
+ 'Content-Type' => base.val['type'],
183
+ 'Content-Length' => body.to_s.size.to_s,
184
+ },
185
+ :body => body
186
+ )
187
+ else
188
+ response_ok :body => _get(base, params)
189
+ end
190
+ end
191
+
192
+ def preview(base, params)
193
+ Runo.transaction[base[:tid]] ||= base if base[:tid] =~ Runo::REX::TID
194
+
195
+ base.update params
196
+ if base.commit(:temp) || params[:sub_action] == :delete
197
+ id_step = result_step(base, params)
198
+ action = "preview_#{params[:sub_action]}"
199
+ response_see_other(
200
+ :location => "#{Runo.uri}/#{base[:tid]}/#{id_step}#{action}.html"
201
+ )
202
+ else
203
+ params = {:action => :update}
204
+ params[:conds] = {:id => base.errors.keys}
205
+ return response_unprocessable_entity(:body => _get(base, params))
206
+ end
207
+ end
208
+
209
+ def post(base, params)
210
+ Runo.transaction[base[:tid]] ||= base if base[:tid] =~ Runo::REX::TID
211
+
212
+ base.update params
213
+ if params[:status]
214
+ if base[:folder].commit :persistent
215
+ Runo.transaction[base[:tid]] = result_summary base
216
+ action = base.workflow.next_action base
217
+ id_step = result_step(base, params) if base[:parent] == base[:folder] && action != :done
218
+ response_see_other(
219
+ :location => "#{Runo.uri}/#{base[:tid]}#{base[:path]}/#{id_step}#{action}.html"
220
+ )
221
+ else
222
+ params = {:action => :update}
223
+ params[:conds] = {:id => base.errors.keys}
224
+ response_unprocessable_entity :body => _get(base, params)
225
+ end
226
+ else
227
+ base.commit :temp
228
+ id_step = result_step(base, params)
229
+ response_see_other(
230
+ :location => "#{Runo.uri}/#{base[:tid]}/#{id_step}update.html"
231
+ )
232
+ end
233
+ end
234
+
235
+ def result_summary(base)
236
+ (base.result || {}).values.inject({}) {|summary, item|
237
+ item_result = item.result.is_a?(::Symbol) ? item.result : :update
238
+ summary[item_result] = summary[item_result].to_i + 1
239
+ summary
240
+ }
241
+ end
242
+
243
+ def result_step(base, params)
244
+ if base.result
245
+ id = base.result.values.collect {|item| item[:id] }
246
+ else
247
+ id = params.keys.select {|id|
248
+ id.is_a?(::String) && (id[Runo::REX::ID] || id[Runo::REX::ID_NEW])
249
+ }
250
+ end
251
+ Runo::Path.path_of(:id => id)
252
+ end
253
+
254
+ def _get(f, params)
255
+ params[:action] ||= f.default_action
256
+ until f.is_a? Runo::Set::Static::Folder
257
+ params = {
258
+ :action => (f.default_action == :read) ? :read : nil,
259
+ :sub_action => f.send(:summary?, params) ? nil : (params[:sub_action] || :detail),
260
+ f[:id] => params,
261
+ }
262
+ params[:conds] = {:id => f[:id]} if f[:parent].is_a? Runo::Set::Dynamic
263
+ f = f[:parent]
264
+ end if f.is_a? Runo::Set::Dynamic
265
+
266
+ f.get params
267
+ end
268
+
269
+ def params_from_request(req)
270
+ params = {
271
+ :action => Runo::Path.action_of(req.path_info),
272
+ :sub_action => Runo::Path.sub_action_of(req.path_info),
273
+ }
274
+ params.merge!(rebuild_params req.params)
275
+
276
+ params[:conds] ||= {}
277
+ params[:conds].merge!(Runo::Path.conds_of req.path_info)
278
+
279
+ params
280
+ end
281
+
282
+ def rebuild_params(src)
283
+ src.each_key.sort.reverse.inject({}) {|params, key|
284
+ name, special = key.split('.', 2)
285
+ steps = name.split '-'
286
+
287
+ if special
288
+ special_id, special_val = special.split('-', 2)
289
+ else
290
+ item_id = steps.pop
291
+ end
292
+
293
+ hash = steps.inject(params) {|v, k| v[k] ||= {} }
294
+ val = src[key]
295
+
296
+ if special_id == 'action'
297
+ action, sub_action = (special_val || val).split('_', 2)
298
+ hash[:action] = action.intern
299
+ hash[:sub_action] = sub_action.intern if sub_action
300
+ elsif special_id == 'status'
301
+ hash[:status] = (special_val || val).intern
302
+ elsif special_id == 'conds'
303
+ hash[:conds] ||= {}
304
+ hash[:conds][special_val.intern] = val
305
+ elsif hash[item_id].is_a? ::Hash
306
+ hash[item_id][:self] = val
307
+ elsif item_id == '_token'
308
+ hash[:token] = val
309
+ else
310
+ hash[item_id] = val
311
+ end
312
+
313
+ params
314
+ }
315
+ end
316
+
317
+ def response_ok(result = {})
318
+ body = result[:body].to_s
319
+ return response_not_found(result) if body.empty?
320
+ [
321
+ 200,
322
+ (
323
+ result[:headers] ||
324
+ {
325
+ 'Content-Type' => 'text/html',
326
+ 'Content-Length' => body.size.to_s,
327
+ }
328
+ ),
329
+ body,
330
+ ]
331
+ end
332
+
333
+ def response_no_content(result = {})
334
+ [
335
+ 204,
336
+ (result[:headers] || {}),
337
+ []
338
+ ]
339
+ end
340
+
341
+ def response_see_other(result = {})
342
+ body = <<_html
343
+ <a href="#{result[:location]}">see other</a>
344
+ _html
345
+ [
346
+ 303,
347
+ {
348
+ 'Content-Type' => 'text/html',
349
+ 'Content-Length' => body.size.to_s,
350
+ 'Location' => result[:location],
351
+ },
352
+ body
353
+ ]
354
+ end
355
+
356
+ def response_forbidden(result = {})
357
+ body = result[:body] || 'Forbidden'
358
+ [
359
+ 403,
360
+ {
361
+ 'Content-Type' => 'text/html',
362
+ 'Content-Length' => body.size.to_s,
363
+ },
364
+ body,
365
+ ]
366
+ end
367
+
368
+ def response_not_found(result = {})
369
+ body = result[:body] || 'Not Found'
370
+ [
371
+ 404,
372
+ {
373
+ 'Content-Type' => 'text/html',
374
+ 'Content-Length' => body.size.to_s,
375
+ },
376
+ body
377
+ ]
378
+ end
379
+
380
+ def response_unprocessable_entity(result = {})
381
+ body = result[:body].to_s
382
+ return response_not_found(result) if body.empty?
383
+ [
384
+ 422,
385
+ (
386
+ result[:headers] ||
387
+ {
388
+ 'Content-Type' => 'text/html',
389
+ 'Content-Length' => body.size.to_s,
390
+ }
391
+ ),
392
+ body,
393
+ ]
394
+ end
395
+
396
+ end