snails 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: fb169efa64f0924773fdb6ecb80d4ee3d97eedf7
4
+ data.tar.gz: 55a634a3eca80533eb19ff052cfde946bff87ee4
5
+ SHA512:
6
+ metadata.gz: 44dd9f9eb766b6f422f7ba845d38a4f447461787d4eb4beaf5799d7db6c04eb90b34a59e75e6ae39c7741d3bce80dd3022d6002c49aca7da58733da8b98f0685
7
+ data.tar.gz: 74f232515e3c4f7c3e8d787cf09c54100fdefab6eab84e113941b0729d23ab71c1877439cdfdbeedad96a4ea8910c88a2dce37c9a6e414cc535e87ef8a42006d
data/.gitignore ADDED
@@ -0,0 +1,3 @@
1
+ pkg
2
+ test
3
+ example/Gemfile.lock
data/Rakefile ADDED
@@ -0,0 +1,2 @@
1
+ require 'bundler'
2
+ Bundler::GemHelper.install_tasks
data/example/Gemfile ADDED
@@ -0,0 +1,5 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gem 'puma'
4
+ gem 'snails', path: '..'
5
+ gem 'rake'
data/example/Rakefile ADDED
@@ -0,0 +1,9 @@
1
+ require 'bundler/setup'
2
+ require 'snails'
3
+
4
+ class MyApp < Snails::App
5
+ # register Snails::Assets
6
+ # register Snails::Database
7
+ end
8
+
9
+ require 'snails/tasks'
data/example/config.ru ADDED
@@ -0,0 +1,12 @@
1
+ require 'bundler/setup'
2
+ require 'snails'
3
+
4
+ class MyApp < Snails::App
5
+ set :session_secret, 'foobar'
6
+
7
+ get '/' do
8
+ 'Hello world!'
9
+ end
10
+ end
11
+
12
+ run MyApp
@@ -0,0 +1,20 @@
1
+ require 'snails'
2
+
3
+ if defined?(Sinatra::ActiveRecordHelper)
4
+ require 'sinatra/activerecord/rake'
5
+ end
6
+
7
+ namespace :assets do
8
+
9
+ desc 'Precompiles assets'
10
+ task :precompile do
11
+ if Snails.apps.empty?
12
+ puts "No apps defined."
13
+ else
14
+ Snails.apps.each do |app|
15
+ Snails::Assets::Tasks.precompile_for(app)
16
+ end
17
+ end
18
+ end
19
+
20
+ end
data/lib/snails.rb ADDED
@@ -0,0 +1,418 @@
1
+ %w(
2
+ logger
3
+ active_support/string_inquirer
4
+ active_support/core_ext/hash
5
+ sinatra/base
6
+ sinatra/content_for
7
+ sinatra/flash
8
+ ).each { |lib| require lib }
9
+
10
+ module Snails
11
+ def self.env
12
+ @env ||= ActiveSupport::StringInquirer.new(ENV['RACK_ENV'] || ENV['RAILS_ENV'] || 'development')
13
+ end
14
+
15
+ def self.apps
16
+ @apps ||= []
17
+ end
18
+
19
+ def self.app
20
+ puts "Warning: There's more than one Snail app defined!" if @apps.count > 1
21
+ @apps.first
22
+ end
23
+ end
24
+
25
+ module Snails
26
+
27
+ module RequiredParams
28
+ def requires!(req, hash = params)
29
+ if req.is_a?(Hash)
30
+ req.each do |k, vals|
31
+ if vals.is_a?(Array) or vals.is_a?(Hash)
32
+ halt(400, "Missing: #{k} in #{hash}") if hash[k].nil?
33
+ requires!(vals, hash[k])
34
+ else
35
+ requires!(k, hash)
36
+ end
37
+ end
38
+ elsif req.nil? or (req.is_a?(Symbol) and hash[req].nil?) \
39
+ or (req.is_a?(Array) and req.any? { |p| hash[p].nil? })
40
+ halt(400, "Required parameters: #{req} (in #{hash})")
41
+ end
42
+ end
43
+ end
44
+
45
+ class App < Sinatra::Base
46
+
47
+ def self.inherited(base)
48
+ Snails.apps << base
49
+ super
50
+ end
51
+
52
+ LOGGER = Logger.new(File.exist?("#{Dir.pwd}/log") ? "#{Dir.pwd}/log/#{Snails.env}.log" : nil)
53
+
54
+ cwd = Pathname.new(Dir.pwd) # settings.root
55
+ set :protection, except: :frame_options
56
+ set :views, cwd.join('lib', 'views')
57
+ set :session_secret, ENV.fetch('SESSION_SECRET') { SecureRandom.hex }
58
+ enable :sessions
59
+ enable :method_override
60
+ enable :logging
61
+
62
+ register Sinatra::Flash
63
+ use Rack::CommonLogger, LOGGER
64
+ use Rack::Static, urls: %w(/css /img /js /files /fonts favicon.ico), root: 'public'
65
+
66
+ configure :production, :staging do
67
+ set :raise_errors, true
68
+ set :dump_errors, false
69
+ end
70
+
71
+ configure :development do
72
+ set :raise_errors, true
73
+ set :show_exceptions, true
74
+ set :log_level, Logger::DEBUG
75
+ end
76
+
77
+ helpers do
78
+ include RequiredParams
79
+ include Sinatra::ContentFor
80
+ def logger; LOGGER; end
81
+ end
82
+
83
+ error do
84
+ err = request.env['sinatra.error']
85
+ logger.error err.message
86
+ logger.error err.backtrace.first(3).join("\n")
87
+ halt(500, err.message)
88
+ end
89
+
90
+ not_found do
91
+ show_error(404)
92
+ end
93
+
94
+ protected
95
+
96
+ def deliver(data, code = 200, format = :json)
97
+ status(code)
98
+ content_type(format)
99
+ data.public_send("to_#{format}")
100
+ end
101
+
102
+ def show_error(code)
103
+ erb :"errors/#{code}", :layout => false
104
+ end
105
+
106
+ end
107
+
108
+ module Database
109
+
110
+ def self.registered(app)
111
+ require 'sinatra/activerecord'
112
+
113
+ app.register Sinatra::ActiveRecordExtension
114
+
115
+ # app.configure :development do
116
+ # ActiveRecord::Base.logger.level = Logger::DEBUG
117
+ # end
118
+ end
119
+
120
+ end
121
+
122
+ module Locales
123
+
124
+ def self.registered(app)
125
+ require 'i18n'
126
+ require 'i18n/backend/fallbacks'
127
+
128
+ cwd = Pathname.new(Dir.pwd)
129
+ app.set :locale, :es
130
+ app.set :locales_path, cwd.join('config', 'locales')
131
+
132
+ app.helpers do
133
+ def t(key); I18n.t(key); end
134
+ end
135
+
136
+ app.configure do
137
+ I18n::Backend::Simple.send(:include, I18n::Backend::Fallbacks)
138
+ I18n.load_path = Dir[File.join(app.settings.locales_path, '*.yml')]
139
+ I18n.enforce_available_locales = false
140
+ I18n.backend.load_translations
141
+ end
142
+
143
+ app.before do
144
+ I18n.locale = app.settings.locale
145
+ end
146
+ end
147
+
148
+ end
149
+
150
+ # usage:
151
+
152
+ # class App < Snails::App
153
+ # register Snails::Assets
154
+ #
155
+ # # optional:
156
+ # set :assets_precompile, %w(js/app.js css/styles.css)
157
+ # # also optional, set compressor
158
+ # sprockets.css_compressor = :csso
159
+ # sprockets.js_compressor = :uglifier
160
+ # end
161
+
162
+ # Then, in your view:
163
+ #
164
+ # <script src="/assets/js/app.js"></script>
165
+ # <link rel="stylesheet" href="/assets/css/styles.css" />
166
+ #
167
+
168
+ module Assets
169
+
170
+ def self.registered(app)
171
+ require 'sprockets-helpers'
172
+
173
+ cwd = Pathname.new(Dir.pwd)
174
+ app.set :sprockets, Sprockets::Environment.new(cwd)
175
+ app.set :assets_prefix, '/assets' # URL
176
+ app.set :digest_assets, false
177
+ app.set :assets_public_path, -> { cwd.join('public', 'assets') } # output dir
178
+ app.set :assets_paths, %w(assets) # source files
179
+ app.set :assets_precompile, %w(js/main.js css/main.css)
180
+
181
+ app.configure do
182
+ app.assets_paths.each do |path|
183
+ app.sprockets.append_path cwd.join(path)
184
+ end
185
+ end
186
+
187
+ app.configure :production, :staging do
188
+ # app.sprockets.css_compressor = :sass
189
+ # app.sprockets.js_compressor = :uglifier
190
+ end
191
+
192
+ app.configure :development do
193
+ # allow asset requests to pass
194
+ # app.allow_paths.push /^#{app.assets_prefix}(\/\w+)?\/([\w\.-]+)/
195
+
196
+ # and serve them
197
+ app.get "#{app.assets_prefix}/*" do |path|
198
+ env_sprockets = request.env.dup
199
+ env_sprockets['PATH_INFO'] = path
200
+ app.sprockets.call(env_sprockets)
201
+ end
202
+ end
203
+
204
+ app.helpers do
205
+ def asset_path(filename)
206
+ file = manifest[filename] or raise "Not found in manifest: #{filename}"
207
+ [settings.assets_prefix, file].join('/')
208
+ end
209
+
210
+ if Snails.env.production?
211
+ def manifest
212
+ @manifest ||= read_manifest
213
+ end
214
+ else
215
+ def manifest
216
+ read_manifest
217
+ end
218
+ end
219
+
220
+ def read_manifest
221
+ file = Dir[settings.assets_public_path + '/.*.json'].first or raise "No manifest found at #{path}"
222
+ JSON.parse(IO.read(file))['assets']
223
+ end
224
+ end
225
+ end
226
+
227
+ module Tasks
228
+
229
+ def self.precompile_for(app)
230
+ unless app.respond_to?(:assets_public_path)
231
+ return puts "#{app.name} doesn't have the Asset module included."
232
+ end
233
+
234
+ puts "Precompiling #{app.name} assets to #{app.assets_public_path}..."
235
+ FileUtils.remove_dir(app.assets_public_path.to_s, true)
236
+
237
+ environment = app.sprockets
238
+ manifest = ::Sprockets::Manifest.new(environment.index, app.assets_public_path)
239
+ manifest.compile(app.assets_precompile)
240
+
241
+ # unless app.development?
242
+ # files = Dir[app.assets_public_path.to_s + '/*/*']
243
+ files = `find #{app.assets_public_path}`.split("\n").select { |f| f[/\.(js|css)/] }
244
+ remove_digests(files)
245
+ # end
246
+ end
247
+
248
+ private
249
+
250
+ def self.remove_digests(files)
251
+ puts "Removing digests from #{files.length} files..."
252
+ files.each do |file|
253
+ dir = File.dirname(file)
254
+ parts = File.basename(file).split(/-|\./)
255
+ if !parts[1] or parts[1].length < 10
256
+ # puts "This doesn't look like a digested file: #{file}. Skipping..."
257
+ next
258
+ end
259
+
260
+ dest = File.join(dir, "#{parts.first}.#{parts.last}").sub('.gz', '.' + parts.last(2).join('.'))
261
+ FileUtils.mv(file, dest)
262
+ puts " --> #{dest}"
263
+ end
264
+ end
265
+ end
266
+
267
+ end
268
+
269
+ module ViewHelpers
270
+
271
+ def action
272
+ request.path_info.gsub('/','').blank? ? 'home' : request.path_info.gsub('/',' ')
273
+ end
274
+
275
+ def partial(name, opts = {})
276
+ partial_name = name.to_s["/"] ? name.to_s.reverse.sub("/", "_/").reverse : "_#{name}"
277
+ erb(partial_name.to_sym, { layout: false }.merge(opts))
278
+ end
279
+
280
+ def view(view_name, opts = {})
281
+ layout = request.xhr? ? false : true
282
+ erb(view_name.to_sym, { layout: layout }.merge(opts))
283
+ end
284
+
285
+ #########################################
286
+ # formatting
287
+
288
+ def tag(name, options = nil, open = false)
289
+ attributes = tag_attributes(options)
290
+ "<#{name}#{attributes}#{open ? '>' : ' />'}"
291
+ end
292
+
293
+ def tag_attributes(options)
294
+ return '' unless options
295
+ options.inject('') do |all,(key,value)|
296
+ next all unless value
297
+ all << ' ' if all.empty?
298
+ all << %(#{key}="#{value}" )
299
+ end.chomp!(' ')
300
+ end
301
+
302
+ def simple_format(text, options = {})
303
+ t = options.delete(:tag) || :p
304
+ start_tag = tag(t, options, true)
305
+ text = text.to_s.dup
306
+ text.gsub!(/\r\n?/, "\n") # \r\n and \r -> \n
307
+ text.gsub!(/\n\n+/, "</#{t}>\n\n#{start_tag}") # 2+ newline -> paragraph
308
+ text.gsub!(/([^\n]\n)(?=[^\n])/, '\1<br />') # 1 newline -> br
309
+ text.insert 0, start_tag
310
+ text << "</#{t}>"
311
+ text
312
+ end
313
+
314
+ #########################################
315
+ # forms
316
+
317
+ def form_input(object, field, options = {})
318
+ id, name, index, label = input_base(object, field, options)
319
+ type = (options[:type] || :text).to_sym
320
+ value = options[:value] ? "value='#{options[:value]}'" : (type == :password ? '' : "value='#{object.send(field)}'")
321
+
322
+ classes = object.errors[field].any? ? 'has-errors' : ''
323
+ label + raw_input(index, type, id, name, value, options[:placeholder], classes, options[:required])
324
+ end
325
+
326
+ def form_password(object, field, options = {})
327
+ form_input(object, field, {:type => 'password'}.merge(options))
328
+ end
329
+
330
+ def form_checkbox(object, field, options = {})
331
+ id, name, index, label = input_base(object, field, options)
332
+
333
+ type = options[:type] || :checkbox
334
+ value = options[:value] ? "value='#{options[:value]}'" : ''
335
+
336
+ if type.to_sym == :radio
337
+ checked = object.send(field) == options[:value]
338
+ value += checked ? " selected='selected'" : ''
339
+ else
340
+ checked = object.send(field)
341
+ value += checked ? " checked='true'" : ''
342
+ end
343
+
344
+ label + raw_input(index, type, id, name, options[:placeholder], value)
345
+ end
346
+
347
+ def form_textarea(object, field, options = {})
348
+ id, name, index, label = input_base(object, field, options)
349
+ style = options[:style] ? "style='#{options[:style]}'" : ''
350
+ label + "<textarea #{style} tabindex='#{index}' id='#{id}' name='#{name}'>#{object.send(field)}</textarea>"
351
+ end
352
+
353
+ def form_select_options(list, selected = nil)
354
+ list.map do |name, val|
355
+ val = name if val.nil?
356
+ sel = val == selected ? 'selected="selected"' : ''
357
+ "<option #{sel} value='#{val}'>#{name}</option>"
358
+ end.join("\n")
359
+ end
360
+
361
+ def form_put
362
+ '<input type="hidden" name="_method" value="put" />'
363
+ end
364
+
365
+ def form_submit(text = 'Actualizar', classes = '')
366
+ @tabindex = @tabindex ? @tabindex + 1 : 1
367
+ "<button tabindex='#{@tabindex}' class='primary #{classes}' type='submit'>#{text}</button>"
368
+ end
369
+
370
+ def post_button(text, path, opts = {})
371
+ form_id = opts.delete(:form_id) || path.gsub(/[^a-z0-9]/, '_').gsub(/_+/, '_')
372
+ css_class = opts.delete(:css_class) || ''
373
+ input_html = opts.delete(:input_html) || ''
374
+ confirm_text = opts.delete(:confirm_text) || 'Seguro?'
375
+ submit_val = opts.delete(:value) || text
376
+ '<form id="' + form_id + '" style="display:inline" method="post" action="' + url(path) + '" onsubmit="return confirm(\'' + confirm_text + '\');">
377
+ ' + input_html + '
378
+ <button name="submit" type="submit" class="' + css_class + ' button" value="' + submit_val + '">' + text + '</button>
379
+ </form>'
380
+ end
381
+
382
+ def delete_link(options = {})
383
+ post_button(options[:text], options[:path], options.merge({
384
+ input_html: "<input type='hidden' name='_method' value='delete' />",
385
+ css_class: 'danger'
386
+ }))
387
+ end
388
+
389
+ protected
390
+
391
+ def input_base(object, field, options)
392
+ label = options[:label] || field.to_s.gsub('_', ' ').capitalize
393
+ example = options[:example] ? "<small class='example'>#{options[:example]}</small>" : ''
394
+
395
+ id = "#{get_model_name(object).downcase}_#{options[:key] || field}"
396
+ name = "#{get_model_name(object).downcase}[#{field}]"
397
+ label = options[:label] == false ? '' : "<label for='#{id}'>#{label}#{example}</label>\n"
398
+
399
+ @tabindex = @tabindex ? @tabindex + 1 : 1
400
+ return id, name, @tabindex, label
401
+ end
402
+
403
+ def raw_input(index, type, id, name, value, placeholder = '', classes = '', required = false)
404
+ req = required ? "required" : ''
405
+ "<input #{req} class='#{classes}' tabindex='#{index}' type='#{type}' id='#{id}' name='#{name}' placeholder='#{placeholder}' #{value} />"
406
+ end
407
+
408
+ def get_model_name(obj)
409
+ obj.respond_to?(:field_name) ? obj.field_name : get_class_name(obj.class)
410
+ end
411
+
412
+ def get_class_name(klass)
413
+ klass.model_name.to_s.split("::").last
414
+ end
415
+
416
+ end
417
+
418
+ end
data/snails.gemspec ADDED
@@ -0,0 +1,27 @@
1
+ # -*- encoding: utf-8 -*-
2
+ # require File.expand_path("../lib/version", __FILE__)
3
+
4
+ Gem::Specification.new do |s|
5
+ s.name = "snails"
6
+ s.version = '0.0.1'
7
+ s.platform = Gem::Platform::RUBY
8
+ s.authors = ['Tomás Pollak']
9
+ s.email = ['tomas@forkhq.com']
10
+ # s.homepage = "https://github.com/tomas/snails"
11
+ s.summary = "A simple-and-nimble version of Rails."
12
+ s.description = "A simple-and-nimble version of Rails."
13
+
14
+ s.required_rubygems_version = ">= 1.3.6"
15
+ s.add_development_dependency "bundler", ">= 1.0.0"
16
+ s.add_development_dependency "rspec", '~> 3.0', '>= 3.0.0'
17
+
18
+ s.add_runtime_dependency "i18n", ">= 1.0.1"
19
+ s.add_runtime_dependency "sinatra-contrib", ">= 2.0.3"
20
+ s.add_runtime_dependency "sinatra-activerecord", ">= 2.0.13"
21
+ s.add_runtime_dependency "sinatra-flash", ">= 0.3.0"
22
+ s.add_runtime_dependency "sprockets-helpers", ">= 1.2"
23
+
24
+ s.files = `git ls-files`.split("\n")
25
+ s.executables = `git ls-files`.split("\n").map{|f| f =~ /^bin\/(.*)/ ? $1 : nil}.compact
26
+ s.require_path = 'lib'
27
+ end
metadata ADDED
@@ -0,0 +1,155 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: snails
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Tomás Pollak
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2018-11-06 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: 1.0.0
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: 1.0.0
27
+ - !ruby/object:Gem::Dependency
28
+ name: rspec
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '3.0'
34
+ - - ">="
35
+ - !ruby/object:Gem::Version
36
+ version: 3.0.0
37
+ type: :development
38
+ prerelease: false
39
+ version_requirements: !ruby/object:Gem::Requirement
40
+ requirements:
41
+ - - "~>"
42
+ - !ruby/object:Gem::Version
43
+ version: '3.0'
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: 3.0.0
47
+ - !ruby/object:Gem::Dependency
48
+ name: i18n
49
+ requirement: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: 1.0.1
54
+ type: :runtime
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ version: 1.0.1
61
+ - !ruby/object:Gem::Dependency
62
+ name: sinatra-contrib
63
+ requirement: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - ">="
66
+ - !ruby/object:Gem::Version
67
+ version: 2.0.3
68
+ type: :runtime
69
+ prerelease: false
70
+ version_requirements: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - ">="
73
+ - !ruby/object:Gem::Version
74
+ version: 2.0.3
75
+ - !ruby/object:Gem::Dependency
76
+ name: sinatra-activerecord
77
+ requirement: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - ">="
80
+ - !ruby/object:Gem::Version
81
+ version: 2.0.13
82
+ type: :runtime
83
+ prerelease: false
84
+ version_requirements: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - ">="
87
+ - !ruby/object:Gem::Version
88
+ version: 2.0.13
89
+ - !ruby/object:Gem::Dependency
90
+ name: sinatra-flash
91
+ requirement: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - ">="
94
+ - !ruby/object:Gem::Version
95
+ version: 0.3.0
96
+ type: :runtime
97
+ prerelease: false
98
+ version_requirements: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - ">="
101
+ - !ruby/object:Gem::Version
102
+ version: 0.3.0
103
+ - !ruby/object:Gem::Dependency
104
+ name: sprockets-helpers
105
+ requirement: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - ">="
108
+ - !ruby/object:Gem::Version
109
+ version: '1.2'
110
+ type: :runtime
111
+ prerelease: false
112
+ version_requirements: !ruby/object:Gem::Requirement
113
+ requirements:
114
+ - - ">="
115
+ - !ruby/object:Gem::Version
116
+ version: '1.2'
117
+ description: A simple-and-nimble version of Rails.
118
+ email:
119
+ - tomas@forkhq.com
120
+ executables: []
121
+ extensions: []
122
+ extra_rdoc_files: []
123
+ files:
124
+ - ".gitignore"
125
+ - Rakefile
126
+ - example/Gemfile
127
+ - example/Rakefile
128
+ - example/config.ru
129
+ - lib/snails.rb
130
+ - lib/snails/tasks.rb
131
+ - snails.gemspec
132
+ homepage:
133
+ licenses: []
134
+ metadata: {}
135
+ post_install_message:
136
+ rdoc_options: []
137
+ require_paths:
138
+ - lib
139
+ required_ruby_version: !ruby/object:Gem::Requirement
140
+ requirements:
141
+ - - ">="
142
+ - !ruby/object:Gem::Version
143
+ version: '0'
144
+ required_rubygems_version: !ruby/object:Gem::Requirement
145
+ requirements:
146
+ - - ">="
147
+ - !ruby/object:Gem::Version
148
+ version: 1.3.6
149
+ requirements: []
150
+ rubyforge_project:
151
+ rubygems_version: 2.6.13
152
+ signing_key:
153
+ specification_version: 4
154
+ summary: A simple-and-nimble version of Rails.
155
+ test_files: []