Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

initial version of the gem + generator

  • Loading branch information...
commit a2008e61bf2509603bc880d19912c599cd0a3a59 1 parent b4bea30
@mattetti authored
Showing with 13,673 additions and 9 deletions.
  1. +5 −6 .gitignore
  2. +4 −0 Gemfile
  3. +22 −0 LICENSE
  4. +29 −3 README.md
  5. +2 −0  Rakefile
  6. +47 −0 bin/wd_sinatra
  7. +7 −0 lib/wd_sinatra.rb
  8. +129 −0 lib/wd_sinatra/app_loader.rb
  9. +112 −0 lib/wd_sinatra/sinatra_ext.rb
  10. +204 −0 lib/wd_sinatra/test_helpers.rb
  11. +5 −0 lib/wd_sinatra/version.rb
  12. +24 −0 templates/Gemfile
  13. +13 −0 templates/Guardfile
  14. +38 −0 templates/Rakefile
  15. +29 −0 templates/api/hello_world.rb
  16. +7 −0 templates/bin/console
  17. +5 −0 templates/config.ru
  18. +1 −0  templates/config/app.rb
  19. +3 −0  templates/config/environments/default.rb
  20. +2 −0  templates/config/environments/production.rb
  21. +3 −0  templates/config/environments/test.rb
  22. 0  templates/config/middleware.rb
  23. +34 −0 templates/lib/auth_helpers.rb
  24. +41 −0 templates/lib/tasks/doc.rake
  25. +4 −0 templates/lib/tasks/doc_generator/bootstrap/.gitignore
  26. +176 −0 templates/lib/tasks/doc_generator/bootstrap/LICENSE
  27. +47 −0 templates/lib/tasks/doc_generator/bootstrap/Makefile
  28. +105 −0 templates/lib/tasks/doc_generator/bootstrap/README.md
  29. +2,467 −0 templates/lib/tasks/doc_generator/bootstrap/bootstrap.css
  30. +356 −0 templates/lib/tasks/doc_generator/bootstrap/bootstrap.min.css
  31. +317 −0 templates/lib/tasks/doc_generator/bootstrap/docs/assets/css/docs.css
  32. BIN  templates/lib/tasks/doc_generator/bootstrap/docs/assets/ico/bootstrap-apple-114x114.png
  33. BIN  templates/lib/tasks/doc_generator/bootstrap/docs/assets/ico/bootstrap-apple-57x57.png
  34. BIN  templates/lib/tasks/doc_generator/bootstrap/docs/assets/ico/bootstrap-apple-72x72.png
  35. BIN  templates/lib/tasks/doc_generator/bootstrap/docs/assets/ico/favicon.ico
  36. BIN  templates/lib/tasks/doc_generator/bootstrap/docs/assets/img/bird.png
  37. BIN  templates/lib/tasks/doc_generator/bootstrap/docs/assets/img/browsers.png
  38. BIN  templates/lib/tasks/doc_generator/bootstrap/docs/assets/img/example-diagram-01.png
  39. BIN  templates/lib/tasks/doc_generator/bootstrap/docs/assets/img/example-diagram-02.png
  40. BIN  templates/lib/tasks/doc_generator/bootstrap/docs/assets/img/example-diagram-03.png
  41. BIN  templates/lib/tasks/doc_generator/bootstrap/docs/assets/img/grid-18px.png
  42. BIN  templates/lib/tasks/doc_generator/bootstrap/docs/assets/img/twitter-logo-no-bird.png
  43. +52 −0 templates/lib/tasks/doc_generator/bootstrap/docs/assets/js/application.js
  44. +94 −0 templates/lib/tasks/doc_generator/bootstrap/docs/assets/js/google-code-prettify/prettify.css
  45. +28 −0 templates/lib/tasks/doc_generator/bootstrap/docs/assets/js/google-code-prettify/prettify.js
  46. +2,037 −0 templates/lib/tasks/doc_generator/bootstrap/docs/index.html
  47. +798 −0 templates/lib/tasks/doc_generator/bootstrap/docs/javascript.html
  48. +119 −0 templates/lib/tasks/doc_generator/bootstrap/examples/container-app.html
  49. +122 −0 templates/lib/tasks/doc_generator/bootstrap/examples/fluid.html
  50. +79 −0 templates/lib/tasks/doc_generator/bootstrap/examples/hero.html
  51. +124 −0 templates/lib/tasks/doc_generator/bootstrap/js/bootstrap-alerts.js
  52. +64 −0 templates/lib/tasks/doc_generator/bootstrap/js/bootstrap-buttons.js
  53. +55 −0 templates/lib/tasks/doc_generator/bootstrap/js/bootstrap-dropdown.js
  54. +260 −0 templates/lib/tasks/doc_generator/bootstrap/js/bootstrap-modal.js
  55. +90 −0 templates/lib/tasks/doc_generator/bootstrap/js/bootstrap-popover.js
  56. +107 −0 templates/lib/tasks/doc_generator/bootstrap/js/bootstrap-scrollspy.js
  57. +80 −0 templates/lib/tasks/doc_generator/bootstrap/js/bootstrap-tabs.js
  58. +321 −0 templates/lib/tasks/doc_generator/bootstrap/js/bootstrap-twipsy.js
  59. +40 −0 templates/lib/tasks/doc_generator/bootstrap/js/tests/index.html
  60. +41 −0 templates/lib/tasks/doc_generator/bootstrap/js/tests/unit/bootstrap-alerts.js
  61. +42 −0 templates/lib/tasks/doc_generator/bootstrap/js/tests/unit/bootstrap-buttons.js
  62. +52 −0 templates/lib/tasks/doc_generator/bootstrap/js/tests/unit/bootstrap-dropdown.js
  63. +151 −0 templates/lib/tasks/doc_generator/bootstrap/js/tests/unit/bootstrap-modal.js
  64. +76 −0 templates/lib/tasks/doc_generator/bootstrap/js/tests/unit/bootstrap-popover.js
  65. +31 −0 templates/lib/tasks/doc_generator/bootstrap/js/tests/unit/bootstrap-scrollspy.js
  66. +77 −0 templates/lib/tasks/doc_generator/bootstrap/js/tests/unit/bootstrap-tabs.js
  67. +81 −0 templates/lib/tasks/doc_generator/bootstrap/js/tests/unit/bootstrap-twipsy.js
  68. +232 −0 templates/lib/tasks/doc_generator/bootstrap/js/tests/vendor/qunit.css
  69. +1,510 −0 templates/lib/tasks/doc_generator/bootstrap/js/tests/vendor/qunit.js
  70. +26 −0 templates/lib/tasks/doc_generator/bootstrap/lib/bootstrap.less
  71. +479 −0 templates/lib/tasks/doc_generator/bootstrap/lib/forms.less
  72. +222 −0 templates/lib/tasks/doc_generator/bootstrap/lib/mixins.less
  73. +1,060 −0 templates/lib/tasks/doc_generator/bootstrap/lib/patterns.less
  74. +141 −0 templates/lib/tasks/doc_generator/bootstrap/lib/reset.less
  75. +139 −0 templates/lib/tasks/doc_generator/bootstrap/lib/scaffolding.less
  76. +224 −0 templates/lib/tasks/doc_generator/bootstrap/lib/tables.less
  77. +187 −0 templates/lib/tasks/doc_generator/bootstrap/lib/type.less
  78. +60 −0 templates/lib/tasks/doc_generator/bootstrap/lib/variables.less
  79. +117 −0 templates/lib/tasks/doc_generator/template.erb
  80. BIN  templates/public/favicon.ico
  81. +17 −0 wd-sinatra.gemspec
