├── lib ├── appetizer.rb └── appetizer │ ├── tasks │ ├── init.rake │ ├── console.rake │ └── test │ │ └── test.rake │ ├── init.rb │ ├── rack.rb │ ├── console.rb │ ├── events.rb │ ├── rake.rb │ ├── rack │ └── splash.rb │ ├── test.rb │ ├── populator.rb │ └── setup.rb ├── Rakefile ├── Gemfile ├── .gitignore ├── appetizer.gemspec ├── test └── appetizer │ └── populator_test.rb └── README.md /lib/appetizer.rb: -------------------------------------------------------------------------------- 1 | require "appetizer/setup" 2 | -------------------------------------------------------------------------------- /lib/appetizer/tasks/init.rake: -------------------------------------------------------------------------------- 1 | task(:init) { App.init! } 2 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env rake 2 | require "bundler/gem_tasks" 3 | -------------------------------------------------------------------------------- /lib/appetizer/init.rb: -------------------------------------------------------------------------------- 1 | require "appetizer/setup" 2 | 3 | App.init! 4 | -------------------------------------------------------------------------------- /lib/appetizer/rack.rb: -------------------------------------------------------------------------------- 1 | require "appetizer/init" 2 | require "appetizer/rack/splash" 3 | -------------------------------------------------------------------------------- /lib/appetizer/console.rb: -------------------------------------------------------------------------------- 1 | def reload! 2 | exec "irb -r bundler/setup -r appetizer/setup" 3 | end 4 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'http://rubygems.org' 2 | 3 | # Specify your gem's dependencies in appetizer.gemspec 4 | gemspec 5 | -------------------------------------------------------------------------------- /lib/appetizer/tasks/console.rake: -------------------------------------------------------------------------------- 1 | desc "A REPL, run `reload!` to reload." 2 | task :console do 3 | require "appetizer/console" 4 | reload! 5 | end 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.rbc 3 | .bundle 4 | .config 5 | .yardoc 6 | Gemfile.lock 7 | InstalledFiles 8 | _yardoc 9 | coverage 10 | doc/ 11 | lib/bundler/man 12 | pkg 13 | rdoc 14 | spec/reports 15 | test/tmp 16 | test/version_tmp 17 | tmp 18 | -------------------------------------------------------------------------------- /lib/appetizer/tasks/test/test.rake: -------------------------------------------------------------------------------- 1 | desc "Run tests." 2 | task :test do 3 | cmd = [ 4 | "ruby", 5 | "-I", "lib:test", 6 | "-e", "Dir['test/**/*_test.rb'].each { |f| load f }" 7 | ] 8 | 9 | exec *cmd 10 | end 11 | 12 | task :default => :test 13 | -------------------------------------------------------------------------------- /lib/appetizer/events.rb: -------------------------------------------------------------------------------- 1 | module Appetizer 2 | module Events 3 | def fire event 4 | hooks[event].each { |h| h.call } 5 | end 6 | 7 | def hooks 8 | @hooks ||= Hash.new { |h, k| h[k] = [] } 9 | end 10 | 11 | def on event, &block 12 | hooks[event] << block 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/appetizer/rake.rb: -------------------------------------------------------------------------------- 1 | # Entry point for Rakefiles. 2 | 3 | require "appetizer/setup" 4 | 5 | # Tasks from Appetizer. Only the first level, since other requires 6 | # (like appetizer/rake/test) have their own tasks in subdirs. 7 | 8 | here = File.expand_path "..", __FILE__ 9 | Dir["#{here}/tasks/*.rake"].sort.each { |f| App.load f } 10 | 11 | # Load test tasks if the app appears to use tests. 12 | 13 | if File.directory?("test") and !ENV["APPETIZER_NO_TESTS"] 14 | Dir["#{here}/tasks/test/*.rake"].sort.each { |f| App.load f } 15 | end 16 | 17 | # Tasks from the app itself. 18 | 19 | Dir["lib/tasks/**/*.rake"].sort.each { |f| App.load f } 20 | -------------------------------------------------------------------------------- /appetizer.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | 3 | Gem::Specification.new do |gem| 4 | gem.authors = ["Audiosocket"] 5 | gem.email = ["tech@audiosocket.com"] 6 | gem.description = "A lightweight init process for Rack apps." 7 | gem.summary = "Provides Railsy environments and initializers." 8 | gem.homepage = "https://github.com/audiosocket/appetizer" 9 | 10 | gem.files = `git ls-files`.split("\n") 11 | gem.test_files = `git ls-files -- test/*`.split("\n") 12 | gem.name = "appetizer" 13 | gem.require_paths = ["lib"] 14 | gem.version = "0.2.0" 15 | 16 | gem.required_ruby_version = ">= 1.9.2" 17 | end 18 | -------------------------------------------------------------------------------- /lib/appetizer/rack/splash.rb: -------------------------------------------------------------------------------- 1 | module Appetizer 2 | module Rack 3 | class Splash 4 | def initialize root = "public", glob = "**/*", ¬found 5 | notfound ||= lambda { |env| 6 | [404, { "Content-Type" => "text/plain" }, []] 7 | } 8 | 9 | urls = Dir[File.join root, glob].sort. 10 | select { |f| File.file? f }. 11 | map { |f| f[root.length..-1] } 12 | 13 | @static = ::Rack::Static.new notfound, root: root, urls: urls 14 | end 15 | 16 | def call env 17 | if env["PATH_INFO"] == "/" 18 | env["PATH_INFO"] = "/index.html" 19 | end 20 | 21 | @static.call env 22 | end 23 | 24 | def self.call env 25 | new.call env 26 | end 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /lib/appetizer/test.rb: -------------------------------------------------------------------------------- 1 | ENV["RACK_ENV"] = ENV["RAILS_ENV"] = "test" 2 | 3 | require "minitest/autorun" 4 | 5 | module Appetizer 6 | class Test < MiniTest::Unit::TestCase 7 | def setup 8 | self.class.setups.each { |s| instance_eval(&s) } 9 | end 10 | 11 | def teardown 12 | self.class.teardowns.each { |t| instance_eval(&t) } 13 | end 14 | 15 | def self.setup &block 16 | setups << block 17 | end 18 | 19 | def self.setups 20 | @setups ||= (ancestors - [self]). 21 | map { |a| a.respond_to?(:setups) && a.setups }. 22 | select { |s| s }. 23 | compact.flatten.reverse 24 | end 25 | 26 | def self.teardown &block 27 | teardowns.unshift block 28 | end 29 | 30 | def self.teardowns 31 | @teardowns ||= (ancestors - [self]). 32 | map { |a| a.respond_to?(:teardowns) && a.teardowns }. 33 | select { |t| t}. 34 | compact.flatten 35 | end 36 | 37 | def self.test name, &block 38 | define_method "test #{name}", &block 39 | end 40 | end 41 | end 42 | 43 | require "appetizer/init" 44 | -------------------------------------------------------------------------------- /test/appetizer/populator_test.rb: -------------------------------------------------------------------------------- 1 | require "appetizer/test" 2 | require "appetizer/populator" 3 | 4 | class Appetizer::PopulatorTest < Appetizer::Test 5 | def test_initialize 6 | p = Appetizer::Populator.new :target, :source 7 | 8 | assert_equal :target, p.target 9 | assert_equal :source, p.source 10 | end 11 | 12 | def test_nested 13 | t2 = mock { expects(:bar=).with "baz" } 14 | 15 | t = mock do 16 | expects(:foo).returns t2 17 | end 18 | 19 | Appetizer::Populator.new t, foo: { bar: "baz" } do |p| 20 | p.nested :foo do |p| 21 | p.set :bar 22 | end 23 | end 24 | end 25 | 26 | def test_populate_target 27 | t2 = mock { expects(:bar=).with "baz" } 28 | t = mock { expects(:foo).never } 29 | 30 | Appetizer::Populator.new t, foo: { bar: "baz" } do |p| 31 | p.populate :foo, t2 do |p| 32 | p.set :bar 33 | end 34 | end 35 | end 36 | 37 | def test_set 38 | t = mock { expects(:foo=).with("bar").twice } 39 | 40 | Appetizer::Populator.new t, foo: "bar" do |p| 41 | p.set :foo 42 | p.set "foo" 43 | end 44 | end 45 | 46 | def test_set_missing_source_value 47 | t = mock { expects(:foo=).never } 48 | 49 | Appetizer::Populator.new t, Hash.new do |p| 50 | p.set :foo 51 | end 52 | end 53 | 54 | def test_set_nil_source_value 55 | t = mock { expects(:foo=).never } 56 | 57 | Appetizer::Populator.new t, foo: nil do |p| 58 | p.set :foo 59 | end 60 | end 61 | 62 | def test_set_empty_source_value 63 | t = mock { expects(:foo=).never } 64 | 65 | Appetizer::Populator.new t, foo: "" do |p| 66 | p.set :foo 67 | end 68 | end 69 | 70 | def test_set_with_block 71 | t = mock { expects(:foo=).with "BAR" } 72 | 73 | Appetizer::Populator.new t, foo: "bar" do |p| 74 | p.set(:foo) { |v| p.target.foo = v.upcase } 75 | end 76 | end 77 | end 78 | -------------------------------------------------------------------------------- /lib/appetizer/populator.rb: -------------------------------------------------------------------------------- 1 | module Appetizer 2 | 3 | # Helps assign values from a low-level structure to a rich domain 4 | # model. Most useful as an explicit alternative to ActiveRecord's 5 | # mass-assignment, with an AR object as the target and a params hash 6 | # as the source. 7 | 8 | class Populator 9 | attr_reader :target 10 | attr_reader :source 11 | 12 | def initialize target, source, &block 13 | @target = target 14 | @source = source 15 | 16 | yield self if block_given? 17 | end 18 | 19 | def nested key, target = nil, &block 20 | source = self.source[key] || self.source[key.intern] 21 | target ||= self.target.send key 22 | 23 | Populator.new target, source, &block if source 24 | end 25 | 26 | def set key, value = nil 27 | value ||= source[key] || source[key.intern] 28 | 29 | return if value.nil? || (value.respond_to?(:empty?) && value.empty?) 30 | set! key, value 31 | end 32 | 33 | def set! key, value, &block 34 | block ? block[value] : target.send("#{key}=", value) 35 | end 36 | 37 | module Helpers 38 | 39 | # Call `ctor.new` to create a new model object, then populate, 40 | # save, and JSONify as in `update`. 41 | 42 | def create ctor, &block 43 | obj = populate ctor.new, &block 44 | obj.save! 45 | 46 | halt 201, json(obj) 47 | end 48 | 49 | # Use a populator to assign values from `params` to `obj`, 50 | # returning it when finished. `&block` is passed a populator 51 | # instance. 52 | 53 | def populate obj, &block 54 | Populator.new(obj, params, &block).target 55 | end 56 | 57 | # Populate (see `populate`) an `obj` with `params` data, saving 58 | # when finished. Returns JSON for `obj`. 59 | 60 | def update obj, &block 61 | populate(obj, &block).save! 62 | json obj 63 | end 64 | end 65 | end 66 | end 67 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Appetizer 2 | 3 | A lightweight init process for Rack apps. 4 | 5 | ## Assumptions 6 | 7 | * Running Ruby 1.9.2 or better. 8 | * Using Bundler. 9 | * Logging in all envs except `test` is to `STDOUT`. Logging in `test` 10 | is to `tmp/test.log`. 11 | * Default internal and external encodings are `Encoding::UTF_8`. 12 | 13 | ## Load/Init Lifecycle 14 | 15 | 0. Optionally `require "appetizer/{rack,rake}"`, which will 16 | 1. `require "appetizer/setup"`, which will 17 | 2. `load "config/env.local.rb"` if it exists, then 18 | 3. `load "config/env.rb"` if **it** exists. 19 | 4. `load "config/env/#{App.env}.rb"` if **it** exists, then 20 | 5. App code is loaded, but not initialized. 21 | 6. `App.init!` is called. Happens automatically if step 1 occurred. 22 | 7. Fire the `initializing` event. 23 | 8. `load "config/init.rb"` if it exists. 24 | 9. `load "config/{init/**/*.rb"`, then 25 | 10. `load "app/models/**/*.rb"` if it exists. 26 | 11. Fire the `initialized` event. 27 | 28 | ## License (MIT) 29 | 30 | Copyright 2011 Audiosocket (tech@audiosocket.com) 31 | 32 | Permission is hereby granted, free of charge, to any person obtaining 33 | a copy of this software and associated documentation files (the 34 | 'Software'), to deal in the Software without restriction, including 35 | without limitation the rights to use, copy, modify, merge, publish, 36 | distribute, sublicense, and/or sell copies of the Software, and to 37 | permit persons to whom the Software is furnished to do so, subject to 38 | the following conditions: 39 | 40 | The above copyright notice and this permission notice shall be 41 | included in all copies or substantial portions of the Software. 42 | 43 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 44 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 45 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 46 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 47 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 48 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 49 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 50 | -------------------------------------------------------------------------------- /lib/appetizer/setup.rb: -------------------------------------------------------------------------------- 1 | $:.unshift File.expand_path "lib" 2 | 3 | require "appetizer/events" 4 | require "fileutils" 5 | require "logger" 6 | 7 | Encoding.default_external = Encoding::UTF_8 8 | Encoding.default_internal = Encoding::UTF_8 9 | 10 | # Make sure tmp exists, a bunch of things may use it. 11 | 12 | FileUtils.mkdir_p "tmp" 13 | 14 | module App 15 | extend Appetizer::Events 16 | 17 | def self.env 18 | (ENV["RACK_ENV"] || ENV["RAILS_ENV"] || "development").intern 19 | end 20 | 21 | def self.development? 22 | :development == env 23 | end 24 | 25 | def self.init! 26 | return true if defined?(@initialized) && @initialized 27 | 28 | load "config/init.rb" if File.exists? "config/init.rb" 29 | 30 | fire :initializing 31 | 32 | Dir["config/init/**/*.rb"].sort.each { |f| load f } 33 | 34 | # If the app has an app/models directory, autorequire 'em. 35 | 36 | if File.directory? "app/models" 37 | $:.unshift File.expand_path "app/models" 38 | Dir["app/models/**/*.rb"].sort.each { |f| require f[11..-4] } 39 | end 40 | 41 | fire :initialized 42 | 43 | @initialized = true 44 | end 45 | 46 | def self.load file 47 | now = Time.now.to_f if ENV["TRACE"] 48 | Kernel.load file 49 | p :load => { file => (Time.now.to_f - now) } if ENV["TRACE"] 50 | end 51 | 52 | def self.log 53 | @log ||= Logger.new test? ? "tmp/test.log" : $stdout 54 | end 55 | 56 | def self.production? 57 | :production == env 58 | end 59 | 60 | def self.require file 61 | now = Time.now.to_f if ENV["TRACE"] 62 | Kernel.require file 63 | p :require => { file => (Time.now.to_f - now) } if ENV["TRACE"] 64 | end 65 | 66 | def self.test? 67 | :test == env 68 | end 69 | end 70 | 71 | # Set default log formatter and level. WARN for production, INFO 72 | # otherwise. Override severity with the `LOG_LEVEL` env 73 | # var. Formatter just prefixes with severity. 74 | 75 | App.log.formatter = lambda do |severity, time, program, message| 76 | "[#{severity}] #{message}\n" 77 | end 78 | 79 | App.log.level = ENV["LOG_LEVEL"] ? 80 | Logger.const_get(ENV["LOG_LEVEL"].upcase) : 81 | App.production? ? Logger::WARN : Logger::INFO 82 | 83 | def (App.log).write message 84 | self << message 85 | end 86 | 87 | # Load the global env files. 88 | 89 | App.load "config/env.local.rb" if File.exists? "config/env.local.rb" 90 | App.load "config/env.rb" if File.exists? "config/env.rb" 91 | 92 | # Load the env-specific file. 93 | 94 | envfile = "config/env/#{App.env}.rb" 95 | load envfile if File.exists? envfile 96 | 97 | if defined? IRB 98 | IRB.conf[:PROMPT_MODE] = :SIMPLE 99 | 100 | App.require "appetizer/console" 101 | App.init! 102 | end 103 | --------------------------------------------------------------------------------