View
11 .gitignore
@@ -2,8 +2,12 @@
*.rbc
.bundle
.config
-coverage
+.yardoc
+Gemfile.lock
InstalledFiles
+_yardoc
+coverage
+doc/
lib/bundler/man
pkg
rdoc
@@ -11,8 +15,3 @@ spec/reports
test/tmp
test/version_tmp
tmp
-
-# YARD artifacts
-.yardoc
-_yardoc
-doc/
View
4 Gemfile
@@ -0,0 +1,4 @@
+source 'https://rubygems.org'
+
+# Specify your gem's dependencies in wd-sinatra.gemspec
+gemspec
View
22 LICENSE
@@ -0,0 +1,22 @@
+Copyright (c) 2012 Matt Aimonetti
+
+MIT License
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
View
32 README.md
@@ -1,4 +1,30 @@
-wd-sinatra
-==========
+# Weasel Diesel Sinatra
-Weasel-Diesel Sinatra app gem, allowing you to generate/update sinatra apps using the Weasel Diesel DSL
+Weasel-Diesel Sinatra app gem, allowing you to generate/update sinatra apps using the Weasel Diesel DSL
+
+
+## Installation
+
+Add this line to your application's Gemfile:
+
+ gem 'wd-sinatra'
+
+And then execute:
+
+ $ bundle
+
+Or install it yourself as:
+
+ $ gem install wd-sinatra
+
+## Usage
+
+TODO: Write usage instructions here
+
+## Contributing
+
+1. Fork it
+2. Create your feature branch (`git checkout -b my-new-feature`)
+3. Commit your changes (`git commit -am 'Added some feature'`)
+4. Push to the branch (`git push origin my-new-feature`)
+5. Create new Pull Request
View
2  Rakefile
@@ -0,0 +1,2 @@
+#!/usr/bin/env rake
+require "bundler/gem_tasks"
View
47 bin/wd_sinatra
@@ -0,0 +1,47 @@
+#!/usr/bin/env ruby
+require "rubygems"
+require "thor/group"
+
+class WdSinatra < Thor::Group
+ include Thor::Actions
+
+ # Define arguments and options
+ # argument :type, :type => :string, :desc => "The type to generate, app by default"
+ argument :name, :type => :string, :desc => "The name of the app to generate"
+ class_option :test_framework, :default => :test_unit
+
+ def self.source_root
+ File.expand_path(File.join('..', 'templates'), File.dirname(__FILE__))
+ end
+
+ def create_lib_directory
+ directory "lib", "#{name}/lib"
+ end
+
+ def create_config_directory
+ directory "config", "#{name}/lib"
+ end
+
+ def create_bin_directory
+ directory "bin", "#{name}/bin"
+ chmod "#{name}/bin/console", 0755
+ end
+
+ def create_public_directory
+ directory "public", "#{name}/public"
+ end
+
+ def create_api_directory
+ directory "api", "#{name}/api"
+ end
+
+ def create_files
+ copy_file "Rakefile", "#{name}/Rakefile"
+ copy_file "Gemfile", "#{name}/Gemfile"
+ copy_file "config.ru", "#{name}/config.ru"
+ copy_file "Guardfile", "#{name}/Guardfile"
+ end
+
+end
+
+WdSinatra.start
View
7 lib/wd_sinatra.rb
@@ -0,0 +1,7 @@
+require "wd_sinatra/version"
+
+module WD
+ module Sinatra
+ # Your code goes here...
+ end
+end
View
129 lib/wd_sinatra/app_loader.rb
@@ -0,0 +1,129 @@
+if RUBY_VERSION =~ /1.8/
+ require 'rubygems'
+ require 'backports'
+end
+require 'bundler'
+Bundler.setup
+require 'logger'
+require 'weasel_diesel'
+
+module WDSinatra
+ module AppLoader
+ module_function
+
+ # Boot in server mode
+ def server(root_path)
+ @root = root_path
+ unless @booted
+ console
+ load_apis
+ load_middleware
+ set_sinatra_routes
+ set_sinatra_settings
+ end
+ end
+
+ # Boot in console mode
+ def console(root_path)
+ @root = root_path
+ unless @booted
+ set_env
+ load_environment
+ set_loadpath
+ load_lib_dependencies
+ #TODO: datastore_setup
+ load_models
+ @booted = true
+ end
+ end
+
+ def root_path
+ @root
+ end
+
+ # PRIVATE
+
+ # Sets the environment (RACK_ENV) based on some env variables.
+ def set_env
+ if !Object.const_defined?(:RACK_ENV)
+ ENV['RACK_ENV'] ||= ENV['RAILS_ENV'] || 'development'
+ Object.const_set(:RACK_ENV, ENV['RACK_ENV'])
+ end
+ puts "Running in #{RACK_ENV} mode" if RACK_ENV == 'development'
+ end
+
+
+ # Loads an environment specific config if available, the config file is where the logger should be set
+ # if it was not, we are using stdout.
+ def load_environment(env=RACK_ENV)
+ # Load the default which can be overwritten or extended by specific
+ # env config files.
+ require File.join(root_path, 'config', 'environments', 'default.rb')
+ env_file = File.join(root_path, "config", "environments", "#{env}.rb")
+ if File.exist?(env_file)
+ require env_file
+ else
+ debug_msg = "Environment file: #{env_file} couldn't be found, using only the default environment config instead." unless env == 'development'
+ end
+ # making sure we have a LOGGER constant defined.
+ unless Object.const_defined?(:LOGGER)
+ Object.const_set(:LOGGER, Logger.new($stdout))
+ end
+ LOGGER.debug(debug_msg) if debug_msg
+ require File.join(root_path, 'config', 'app')
+ end
+
+ def set_loadpath
+ $: << root_path
+ $: << File.join(root_path, 'lib')
+ $: << File.join(root_path, 'models')
+ end
+
+ def load_lib_dependencies
+ # WeaselDiesel is the web service DSL gem used to define services.
+ require 'weasel_diesel'
+ require 'sinatra'
+ require 'wd_sinatra/sinatra_ext'
+ # TODO: hook to custom app dependencies
+ end
+
+ def load_models
+ Dir.glob(File.join(root_path, "models", "**", "*.rb")).each do |model|
+ require model
+ end
+ end
+
+ # DSL routes are located in the api folder
+ def load_apis
+ Dir.glob(File.join(root_path, "api", "**", "*.rb")).each do |api|
+ require api
+ end
+ end
+
+ def set_sinatra_routes
+ WSList.all.sort.each{|api| api.load_sinatra_route }
+ end
+
+ def load_middleware
+ require File.join(root_path, 'config', 'middleware')
+ end
+
+ def set_sinatra_settings
+ # Using the :production env would wrap errors instead of displaying them
+ # like in dev mode
+ set :environment, RACK_ENV
+ set :root, ROOT
+ set :app_file, __FILE__
+ set :public_folder, File.join(root_path, "public")
+ # Checks on static files before dispatching calls
+ enable :static
+ # enable rack session
+ enable :session
+ set :raise_errors, false
+ # enable that option to run by calling this file automatically (without using the config.ru file)
+ # enable :run
+ use Rack::ContentLength
+ end
+
+ end
+end
View
112 lib/wd_sinatra/sinatra_ext.rb
@@ -0,0 +1,112 @@
+require 'forwardable'
+require 'params_verification'
+require 'json'
+
+class WeaselDiesel
+
+ class RequestHandler
+ extend Forwardable
+
+ # @return [WeaselDiesel] The service served by this controller
+ # @api public
+ attr_reader :service
+
+ # @return [Sinatra::Application]
+ # @api public
+ attr_reader :app
+
+ # @return [Hash]
+ # @api public
+ attr_reader :env
+
+ # @return [Sinatra::Request]
+ # @see http://rubydoc.info/github/sinatra/sinatra/Sinatra/Request
+ # @api public
+ attr_reader :request
+
+ # @return [Sinatra::Response]
+ # @see http://rubydoc.info/github/sinatra/sinatra/Sinatra/Response
+ # @api public
+ attr_reader :response
+
+ # @return [Hash]
+ # @api public
+ attr_accessor :params
+
+ attr_accessor :current_user
+
+ # The service controller might be loaded outside of a Sinatra App
+ # in this case, we don't need to load the helpers
+ if Object.const_defined?(:Sinatra)
+ include Sinatra::Helpers
+ end
+
+ def initialize(service, &block)
+ @service = service
+ @implementation = block
+ end
+
+ def dispatch(app)
+ @app = app
+ @env = app.env
+ @request = app.request
+ @response = app.response
+ @service = service
+
+ begin
+ # raises an exception if the params are not valid
+ # otherwise update the app params with potentially new params (using default values)
+ # note that if a type is mentioned for a params, the object will be cast to this object type
+ #
+ # removing the fake sinatra params since v1.3 added this. (should be eventually removed)
+ if app.params['splat']
+ processed_params = app.params.dup
+ processed_params.delete('splat')
+ processed_params.delete('captures')
+ end
+ @params = ParamsVerification.validate!((processed_params || app.params), service.defined_params)
+ rescue Exception => e
+ LOGGER.error e.message
+ LOGGER.error "passed params: #{app.params.inspect}"
+ halt 400, {:error => e.message}.to_json
+ end
+
+ # Define WeaselDiesel::RequestHandler#authorization_check in your app if
+ # you want to use an auth check.
+ pre_dispatch_hook if self.respond_to?(:pre_dispatch_hook)
+ service_dispatch
+ end
+
+ # Forwarding some methods to the underlying app object
+ def_delegators :app, :settings, :halt, :compile_template, :session
+
+ private ##################################################
+
+ end # of RequestHandler
+
+ attr_reader :handler
+
+ def implementation(&block)
+ if block_given?
+ @handler = RequestHandler.new(self, &block)
+ @handler.define_singleton_method(:service_dispatch, block)
+ end
+ @handler
+ end
+
+ def load_sinatra_route
+ service = self
+ upcase_verb = service.verb.to_s.upcase
+ LOGGER.info "Available endpoint: #{self.http_verb.upcase} /#{self.url}" unless ENV['NO_ROUTE_PRINT']
+ raise "DSL is missing the implementation block" unless self.handler && self.handler.respond_to?(:service_dispatch)
+
+ # Define the route directly to save some object allocations on the critical path
+ # Note that we are using a private API to define the route and that unlike sinatra usual DSL
+ # we do NOT define a HEAD route for every GET route.
+ Sinatra::Base.send(:route, upcase_verb, "/#{self.url}") do
+ service.handler.dispatch(self)
+ end
+
+ end
+
+end
View
204 lib/wd_sinatra/test_helpers.rb
@@ -0,0 +1,204 @@
+ENV['RACK_ENV'] ||= 'test'
+require 'test/unit'
+require 'rack'
+require 'rack/test'
+require 'json'
+require 'json_response_verification'
+require File.join(File.dirname(__FILE__), '..', 'lib', 'bootloader')
+
+require 'minitest/autorun'
+
+ENV['NO_ROUTE_PRINT'] = 'true'
+Bootloader.start
+WeaselDiesel.send(:include, JSONResponseVerification)
+
+ActiveRecord::Base.logger = nil
+
+class Requester
+ include ::Rack::Test::Methods
+
+ def app
+ Sinatra::Application
+ end
+end
+
+module TestApi
+ module_function
+
+ URL_PLACEHOLDER = /\/*(:[a-z A-Z _]+)\/*/
+ INTERNAL_X_HEADER = AuthHelpers::INTERNAL_X_HEADER[/HTTP_(.*)/, 1] # strip the header marker added by Rack
+ MOBILE_X_HEADER = AuthHelpers::MOBILE_X_HEADER[/HTTP_(.*)/, 1] # strip the header marker added by Rack
+
+ def request(verb, uri, params={}, headers=nil)
+ params ||= {}
+ service_uri = uri.dup
+ matching = uri.scan URL_PLACEHOLDER
+ unless matching.empty?
+ # replace the placeholder by real value
+ matching.flatten.each_with_index do |str, idx|
+ key = str.delete(":").to_sym
+ value = params[key].to_s
+ # delete the value from the params
+ params.delete(key)
+ uri = uri.gsub(str, value)
+ end
+ end
+
+ request = Requester.new
+ yield request if block_given?
+ headers.each {|name, value| request.header(name, value) } if headers
+ response = request.send(verb, uri, params)
+ @json_response = JsonWrapperResponse.new(response, :verb => verb, :uri => service_uri)
+ end
+
+ def mobile_account=(account)
+ @account = account
+ end
+
+ def get(uri, params=nil, headers=nil)
+ request(:get, uri, params, headers)
+ end
+
+ def internal_get(uri, params=nil, headers=nil)
+ get(uri, params, valid_internal_api_headers(headers))
+ end
+
+ def mobile_get(uri, params=nil, headers=nil)
+ request(:get, uri, params, mobile_headers(headers))
+ end
+
+ def post(uri, params=nil, headers=nil)
+ request(:post, uri, params, headers)
+ end
+
+ def internal_post(uri, params=nil, headers=nil)
+ post(uri, params, valid_internal_api_headers(headers))
+ end
+
+ def mobile_post(uri, params=nil, headers=nil)
+ request(:post, uri, params, mobile_headers(headers))
+ end
+
+ def put(uri, params=nil, headers=nil)
+ request(:put, uri, params, headers)
+ end
+
+ def internal_put(uri, params=nil, headers=nil)
+ put(uri, params, valid_internal_api_headers(headers))
+ end
+
+ def mobile_put(uri, params=nil, headers=nil)
+ request(:put, uri, params, mobile_headers(headers))
+ end
+
+ def delete(uri, params=nil, headers=nil)
+ request(:delete, uri, params, headers)
+ end
+
+ def internal_delete(uri, params=nil, headers=nil)
+ delete(uri, params, valid_internal_api_headers(headers))
+ end
+
+ def mobile_delete(uri, params=nil, headers=nil)
+ request(:delete, uri, params, mobile_headers(headers))
+ end
+
+ def head(uri, params=nil, headers=nil)
+ request(:head, uri, params, headers)
+ end
+
+ def internal_head(uri, params=nil, headers=nil)
+ head(uri, params, valid_internal_api_headers(headers))
+ end
+
+ def mobile_head(uri, params=nil, headers=nil)
+ request(:head, uri, params, mobile_headers(headers))
+ end
+
+ def json_response
+ @json_response
+ end
+
+ def last_response
+ @json_response.rest_response if @json_response
+ end
+
+ def valid_internal_api_headers(headers)
+ custom_headers = {INTERNAL_X_HEADER => AuthHelpers::ALLOWED_API_KEYS[0]}
+ custom_headers.merge!(headers) if headers
+ custom_headers
+ end
+
+ def mobile_headers(headers)
+ custom_headers = {MOBILE_X_HEADER => @account ? Base64.urlsafe_encode64(@account.mobile_token) : nil}
+ custom_headers.merge!(headers) if headers
+ custom_headers
+ end
+
+end
+
+
+# Wrapper around a rest response
+class JsonWrapperResponse
+ extend Forwardable
+
+ attr_reader :rest_response
+ attr_reader :verb
+ attr_reader :uri
+
+ def initialize(response, opts={})
+ @rest_response = response
+ @verb = opts[:verb]
+ @uri = opts[:uri]
+ end
+
+ def body
+ @body ||= JSON.load(rest_response.body) rescue rest_response.body
+ end
+
+ def success?
+ @rest_response.status == 200
+ end
+
+ def redirected?
+ @rest_response.status.to_s =~ /30\d/
+ end
+
+ def [](val)
+ if body
+ body[val.to_s]
+ else
+ nil
+ end
+ end
+
+ def method_missing(meth, *args)
+ body.send(meth, args)
+ end
+
+ def_delegators :rest_response, :code, :headers, :raw_headers, :cookies, :status, :errors
+end
+
+
+# Custom assertions
+def assert_api_response(response=nil, message=nil)
+ response ||= TestApi.json_response
+ print response.rest_response.errors if response.status === 500
+ assert response.success?, message || ["Body: #{response.rest_response.body}", "Errors: #{response.errors}", "Status code: #{response.status}"].join("\n")
+ service = WSList.all.find{|s| s.verb == response.verb && s.url == response.uri[1..-1]}
+ raise "Service for (#{response.verb.upcase} #{response.uri[1..-1]}) not found" unless service
+ unless service.response.nodes.empty?
+ assert response.body.is_a?(Hash), "Invalid JSON response:\n#{response.body}"
+ valid, errors = service.validate_hash_response(response.body)
+ assert valid, errors.join(" & ") || message
+ end
+end
+
+def assert_api_response_with_redirection(redirection_url=nil)
+ response = TestApi.json_response
+ print response.rest_response.errors if response.status === 500
+ assert response.status == 302, "Redirection expect, but got #{response.status}"
+ if redirection_url
+ assert response.headers["location"], redirection_url
+ end
+end
View
5 lib/wd_sinatra/version.rb
@@ -0,0 +1,5 @@
+module WD
+ module Sinatra
+ VERSION = "0.0.1"
+ end
+end
View
24 templates/Gemfile
@@ -0,0 +1,24 @@
+source "http://rubygems.org"
+
+# web engine
+gem "sinatra", "1.3.2"
+# service DSL
+gem "weasel_diesel"
+gem "wd_sinatra", :git => "git://github.com/mattetti/wd-sinatra.git"
+
+
+if RUBY_VERSION =~ /1.8/
+ gem 'backports', '2.3.0'
+ gem 'json'
+end
+
+if ENV['RACK_ENV'] != "production"
+ gem "rack-test", "0.6.1"
+ gem "foreman"
+ gem "puma"
+ gem "minitest"
+ gem "guard-puma"
+ gem "guard-minitest"
+ gem "launchy"
+ gem "rake"
+end
View
13 templates/Guardfile
@@ -0,0 +1,13 @@
+# A sample Guardfile
+# More info at https://github.com/guard/guard#readme
+
+guard 'puma' do
+ watch('Gemfile.lock')
+ watch(%r{^config|lib/.*})
+end
+
+guard 'minitest', :test_file_patterns => '*_test.rb' do
+ watch(%r|^test/(.*)_test\.rb|)
+ watch(%r{^api/(.*/)?([^/]+)\.rb$}) { |m| "test/api/#{m[1]}#{m[2]}_test.rb" }
+ watch(%r|^test/test_helper\.rb|) { "test" }
+end
View
38 templates/Rakefile
@@ -0,0 +1,38 @@
+require 'rbconfig'
+require 'rake/testtask'
+require 'rubygems'
+require 'bundler'
+Bundler.setup
+require 'wd_sinatra/app_loader'
+
+Rake::TestTask.new do |t|
+ t.libs << "."
+ t.libs << 'test'
+ t.pattern = "test/**/*_test.rb"
+end
+
+task :default => :test
+
+# boot the app
+task :setup_app do
+ ENV['DONT_CONNECT'] = 'true'
+ Bootloader.start
+end
+
+task :environment do
+ ENV['DONT_CONNECT'] = nil
+ Bootloader.start
+end
+
+desc "Run the test suite by resting the DB first"
+task :clean_test_suite do
+ ENV['RACK_ENV'] ||= 'test'
+ Rake::Task["db:drop"].invoke
+ Rake::Task["db:create"].invoke
+ Rake::Task["db:setup"].invoke
+ Rake::Task["test"].invoke
+end
+
+Bootloader.set_loadpath
+load File.join('tasks', 'db.rake')
+load File.join('tasks', 'doc.rake')
View
29 templates/api/hello_world.rb
@@ -0,0 +1,29 @@
+describe_service "hello_world" do |service|
+ service.formats :json
+ service.http_verb :get
+ service.disable_auth # on by default
+
+ # INPUT
+ service.param.string :name, :default => 'World'
+
+ # OUTPUT
+ service.response do |response|
+ response.object do |obj|
+ obj.string :message, :doc => "The greeting message sent back. Defaults to 'World'"
+ obj.datetime :at, :doc => "The timestamp of when the message was dispatched"
+ end
+ end
+
+ # DOCUMENTATION
+ service.documentation do |doc|
+ doc.overall "This service provides a simple hello world implementation example."
+ doc.param :name, "The name of the person to greet."
+ doc.example "<code>curl -I 'http://localhost:9292/hello_world?name=Matt'</code>"
+ end
+
+ # ACTION/IMPLEMENTATION
+ service.implementation do
+ {:message => "Hello #{params[:name]}", :at => Time.now}.to_json
+ end
+
+end
View
7 templates/bin/console
@@ -0,0 +1,7 @@
+#!/usr/bin/env ruby
+require 'irb'
+require 'rubygems'
+require 'wd_sinatra/app_loader'
+root = File.expand_path(File.dirname(__FILE__))
+WDSinatra::AppLoader.console(root)
+IRB.start(__FILE__)
View
5 templates/config.ru
@@ -0,0 +1,5 @@
+require 'rubygems'
+require 'wd_sinatra/app_loader'
+root = File.expand_path(File.dirname(__FILE__))
+Bootloader.server(root)
+run Sinatra::Application
View
1  templates/config/app.rb
@@ -0,0 +1 @@
+# called after the environment is setup and loaded
View
3  templates/config/environments/default.rb
@@ -0,0 +1,3 @@
+# This is the default environment setup.
+# It can be overwritten or extended by a custom environment file.
+# Note: the LOGGER constant isn't yet defined and shouldn't be defined here.
View
2  templates/config/environments/production.rb
@@ -0,0 +1,2 @@
+require 'logger'
+LOGGER.level = Logger::INFO
View
3  templates/config/environments/test.rb
@@ -0,0 +1,3 @@
+require 'logger'
+LOGGER = Logger.new($stdout)
+LOGGER.level = Logger::FATAL
View
0  templates/config/middleware.rb
No changes.
View
34 templates/lib/auth_helpers.rb
@@ -0,0 +1,34 @@
+module AuthHelpers
+
+ MOBILE_X_HEADER = 'HTTP_X_MOBILE_TOKEN'
+ INTERNAL_X_HEADER = 'HTTP_X_INTERNAL_API_KEY'
+
+ # This hook is called before dispatching any
+ # requests.
+ #
+ # Implementation example
+ def pre_dispatch_hook
+ if service.extra[:mobile]
+ mobile_auth_check
+ elsif service.extra[:internal]
+ internal_api_key_check
+ elsif !service.auth_required
+ return
+ else
+ halt 403 # protect by default
+ end
+ end
+
+ # Implementation example
+ def mobile_auth_check
+ true
+ end
+
+ # Implementation example
+ def internal_api_key_check
+ true
+ end
+
+end
+
+Sinatra::Helpers.send(:include, AuthHelpers)
View
41 templates/lib/tasks/doc.rake
@@ -0,0 +1,41 @@
+namespace :doc do
+ desc "Generate documentation for the web services"
+ task :services do
+ require "launchy"
+
+ ENV['DONT_CONNECT'] = 'true'
+ ENV['NO_ROUTE_PRINT'] = 'true'
+ require File.expand_path('../../lib/bootloader', File.dirname(__FILE__))
+ Bootloader.start
+ LOGGER.level = Logger::FATAL
+
+ require 'fileutils'
+ destination = File.join(File.dirname(__FILE__), '..', '..', 'doc')
+ FileUtils.mkdir_p(destination) unless File.exist?(destination)
+ copy_assets(destination)
+
+ File.open("#{destination}/index.html", "w"){|f| f << template.result(binding)}
+
+ Launchy.open("#{destination}/index.html")
+ end
+
+ def template
+ file = resources.join 'template.erb'
+ ERB.new File.read(file)
+ end
+
+ def resources
+ require 'pathname'
+ @resources ||= Pathname.new(File.join(File.dirname(__FILE__), 'doc_generator'))
+ end
+
+ def copy_assets(destination)
+ %W{css js images}.each do |asset_type|
+ FileUtils.mkdir_p(File.join(destination, asset_type))
+ end
+ Dir.glob(resources.join("bootstrap", "js", "*.js")).each do |file|
+ FileUtils.cp(file, File.join(destination, 'js'))
+ end
+ FileUtils.cp(resources.join('bootstrap', 'bootstrap.css'), File.join(destination, 'css'))
+ end
+end
View
4 templates/lib/tasks/doc_generator/bootstrap/.gitignore
@@ -0,0 +1,4 @@
+*~
+.DS_Store
+thumbs.db
+js/min
View
176 templates/lib/tasks/doc_generator/bootstrap/LICENSE
@@ -0,0 +1,176 @@
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
View
47 templates/lib/tasks/doc_generator/bootstrap/Makefile
@@ -0,0 +1,47 @@
+VERSION=1.4.0
+DATE=$(shell DATE)
+BOOTSTRAP = ./bootstrap.css
+BOOTSTRAP_MIN = ./bootstrap.min.css
+BOOTSTRAP_LESS = ./lib/bootstrap.less
+LESS_COMPRESSOR ?= `which lessc`
+UGLIFY_JS ?= `which uglifyjs`
+WATCHR ?= `which watchr`
+
+build:
+ @@if test ! -z ${LESS_COMPRESSOR}; then \
+ sed -e 's/@VERSION/'"v${VERSION}"'/' -e 's/@DATE/'"${DATE}"'/' <${BOOTSTRAP_LESS} >${BOOTSTRAP_LESS}.tmp; \
+ lessc ${BOOTSTRAP_LESS}.tmp > ${BOOTSTRAP}; \
+ lessc ${BOOTSTRAP_LESS}.tmp > ${BOOTSTRAP_MIN} --compress; \
+ rm -f ${BOOTSTRAP_LESS}.tmp; \
+ echo "Bootstrap successfully built! - `date`"; \
+ else \
+ echo "You must have the LESS compiler installed in order to build Bootstrap."; \
+ echo "You can install it by running: npm install less -g"; \
+ fi
+
+js/min:
+ @@if test ! -z ${UGLIFY_JS}; then \
+ mkdir -p js/min; \
+ uglifyjs -o js/min/bootstrap-alerts.min.js js/bootstrap-alerts.js;\
+ uglifyjs -o js/min/bootstrap-buttons.min.js js/bootstrap-buttons.js;\
+ uglifyjs -o js/min/bootstrap-dropdown.min.js js/bootstrap-dropdown.js;\
+ uglifyjs -o js/min/bootstrap-modal.min.js js/bootstrap-modal.js;\
+ uglifyjs -o js/min/bootstrap-popover.min.js js/bootstrap-popover.js;\
+ uglifyjs -o js/min/bootstrap-scrollspy.min.js js/bootstrap-scrollspy.js;\
+ uglifyjs -o js/min/bootstrap-tabs.min.js js/bootstrap-tabs.js;\
+ uglifyjs -o js/min/bootstrap-twipsy.min.js js/bootstrap-twipsy.js;\
+ else \
+ echo "You must have the UGLIFYJS minifier installed in order to minify Bootstrap's js."; \
+ echo "You can install it by running: npm install uglify-js -g"; \
+ fi
+
+watch:
+ @@if test ! -z ${WATCHR}; then \
+ echo "Watching less files..."; \
+ watchr -e "watch('lib/.*\.less') { system 'make' }"; \
+ else \
+ echo "You must have the watchr installed in order to watch Bootstrap less files."; \
+ echo "You can install it by running: gem install watchr"; \
+ fi
+
+.PHONY: build watch
View
105 templates/lib/tasks/doc_generator/bootstrap/README.md
@@ -0,0 +1,105 @@
+TWITTER BOOTSTRAP
+=================
+
+Bootstrap is Twitter's toolkit for kickstarting CSS for websites, apps, and more. It includes base CSS styles for typography, forms, buttons, tables, grids, navigation, alerts, and more.
+
+To get started -- checkout http://twitter.github.com/bootstrap!
+
+
+Usage
+-----
+
+You can use Twitter Bootstrap in one of two ways: just drop the compiled CSS into any new project and start cranking, or run LESS on your site and compile on the fly like a boss.
+
+Here's what the LESS version looks like:
+
+``` html
+<link rel="stylesheet/less" type="text/css" href="lib/bootstrap.less">
+<script src="less.js" type="text/javascript"></script>
+```
+
+Or if you prefer, the standard css way:
+
+``` html
+<link rel="stylesheet" type="text/css" href="bootstrap.css">
+```
+
+For more info, refer to the docs!
+
+
+Versioning
+----------
+
+For transparency and insight into our release cycle, and for striving to maintain backwards compatibility, Bootstrap will be maintained under the Semantic Versioning guidelines as much as possible.
+
+Releases will be numbered with the follow format:
+
+`<major>.<minor>.<patch>`
+
+And constructed with the following guidelines:
+
+* Breaking backwards compatibility bumps the major
+* New additions without breaking backwards compatibility bumps the minor
+* Bug fixes and misc changes bump the patch
+
+For more information on SemVer, please visit http://semver.org/.
+
+
+Bug tracker
+-----------
+
+Have a bug? Please create an issue here on GitHub!
+
+https://github.com/twitter/bootstrap/issues
+
+
+Twitter account
+---------------
+
+Keep up to date on announcements and more by following Bootstrap on Twitter, <a href="http://twitter.com/TwBootstrap">@TwBootstrap</a>.
+
+
+Mailing list
+------------
+
+Have a question? Ask on our mailing list!
+
+twitter-bootstrap@googlegroups.com
+
+http://groups.google.com/group/twitter-bootstrap
+
+
+Developers
+----------
+
+We have included a makefile with convenience methods for working with the bootstrap library.
+
++ **build** - `make build`
+This will run the less compiler on the bootstrap lib and generate a bootstrap.css and bootstrap.min.css file.
+The lessc compiler is required for this command to run.
+
++ **watch** - `make watch`
+This is a convenience method for watching your less files and automatically building them whenever you save.
+Watchr is required for this command to run.
+
+
+Authors
+-------
+
+**Mark Otto**
+
++ http://twitter.com/mdo
++ http://github.com/markdotto
+
+**Jacob Thornton**
+
++ http://twitter.com/fat
++ http://github.com/fat
+
+
+License
+---------------------
+
+Copyright 2011 Twitter, Inc.
+
+Licensed under the Apache License, Version 2.0: http://www.apache.org/licenses/LICENSE-2.0
View
2,467 templates/lib/tasks/doc_generator/bootstrap/bootstrap.css
@@ -0,0 +1,2467 @@
+/*!
+ * Bootstrap v1.4.0
+ *
+ * Copyright 2011 Twitter, Inc
+ * Licensed under the Apache License v2.0
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Designed and built with all the love in the world @twitter by @mdo and @fat.
+ * Date: Sun Nov 20 21:42:29 PST 2011
+ */
+/* Reset.less
+ * Props to Eric Meyer (meyerweb.com) for his CSS reset file. We're using an adapted version here that cuts out some of the reset HTML elements we will never need here (i.e., dfn, samp, etc).
+ * ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- */
+html, body {
+ margin: 0;
+ padding: 0;
+}
+h1,
+h2,
+h3,
+h4,
+h5,
+h6,
+p,
+blockquote,
+pre,
+a,
+abbr,
+acronym,
+address,
+cite,
+code,
+del,
+dfn,
+em,
+img,
+q,
+s,
+samp,
+small,
+strike,
+strong,
+sub,
+sup,
+tt,
+var,
+dd,
+dl,
+dt,
+li,
+ol,
+ul,
+fieldset,
+form,
+label,
+legend,
+button,
+table,
+caption,
+tbody,
+tfoot,
+thead,
+tr,
+th,
+td {
+ margin: 0;
+ padding: 0;
+ border: 0;
+ font-weight: normal;
+ font-style: normal;
+ font-size: 100%;
+ line-height: 1;
+ font-family: inherit;
+}
+table {
+ border-collapse: collapse;
+ border-spacing: 0;
+}
+ol, ul {
+ list-style: none;
+}
+q:before,
+q:after,
+blockquote:before,
+blockquote:after {
+ content: "";
+}
+html {
+ overflow-y: scroll;
+ font-size: 100%;
+ -webkit-text-size-adjust: 100%;
+ -ms-text-size-adjust: 100%;
+}
+a:focus {
+ outline: thin dotted;
+}
+a:hover, a:active {
+ outline: 0;
+}
+article,
+aside,
+details,
+figcaption,
+figure,
+footer,
+header,
+hgroup,
+nav,
+section {
+ display: block;
+}
+audio, canvas, video {
+ display: inline-block;
+ *display: inline;
+ *zoom: 1;
+}
+audio:not([controls]) {
+ display: none;
+}
+sub, sup {
+ font-size: 75%;
+ line-height: 0;
+ position: relative;
+ vertical-align: baseline;
+}
+sup {
+ top: -0.5em;
+}
+sub {
+ bottom: -0.25em;
+}
+img {
+ border: 0;
+ -ms-interpolation-mode: bicubic;
+}
+button,
+input,
+select,
+textarea {
+ font-size: 100%;
+ margin: 0;
+ vertical-align: baseline;
+ *vertical-align: middle;
+}
+button, input {
+ line-height: normal;
+ *overflow: visible;
+}
+button::-moz-focus-inner, input::-moz-focus-inner {
+ border: 0;
+ padding: 0;
+}
+button,
+input[type="button"],
+input[type="reset"],
+input[type="submit"] {
+ cursor: pointer;
+ -webkit-appearance: button;
+}
+input[type="search"] {
+ -webkit-appearance: textfield;
+ -webkit-box-sizing: content-box;
+ -moz-box-sizing: content-box;
+ box-sizing: content-box;
+}
+input[type="search"]::-webkit-search-decoration {
+ -webkit-appearance: none;
+}
+textarea {
+ overflow: auto;
+ vertical-align: top;
+}
+/* Variables.less
+ * Variables to customize the look and feel of Bootstrap
+ * ----------------------------------------------------- */
+/* Mixins.less
+ * Snippets of reusable CSS to develop faster and keep code readable
+ * ----------------------------------------------------------------- */
+/*
+ * Scaffolding
+ * Basic and global styles for generating a grid system, structural layout, and page templates
+ * ------------------------------------------------------------------------------------------- */
+body {
+ background-color: #ffffff;
+ margin: 0;
+ font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
+ font-size: 13px;
+ font-weight: normal;
+ line-height: 18px;
+ color: #404040;
+}
+.container {
+ width: 940px;
+ margin-left: auto;
+ margin-right: auto;
+ zoom: 1;
+}
+.container:before, .container:after {
+ display: table;
+ content: "";
+ zoom: 1;
+}
+.container:after {
+ clear: both;
+}
+.container-fluid {
+ position: relative;
+ min-width: 940px;
+ padding-left: 20px;
+ padding-right: 20px;
+ zoom: 1;
+}
+.container-fluid:before, .container-fluid:after {
+ display: table;
+ content: "";
+ zoom: 1;
+}
+.container-fluid:after {
+ clear: both;
+}
+.container-fluid > .sidebar {
+ position: absolute;
+ top: 0;
+ left: 20px;
+ width: 220px;
+}
+.container-fluid > .content {
+ margin-left: 240px;
+}
+a {
+ color: #0069d6;
+ text-decoration: none;
+ line-height: inherit;
+ font-weight: inherit;
+}
+a:hover {
+ color: #00438a;
+ text-decoration: underline;
+}
+.pull-right {
+ float: right;
+}
+.pull-left {
+ float: left;
+}
+.hide {
+ display: none;
+}
+.show {
+ display: block;
+}
+.row {
+ zoom: 1;
+ margin-left: -20px;
+}
+.row:before, .row:after {
+ display: table;
+ content: "";
+ zoom: 1;
+}
+.row:after {
+ clear: both;
+}
+.row > [class*="span"] {
+ display: inline;
+ float: left;
+ margin-left: 20px;
+}
+.span1 {
+ width: 40px;
+}
+.span2 {
+ width: 100px;
+}
+.span3 {
+ width: 160px;
+}
+.span4 {
+ width: 220px;
+}
+.span5 {
+ width: 280px;
+}
+.span6 {
+ width: 340px;
+}
+.span7 {
+ width: 400px;
+}
+.span8 {
+ width: 460px;
+}
+.span9 {
+ width: 520px;
+}
+.span10 {
+ width: 580px;
+}
+.span11 {
+ width: 640px;
+}
+.span12 {
+ width: 700px;
+}
+.span13 {
+ width: 760px;
+}
+.span14 {
+ width: 820px;
+}
+.span15 {
+ width: 880px;
+}
+.span16 {
+ width: 940px;
+}
+.span17 {
+ width: 1000px;
+}
+.span18 {
+ width: 1060px;
+}
+.span19 {
+ width: 1120px;
+}
+.span20 {
+ width: 1180px;
+}
+.span21 {
+ width: 1240px;
+}
+.span22 {
+ width: 1300px;
+}
+.span23 {
+ width: 1360px;
+}
+.span24 {
+ width: 1420px;
+}
+.row > .offset1 {
+ margin-left: 80px;
+}
+.row > .offset2 {
+ margin-left: 140px;
+}
+.row > .offset3 {
+ margin-left: 200px;
+}
+.row > .offset4 {
+ margin-left: 260px;
+}
+.row > .offset5 {
+ margin-left: 320px;
+}
+.row > .offset6 {
+ margin-left: 380px;
+}
+.row > .offset7 {
+ margin-left: 440px;
+}
+.row > .offset8 {
+ margin-left: 500px;
+}
+.row > .offset9 {
+ margin-left: 560px;
+}
+.row > .offset10 {
+ margin-left: 620px;
+}
+.row > .offset11 {
+ margin-left: 680px;
+}
+.row > .offset12 {
+ margin-left: 740px;
+}
+.span-one-third {
+ width: 300px;
+}
+.span-two-thirds {
+ width: 620px;
+}
+.row > .offset-one-third {
+ margin-left: 340px;
+}
+.row > .offset-two-thirds {
+ margin-left: 660px;
+}
+/* Typography.less
+ * Headings, body text, lists, code, and more for a versatile and durable typography system
+ * ---------------------------------------------------------------------------------------- */
+p {
+ font-size: 13px;
+ font-weight: normal;
+ line-height: 18px;
+ margin-bottom: 9px;
+}
+p small {
+ font-size: 11px;
+ color: #bfbfbf;
+}
+h1,
+h2,
+h3,
+h4,
+h5,
+h6 {
+ font-weight: bold;
+ color: #404040;
+}
+h1 small,
+h2 small,
+h3 small,
+h4 small,
+h5 small,
+h6 small {
+ color: #bfbfbf;
+}
+h1 {
+ margin-bottom: 18px;
+ font-size: 30px;
+ line-height: 36px;
+}
+h1 small {
+ font-size: 18px;
+}
+h2 {
+ font-size: 24px;
+ line-height: 36px;
+}
+h2 small {
+ font-size: 14px;
+}
+h3,
+h4,
+h5,
+h6 {
+ line-height: 36px;
+}
+h3 {
+ font-size: 18px;
+}
+h3 small {
+ font-size: 14px;
+}
+h4 {
+ font-size: 16px;
+}
+h4 small {
+ font-size: 12px;
+}
+h5 {
+ font-size: 14px;
+}
+h6 {
+ font-size: 13px;
+ color: #bfbfbf;
+ text-transform: uppercase;
+}
+ul, ol {
+ margin: 0 0 18px 25px;
+}
+ul ul,
+ul ol,
+ol ol,
+ol ul {
+ margin-bottom: 0;
+}
+ul {
+ list-style: disc;
+}
+ol {
+ list-style: decimal;
+}
+li {
+ line-height: 18px;
+ color: #808080;
+}
+ul.unstyled {
+ list-style: none;
+ margin-left: 0;
+}
+dl {
+ margin-bottom: 18px;
+}
+dl dt, dl dd {
+ line-height: 18px;
+}
+dl dt {
+ font-weight: bold;
+}
+dl dd {
+ margin-left: 9px;
+}
+hr {
+ margin: 20px 0 19px;
+ border: 0;
+ border-bottom: 1px solid #eee;
+}
+strong {
+ font-style: inherit;
+ font-weight: bold;
+}
+em {
+ font-style: italic;
+ font-weight: inherit;
+ line-height: inherit;
+}
+.muted {
+ color: #bfbfbf;
+}
+blockquote {
+ margin-bottom: 18px;
+ border-left: 5px solid #eee;
+ padding-left: 15px;
+}
+blockquote p {
+ font-size: 14px;
+ font-weight: 300;
+ line-height: 18px;
+ margin-bottom: 0;
+}
+blockquote small {
+ display: block;
+ font-size: 12px;
+ font-weight: 300;
+ line-height: 18px;
+ color: #bfbfbf;
+}
+blockquote small:before {
+ content: '\2014 \00A0';
+}
+address {
+ display: block;
+ line-height: 18px;
+ margin-bottom: 18px;
+}
+code, pre {
+ padding: 0 3px 2px;
+ font-family: Monaco, Andale Mono, Courier New, monospace;
+ font-size: 12px;
+ -webkit-border-radius: 3px;
+ -moz-border-radius: 3px;
+ border-radius: 3px;
+}
+code {
+ background-color: #fee9cc;
+ color: rgba(0, 0, 0, 0.75);
+ padding: 1px 3px;
+}
+pre {
+ background-color: #f5f5f5;
+ display: block;
+ padding: 8.5px;
+ margin: 0 0 18px;
+ line-height: 18px;
+ font-size: 12px;
+ border: 1px solid #ccc;
+ border: 1px solid rgba(0, 0, 0, 0.15);
+ -webkit-border-radius: 3px;
+ -moz-border-radius: 3px;
+ border-radius: 3px;
+ white-space: pre;
+ white-space: pre-wrap;
+ word-wrap: break-word;
+}
+/* Forms.less
+ * Base styles for various input types, form layouts, and states
+ * ------------------------------------------------------------- */
+form {
+ margin-bottom: 18px;
+}
+fieldset {
+ margin-bottom: 18px;
+ padding-top: 18px;
+}
+fieldset legend {
+ display: block;
+ padding-left: 150px;
+ font-size: 19.5px;
+ line-height: 1;
+ color: #404040;
+ *padding: 0 0 5px 145px;
+ /* IE6-7 */
+
+ *line-height: 1.5;
+ /* IE6-7 */
+
+}
+form .clearfix {
+ margin-bottom: 18px;
+ zoom: 1;
+}
+form .clearfix:before, form .clearfix:after {
+ display: table;
+ content: "";
+ zoom: 1;
+}
+form .clearfix:after {
+ clear: both;
+}
+label,
+input,
+select,
+textarea {
+ font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
+ font-size: 13px;
+ font-weight: normal;
+ line-height: normal;
+}
+label {
+ padding-top: 6px;
+ font-size: 13px;
+ line-height: 18px;
+ float: left;
+ width: 130px;
+ text-align: right;
+ color: #404040;
+}
+form .input {
+ margin-left: 150px;
+}
+input[type=checkbox], input[type=radio] {
+ cursor: pointer;
+}
+input,
+textarea,
+select,
+.uneditable-input {
+ display: inline-block;
+ width: 210px;
+ height: 18px;
+ padding: 4px;
+ font-size: 13px;
+ line-height: 18px;
+ color: #808080;
+ border: 1px solid #ccc;
+ -webkit-border-radius: 3px;
+ -moz-border-radius: 3px;
+ border-radius: 3px;
+}
+select {
+ padding: initial;
+}
+input[type=checkbox], input[type=radio] {
+ width: auto;
+ height: auto;
+ padding: 0;
+ margin: 3px 0;
+ *margin-top: 0;
+ /* IE6-7 */
+
+ line-height: normal;
+ border: none;
+}
+input[type=file] {
+ background-color: #ffffff;
+ padding: initial;
+ border: initial;
+ line-height: initial;
+ -webkit-box-shadow: none;
+ -moz-box-shadow: none;
+ box-shadow: none;
+}
+input[type=button], input[type=reset], input[type=submit] {
+ width: auto;
+ height: auto;
+}
+select, input[type=file] {
+ height: 27px;
+ *height: auto;
+ line-height: 27px;
+ *margin-top: 4px;
+ /* For IE7, add top margin to align select with labels */
+
+}
+select[multiple] {
+ height: inherit;
+ background-color: #ffffff;
+}
+textarea {
+ height: auto;
+}
+.uneditable-input {
+ background-color: #ffffff;
+ display: block;
+ border-color: #eee;
+ -webkit-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.025);
+ -moz-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.025);
+ box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.025);
+ cursor: not-allowed;
+}
+:-moz-placeholder {
+ color: #bfbfbf;
+}
+::-webkit-input-placeholder {
+ color: #bfbfbf;
+}
+input, textarea {
+ -webkit-transition: border linear 0.2s, box-shadow linear 0.2s;
+ -moz-transition: border linear 0.2s, box-shadow linear 0.2s;
+ -ms-transition: border linear 0.2s, box-shadow linear 0.2s;
+ -o-transition: border linear 0.2s, box-shadow linear 0.2s;
+ transition: border linear 0.2s, box-shadow linear 0.2s;
+ -webkit-box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.1);
+ -moz-box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.1);
+ box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.1);
+}
+input:focus, textarea:focus {
+ outline: 0;
+ border-color: rgba(82, 168, 236, 0.8);
+ -webkit-box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.1), 0 0 8px rgba(82, 168, 236, 0.6);
+ -moz-box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.1), 0 0 8px rgba(82, 168, 236, 0.6);
+ box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.1), 0 0 8px rgba(82, 168, 236, 0.6);
+}
+input[type=file]:focus, input[type=checkbox]:focus, select:focus {
+ -webkit-box-shadow: none;
+ -moz-box-shadow: none;
+ box-shadow: none;
+ outline: 1px dotted #666;
+}
+form .clearfix.error > label, form .clearfix.error .help-block, form .clearfix.error .help-inline {
+ color: #b94a48;
+}
+form .clearfix.error input, form .clearfix.error textarea {
+ color: #b94a48;
+ border-color: #ee5f5b;
+}
+form .clearfix.error input:focus, form .clearfix.error textarea:focus {
+ border-color: #e9322d;
+ -webkit-box-shadow: 0 0 6px #f8b9b7;
+ -moz-box-shadow: 0 0 6px #f8b9b7;
+ box-shadow: 0 0 6px #f8b9b7;
+}
+form .clearfix.error .input-prepend .add-on, form .clearfix.error .input-append .add-on {
+ color: #b94a48;
+ background-color: #fce6e6;
+ border-color: #b94a48;
+}
+form .clearfix.warning > label, form .clearfix.warning .help-block, form .clearfix.warning .help-inline {
+ color: #c09853;
+}
+form .clearfix.warning input, form .clearfix.warning textarea {
+ color: #c09853;
+ border-color: #ccae64;
+}
+form .clearfix.warning input:focus, form .clearfix.warning textarea:focus {
+ border-color: #be9a3f;
+ -webkit-box-shadow: 0 0 6px #e5d6b1;
+ -moz-box-shadow: 0 0 6px #e5d6b1;
+ box-shadow: 0 0 6px #e5d6b1;
+}
+form .clearfix.warning .input-prepend .add-on, form .clearfix.warning .input-append .add-on {
+ color: #c09853;
+ background-color: #d2b877;
+ border-color: #c09853;
+}
+form .clearfix.success > label, form .clearfix.success .help-block, form .clearfix.success .help-inline {
+ color: #468847;
+}
+form .clearfix.success input, form .clearfix.success textarea {
+ color: #468847;
+ border-color: #57a957;
+}
+form .clearfix.success input:focus, form .clearfix.success textarea:focus {
+ border-color: #458845;
+ -webkit-box-shadow: 0 0 6px #9acc9a;
+ -moz-box-shadow: 0 0 6px #9acc9a;
+ box-shadow: 0 0 6px #9acc9a;
+}
+form .clearfix.success .input-prepend .add-on, form .clearfix.success .input-append .add-on {
+ color: #468847;
+ background-color: #bcddbc;
+ border-color: #468847;
+}
+.input-mini,
+input.mini,
+textarea.mini,
+select.mini {
+ width: 60px;
+}
+.input-small,
+input.small,
+textarea.small,
+select.small {
+ width: 90px;
+}
+.input-medium,
+input.medium,
+textarea.medium,
+select.medium {
+ width: 150px;
+}
+.input-large,
+input.large,
+textarea.large,
+select.large {
+ width: 210px;
+}
+.input-xlarge,
+input.xlarge,
+textarea.xlarge,
+select.xlarge {
+ width: 270px;
+}
+.input-xxlarge,
+input.xxlarge,
+textarea.xxlarge,
+select.xxlarge {
+ width: 530px;
+}
+textarea.xxlarge {
+ overflow-y: auto;
+}
+input.span1, textarea.span1 {
+ display: inline-block;
+ float: none;
+ width: 30px;
+ margin-left: 0;
+}
+input.span2, textarea.span2 {
+ display: inline-block;
+ float: none;
+ width: 90px;
+ margin-left: 0;
+}
+input.span3, textarea.span3 {
+ display: inline-block;
+ float: none;
+ width: 150px;
+ margin-left: 0;
+}
+input.span4, textarea.span4 {
+ display: inline-block;
+ float: none;
+ width: 210px;
+ margin-left: 0;
+}
+input.span5, textarea.span5 {
+ display: inline-block;
+ float: none;
+ width: 270px;
+ margin-left: 0;
+}
+input.span6, textarea.span6 {
+ display: inline-block;
+ float: none;
+ width: 330px;
+ margin-left: 0;
+}
+input.span7, textarea.span7 {
+ display: inline-block;
+ float: none;
+ width: 390px;
+ margin-left: 0;
+}
+input.span8, textarea.span8 {
+ display: inline-block;
+ float: none;
+ width: 450px;
+ margin-left: 0;
+}
+input.span9, textarea.span9 {
+ display: inline-block;
+ float: none;
+ width: 510px;
+ margin-left: 0;
+}
+input.span10, textarea.span10 {
+ display: inline-block;
+ float: none;
+ width: 570px;
+ margin-left: 0;
+}
+input.span11, textarea.span11 {
+ display: inline-block;
+ float: none;
+ width: 630px;
+ margin-left: 0;
+}
+input.span12, textarea.span12 {
+ display: inline-block;
+ float: none;
+ width: 690px;
+ margin-left: 0;
+}
+input.span13, textarea.span13 {
+ display: inline-block;
+ float: none;
+ width: 750px;
+ margin-left: 0;
+}
+input.span14, textarea.span14 {
+ display: inline-block;
+ float: none;
+ width: 810px;
+ margin-left: 0;
+}
+input.span15, textarea.span15 {
+ display: inline-block;
+ float: none;
+ width: 870px;
+ margin-left: 0;
+}
+input.span16, textarea.span16 {
+ display: inline-block;
+ float: none;
+ width: 930px;
+ margin-left: 0;
+}
+input[disabled],
+select[disabled],
+textarea[disabled],
+input[readonly],
+select[readonly],
+textarea[readonly] {
+ background-color: #f5f5f5;
+ border-color: #ddd;
+ cursor: not-allowed;
+}
+.actions {
+ background: #f5f5f5;
+ margin-top: 18px;
+ margin-bottom: 18px;
+ padding: 17px 20px 18px 150px;
+ border-top: 1px solid #ddd;
+ -webkit-border-radius: 0 0 3px 3px;
+ -moz-border-radius: 0 0 3px 3px;
+ border-radius: 0 0 3px 3px;
+}
+.actions .secondary-action {
+ float: right;
+}
+.actions .secondary-action a {
+ line-height: 30px;
+}
+.actions .secondary-action a:hover {
+ text-decoration: underline;
+}
+.help-inline, .help-block {
+ font-size: 13px;
+ line-height: 18px;
+ color: #bfbfbf;
+}
+.help-inline {
+ padding-left: 5px;
+ *position: relative;
+ /* IE6-7 */
+
+ *top: -5px;
+ /* IE6-7 */
+
+}
+.help-block {
+ display: block;
+ max-width: 600px;
+}
+.inline-inputs {
+ color: #808080;
+}
+.inline-inputs span {
+ padding: 0 2px 0 1px;
+}
+.input-prepend input, .input-append input {
+ -webkit-border-radius: 0 3px 3px 0;
+ -moz-border-radius: 0 3px 3px 0;
+ border-radius: 0 3px 3px 0;
+}
+.input-prepend .add-on, .input-append .add-on {
+ position: relative;
+ background: #f5f5f5;
+ border: 1px solid #ccc;
+ z-index: 2;
+ float: left;
+ display: block;
+ width: auto;
+ min-width: 16px;
+ height: 18px;
+ padding: 4px 4px 4px 5px;
+ margin-right: -1px;
+ font-weight: normal;
+ line-height: 18px;
+ color: #bfbfbf;
+ text-align: center;
+ text-shadow: 0 1px 0 #ffffff;
+ -webkit-border-radius: 3px 0 0 3px;
+ -moz-border-radius: 3px 0 0 3px;
+ border-radius: 3px 0 0 3px;
+}
+.input-prepend .active, .input-append .active {
+ background: #a9dba9;
+ border-color: #46a546;
+}
+.input-prepend .add-on {
+ *margin-top: 1px;
+ /* IE6-7 */
+
+}
+.input-append input {
+ float: left;
+ -webkit-border-radius: 3px 0 0 3px;
+ -moz-border-radius: 3px 0 0 3px;
+ border-radius: 3px 0 0 3px;
+}
+.input-append .add-on {
+ -webkit-border-radius: 0 3px 3px 0;
+ -moz-border-radius: 0 3px 3px 0;
+ border-radius: 0 3px 3px 0;
+ margin-right: 0;
+ margin-left: -1px;
+}
+.inputs-list {
+ margin: 0 0 5px;
+ width: 100%;
+}
+.inputs-list li {
+ display: block;
+ padding: 0;
+ width: 100%;
+}
+.inputs-list label {
+ display: block;
+ float: none;
+ width: auto;
+ padding: 0;
+ margin-left: 20px;
+ line-height: 18px;
+ text-align: left;
+ white-space: normal;
+}
+.inputs-list label strong {
+ color: #808080;
+}
+.inputs-list label small {
+ font-size: 11px;
+ font-weight: normal;
+}
+.inputs-list .inputs-list {
+ margin-left: 25px;
+ margin-bottom: 10px;
+ padding-top: 0;
+}
+.inputs-list:first-child {
+ padding-top: 6px;
+}
+.inputs-list li + li {
+ padding-top: 2px;
+}
+.inputs-list input[type=radio], .inputs-list input[type=checkbox] {
+ margin-bottom: 0;
+ margin-left: -20px;
+ float: left;
+}
+.form-stacked {
+ padding-left: 20px;
+}
+.form-stacked fieldset {
+ padding-top: 9px;
+}
+.form-stacked legend {
+ padding-left: 0;
+}
+.form-stacked label {
+ display: block;
+ float: none;
+ width: auto;
+ font-weight: bold;
+ text-align: left;
+ line-height: 20px;
+ padding-top: 0;
+}
+.form-stacked .clearfix {
+ margin-bottom: 9px;
+}
+.form-stacked .clearfix div.input {
+ margin-left: 0;
+}
+.form-stacked .inputs-list {
+ margin-bottom: 0;
+}
+.form-stacked .inputs-list li {
+ padding-top: 0;