herbert 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +5 -0
- data/Gemfile +4 -0
- data/Rakefile +2 -0
- data/herbert.gemspec +32 -0
- data/lib/file.rb +2 -0
- data/lib/herbert.rb +3 -0
- data/lib/herbert/Ajaxify.rb +50 -0
- data/lib/herbert/AppLogger.rb +90 -0
- data/lib/herbert/ApplicationError.rb +61 -0
- data/lib/herbert/Configurator.rb +49 -0
- data/lib/herbert/Error.rb +60 -0
- data/lib/herbert/Jsonify.rb +79 -0
- data/lib/herbert/Log.rb +30 -0
- data/lib/herbert/Resource.rb +49 -0
- data/lib/herbert/Services.rb +90 -0
- data/lib/herbert/Utils.rb +10 -0
- data/lib/herbert/loader.rb +63 -0
- data/lib/herbert/version.rb +3 -0
- metadata +184 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/Rakefile
ADDED
data/herbert.gemspec
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require "herbert/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = "herbert"
|
7
|
+
s.version = Herbert::VERSION
|
8
|
+
s.platform = Gem::Platform::RUBY
|
9
|
+
s.authors = ["Pavel Kalvoda"]
|
10
|
+
s.email = ["me@pavelkalvoda.com","pavel@drinkwithabraham.com"]
|
11
|
+
#s.homepage = ""
|
12
|
+
s.summary = %q{Sinatra-based toolset for creating JSON API servers backed by Mongo & Memcached}
|
13
|
+
s.description = <<-desc
|
14
|
+
Herbert makes development of JSON REST API servers ridiculously simple.
|
15
|
+
It provides a bunch of useful helpers and conventions to speed up development.
|
16
|
+
Input validation, logs and advanced AJAX support are baked in.
|
17
|
+
Herbert is very lightweight and transparent, making it easy to use & modify.
|
18
|
+
desc
|
19
|
+
|
20
|
+
s.add_dependency("sinatra","= 1.2.6")
|
21
|
+
s.add_dependency("memcache-client")
|
22
|
+
s.add_dependency("mongo")
|
23
|
+
s.add_dependency("syslogger")
|
24
|
+
s.add_dependency("kwalify","= 0.7.2")
|
25
|
+
s.add_dependency("activesupport")
|
26
|
+
s.add_dependency("bson_ext",">= 1.3.1")
|
27
|
+
|
28
|
+
s.files = `git ls-files`.split("\n")
|
29
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
30
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
31
|
+
s.require_paths = ["lib"]
|
32
|
+
end
|
data/lib/file.rb
ADDED
data/lib/herbert.rb
ADDED
@@ -0,0 +1,50 @@
|
|
1
|
+
module Herbert
|
2
|
+
module Ajaxify
|
3
|
+
Headers = {
|
4
|
+
'Access-Control-Allow-Methods' => %w{POST GET PUT DELETE OPTIONS},
|
5
|
+
'Access-Control-Allow-Headers' => %w{Content-Type X-Requested-With},
|
6
|
+
'Access-Control-Allow-Origin' => %w{*},
|
7
|
+
'Access-Control-Expose-Header' => %w{Content-Type Content-Length X-Build},
|
8
|
+
'X-Build' => [Herbert::Utils.version]
|
9
|
+
}
|
10
|
+
|
11
|
+
def self.registered(app)
|
12
|
+
# Heeeaderzz!!! Gimme heaaaderzzz!!!
|
13
|
+
path = File.join(app.settings.root, 'config','headers.rb')
|
14
|
+
if File.exists?(path) then
|
15
|
+
log.h_debug("Loading additional headers from #{path}")
|
16
|
+
custom = eval(File.open(path).read)
|
17
|
+
custom.each {|name, value|
|
18
|
+
value = [value] unless value.is_a?(Array)
|
19
|
+
Headers[name] = (Headers[name] || []) | value
|
20
|
+
}
|
21
|
+
else
|
22
|
+
log.h_info("File #{path} doesn't exists; no addition headers loaded")
|
23
|
+
end
|
24
|
+
|
25
|
+
app.before do
|
26
|
+
# Add the headers to the response
|
27
|
+
Headers.each {|name, value|
|
28
|
+
value = [value] unless value.is_a?(Array)
|
29
|
+
value.map! {|val|
|
30
|
+
(val.is_a?(Proc) ? val.call : val).to_s
|
31
|
+
}
|
32
|
+
response[name] = value.join(', ')
|
33
|
+
}
|
34
|
+
end
|
35
|
+
|
36
|
+
# Proxy for not CORS enables services such as
|
37
|
+
# Google Maps
|
38
|
+
# /proxy/<url to fetch>
|
39
|
+
if app.get '/proxy/:url' do
|
40
|
+
url = URI.parse(URI.decode(params[:url]))
|
41
|
+
res = Net::HTTP.start(url.host, 80) {|http|
|
42
|
+
http.get(url.path + (url.query ? '?' + url.query : ''))
|
43
|
+
}
|
44
|
+
response['content-type'] = res['content-type']
|
45
|
+
res.body
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,90 @@
|
|
1
|
+
module Herbert
|
2
|
+
# Full-fledged request & response logger with
|
3
|
+
# several storage providers (console, mongo, cache)
|
4
|
+
class AppLogger
|
5
|
+
def self.provider
|
6
|
+
@@provider
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.provider=(prov)
|
10
|
+
@@provider = prov
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.log(request, response)
|
14
|
+
log = {
|
15
|
+
"request"=> {
|
16
|
+
"path"=> request.path,
|
17
|
+
"method"=> request.request_method,
|
18
|
+
"xhr" => request.xhr?,
|
19
|
+
"postData"=> request.POST,
|
20
|
+
"query"=> request.GET,
|
21
|
+
"headers"=> {},
|
22
|
+
"body"=> {
|
23
|
+
"isJson"=> request.json?,
|
24
|
+
"value"=> request.json? ? request.body : request.body_raw
|
25
|
+
},
|
26
|
+
"client"=> {
|
27
|
+
"ip"=> request.ip,
|
28
|
+
"hostname"=> request.host,
|
29
|
+
"referer"=> request.referer
|
30
|
+
}
|
31
|
+
},
|
32
|
+
"response"=> {
|
33
|
+
"code"=> response.status,
|
34
|
+
"headers"=> response.headers,
|
35
|
+
"body"=> {
|
36
|
+
"isJson"=> response.json?,
|
37
|
+
"value"=> response.body
|
38
|
+
},
|
39
|
+
},
|
40
|
+
"meta"=> {
|
41
|
+
"dateTime"=> Time.new,
|
42
|
+
"processingTime"=> response.app.timer_elapsed.round(3),
|
43
|
+
"port" => request.port
|
44
|
+
}
|
45
|
+
}
|
46
|
+
|
47
|
+
# Extract tha headerz
|
48
|
+
request.env.keys.each do |key|
|
49
|
+
if key =~ /^HTTP_/ then
|
50
|
+
log['request']['headers'][key.gsub(/^HTTP_/,'')] = request.env[key]
|
51
|
+
end
|
52
|
+
end
|
53
|
+
# do not log bodies from GET/DELETE/HEAD
|
54
|
+
log["request"].delete("body") if %{GET DELETE HEAD}.include?(log["request"]["method"])
|
55
|
+
# If an error occured, add it
|
56
|
+
log["response"]["error"] = request.env['sinatra.error'].to_hash if request.env['sinatra.error']
|
57
|
+
id = @@provider.save(log)
|
58
|
+
response['X-RequestId'] = id.to_s if @@provider.respond_to?(:id) && response.app.settings.append_log_id
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
module LoggingProviders
|
63
|
+
class StdoutProvider
|
64
|
+
def initialize
|
65
|
+
require 'pp'
|
66
|
+
end
|
67
|
+
|
68
|
+
def save(log)
|
69
|
+
pp log
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
class MongoProvider
|
74
|
+
|
75
|
+
Collection = 'logs'
|
76
|
+
|
77
|
+
def initialize(db)
|
78
|
+
@db = db
|
79
|
+
end
|
80
|
+
|
81
|
+
def save(log)
|
82
|
+
@db[Collection].save(log)
|
83
|
+
end
|
84
|
+
|
85
|
+
def id
|
86
|
+
true
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
module Herbert
|
2
|
+
|
3
|
+
# Provides centralized handling of exceptions in an
|
4
|
+
# application context
|
5
|
+
module Error
|
6
|
+
# Error relevant in application context
|
7
|
+
class ApplicationError < StandardError
|
8
|
+
attr_reader :code, :message, :http_code, :errors
|
9
|
+
|
10
|
+
# Code to text translation
|
11
|
+
Translation = {
|
12
|
+
"1000" => ["Malformated JSON", 400],
|
13
|
+
"1001" => ["Non-unicode encoding",400],
|
14
|
+
"1002" => ["Non-acceptable Accept header", 406],
|
15
|
+
"1003" => ["Not found", 404],
|
16
|
+
"1010" => ["Missing request body", 400],
|
17
|
+
"1011" => ["Missign required parameter", 400],
|
18
|
+
"1012" => ["Invalid request body", 400],
|
19
|
+
"1020" => ["Unspecified error occured", 500]
|
20
|
+
}
|
21
|
+
|
22
|
+
def initialize(errno, http_code = nil, errors = [])
|
23
|
+
raise ArgumentError, "Unknown error code: #{errno}" unless Translation.has_key?(errno.to_s)
|
24
|
+
@code = errno
|
25
|
+
@message = Translation[@code.to_s][0]
|
26
|
+
@http_code = (http_code || Translation[@code.to_s][1])
|
27
|
+
@errors = errors.to_a
|
28
|
+
end
|
29
|
+
|
30
|
+
def to_hash
|
31
|
+
{ :code => @code, :stackTrace => backtrace, :validationTrace => @errors}
|
32
|
+
end
|
33
|
+
|
34
|
+
# Add an error
|
35
|
+
def self.push(code, error)
|
36
|
+
Translation[code.to_s] = error.to_a
|
37
|
+
end
|
38
|
+
|
39
|
+
# Add a hash of errors
|
40
|
+
def self.merge(errors)
|
41
|
+
if errors.is_a? Hash then
|
42
|
+
Translation.merge!(errors)
|
43
|
+
else
|
44
|
+
raise ArgumentError("Expected a hash of codes and descriptions")
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
module Sinatra
|
53
|
+
class NotFound
|
54
|
+
def to_hash
|
55
|
+
{
|
56
|
+
:code => 1003,
|
57
|
+
:message => "Not found"
|
58
|
+
}
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
module Herbert
|
2
|
+
module Configurator
|
3
|
+
module Prepatch
|
4
|
+
def self.registered(app)
|
5
|
+
# Enable envs such as development;debug, where debug is herberts debug flag
|
6
|
+
env = ENV['RACK_ENV'].split(';')
|
7
|
+
ENV['RACK_ENV'], ENV['HERBERT_DEBUG'] = (env[0].empty? ? 'development' : env[0]), (env[1] == 'debug' ? 1:0).to_s
|
8
|
+
app.set :environment, ENV['RACK_ENV'].downcase.to_sym
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
module Helpers
|
13
|
+
def staging?
|
14
|
+
ENV['RACK_ENV'] == 'staging'
|
15
|
+
end
|
16
|
+
|
17
|
+
def development?
|
18
|
+
ENV['RACK_ENV'] == 'development' || (ENV['RACK_ENV'].empty?)
|
19
|
+
end
|
20
|
+
|
21
|
+
def debug?
|
22
|
+
ENV['HERBERT_DEBUG'] == '1'
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.registered(app)
|
27
|
+
app.enable :logging if app.development?
|
28
|
+
#Assume loading by rackup...
|
29
|
+
app.settings.root ||= File.join(Dir.getwd, 'lib')
|
30
|
+
path = File.join(app.settings.root, 'config')
|
31
|
+
# Load and evaluate common.rb and appropriate settings
|
32
|
+
['common.rb', app.environment.to_s + '.rb'].each do |file|
|
33
|
+
cpath = File.join(path, file)
|
34
|
+
if File.exists?(cpath) then
|
35
|
+
# Ummm, I'm sorry?
|
36
|
+
app.instance_eval(IO.read(cpath))
|
37
|
+
log.h_debug("Applying #{cpath} onto the application")
|
38
|
+
else
|
39
|
+
log.h_warn("Configuration file #{cpath} not found")
|
40
|
+
end
|
41
|
+
end
|
42
|
+
# So, we have all our settings... Please note that configure
|
43
|
+
# block inside an App can overwrite our settings, but Herbert's
|
44
|
+
# services are being created right now, so they only take in account
|
45
|
+
# previous declarations
|
46
|
+
end
|
47
|
+
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
|
3
|
+
require File.dirname(__FILE__) + '/ApplicationError.rb'
|
4
|
+
|
5
|
+
module Herbert
|
6
|
+
module Error
|
7
|
+
# Inclusion hook
|
8
|
+
def self.registered(app)
|
9
|
+
# Disable HTML errors and preliminary reporting
|
10
|
+
log.h_warn("Herbert is running in debugging mode - exceptions will be visualized") if app.debug?
|
11
|
+
app.set :raise_errors, false
|
12
|
+
app.set :show_exceptions, false
|
13
|
+
app.set :dump_errors, app.debug?
|
14
|
+
# Add a new error state handler which produces
|
15
|
+
# compact JSON error reports (handled by #Sinatra::Jsonify)
|
16
|
+
app.error do
|
17
|
+
err = request.env['sinatra.error']
|
18
|
+
if err.class == ApplicationError then
|
19
|
+
log.h_debug("Caught manageable error")
|
20
|
+
response.status = err.http_code
|
21
|
+
body = {
|
22
|
+
:error => {
|
23
|
+
:code => err.code,
|
24
|
+
:message => err.message
|
25
|
+
}
|
26
|
+
}
|
27
|
+
# Add backtrace, Kwalify validation report and other info if
|
28
|
+
# running in development mode
|
29
|
+
if settings.development? then
|
30
|
+
log.h_debug("Adding stacktrace and report to the error")
|
31
|
+
body[:error][:stacktrace] = err.backtrace.join("\n")
|
32
|
+
body[:error][:info] = (err.errors || [])
|
33
|
+
end
|
34
|
+
response.body = body
|
35
|
+
else
|
36
|
+
# If the exception is not manageable, bust it
|
37
|
+
log.h_error("A non-managed error occured! Backtrace: #{err.backtrace.join("\n")}")
|
38
|
+
response.status = 500
|
39
|
+
response.body = settings.development? ? err.to_s : nil
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
#Ummm, nasty.... FIXME
|
44
|
+
app.not_found do
|
45
|
+
content_type 'application/json', :charset => 'utf-8'
|
46
|
+
{:error => {
|
47
|
+
:code => 1003,
|
48
|
+
:message => "Not found"
|
49
|
+
}}
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
module Helpers
|
54
|
+
# Request-context helper of error states
|
55
|
+
def error(code = 1020, http_code = nil, errors = nil)
|
56
|
+
raise Herbert::Error::ApplicationError.new(code, http_code, errors)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
|
2
|
+
class True
|
3
|
+
def to_json
|
4
|
+
return 1
|
5
|
+
end
|
6
|
+
end
|
7
|
+
|
8
|
+
module Sinatra
|
9
|
+
|
10
|
+
# Makes JSON the default DDL of HTTP communication
|
11
|
+
module Jsonify
|
12
|
+
# Sinatra inclusion hook
|
13
|
+
def self.registered(app)
|
14
|
+
app.before do
|
15
|
+
log.h_debug("Adding proper content-type and charset")
|
16
|
+
content_type 'application/json', :charset => 'utf-8'
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
class Sinatra::Request
|
21
|
+
|
22
|
+
@is_json = false
|
23
|
+
# Encapsulates #Rack::Request.body in order to remove #IO.String
|
24
|
+
# and therefore to enable repeated reads
|
25
|
+
def body_raw
|
26
|
+
@body_raw ||= body(true).read
|
27
|
+
@body_raw
|
28
|
+
end
|
29
|
+
|
30
|
+
def ensure_encoded(strict = true)
|
31
|
+
if !@is_json then
|
32
|
+
begin
|
33
|
+
@body_decoded ||= ActiveSupport::JSON.decode(body_raw)
|
34
|
+
@is_json = true;
|
35
|
+
rescue StandardError
|
36
|
+
@is_json = false;
|
37
|
+
raise ::Herbert::Error::ApplicationError.new(1000) if strict
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
# Overrides #Rack::Request.body, returns native #Hash
|
43
|
+
# Preserves access to underlying @env['rack.input'] #IO.String
|
44
|
+
def body(rack = false)
|
45
|
+
if rack then
|
46
|
+
super()
|
47
|
+
else
|
48
|
+
ensure_encoded
|
49
|
+
@body_decoded
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def json?
|
54
|
+
ensure_encoded(false)
|
55
|
+
@is_json
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
class Sinatra::Response
|
60
|
+
# Reference to application instance that created this response
|
61
|
+
attr_accessor :app
|
62
|
+
|
63
|
+
# Automatically encode body to JSON, but only as long as
|
64
|
+
# the content-type remained set to app/json
|
65
|
+
def finish
|
66
|
+
@app.log_request
|
67
|
+
if json?
|
68
|
+
log.h_debug("Serializing response into JSON")
|
69
|
+
@body = [ActiveSupport::JSON.encode(@body)]
|
70
|
+
end
|
71
|
+
super
|
72
|
+
end
|
73
|
+
|
74
|
+
def json?
|
75
|
+
@header['Content-type'] === 'application/json;charset=utf-8'
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
data/lib/herbert/Log.rb
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
module Sinatra
|
2
|
+
module Log
|
3
|
+
def log_request
|
4
|
+
::Herbert::AppLogger.log(request, response) if settings.log_requests
|
5
|
+
end
|
6
|
+
|
7
|
+
def timer_elapsed
|
8
|
+
return (@timer_stop.to_f - @timer_start.to_f)*100
|
9
|
+
end
|
10
|
+
|
11
|
+
module Extension
|
12
|
+
def self.registered(app)
|
13
|
+
case app.log_requests
|
14
|
+
when :db
|
15
|
+
provider = Herbert::LoggingProviders::MongoProvider.new(app.db)
|
16
|
+
when :stdout
|
17
|
+
provider = Herbert::LoggingProviders::StdoutProvider.new
|
18
|
+
else
|
19
|
+
app.log_requests.respond_to?(:save) ? provider = app.log_requests : log.h_fatal("Unknown logs storage provider.")
|
20
|
+
end
|
21
|
+
Herbert::AppLogger.provider = provider
|
22
|
+
# Make the app automatically inject refernce to iteself into the response,
|
23
|
+
# so Sinatra::Response::finish can manipulate it
|
24
|
+
app.before { response.app = self; @timer_start = Time.new }
|
25
|
+
app.after { @timer_stop = Time.new}
|
26
|
+
#app.before { log_request }
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
module Herbert
|
2
|
+
# This class allows you to organize code by REST resources.
|
3
|
+
# Any class that subclasses Herbert::Resource is automatically "merged"
|
4
|
+
# into the application. Resource name will be derived from the class name.
|
5
|
+
#
|
6
|
+
# For instance,
|
7
|
+
# class Messages < Herbert::Resource
|
8
|
+
# get '/' do
|
9
|
+
# "here's a message for you!"
|
10
|
+
# end
|
11
|
+
# end
|
12
|
+
# will respond to
|
13
|
+
# GET /messages/
|
14
|
+
#
|
15
|
+
|
16
|
+
class Resource
|
17
|
+
def self.new
|
18
|
+
raise StandardError.new('You are not allowed to instantize this class directly')
|
19
|
+
end
|
20
|
+
|
21
|
+
# Translates Sintra DSL calls
|
22
|
+
def self.inherited(subclass)
|
23
|
+
%w{get post put delete}.each do |verb|
|
24
|
+
subclass.define_singleton_method verb.to_sym do |route, &block|
|
25
|
+
app.send verb.to_sym, "/#{subclass.to_s.downcase}#{route}", &block
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
# Loads all Herbert resources
|
32
|
+
module ResourceLoader
|
33
|
+
def self.registered(app)
|
34
|
+
# Inject refence to the app into Resource
|
35
|
+
Resource.class_eval do
|
36
|
+
define_singleton_method :app do
|
37
|
+
app
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
# And load all resource definitions
|
42
|
+
path = File.join(app.settings.root, 'Resources')
|
43
|
+
Dir.new(path).each do |file|
|
44
|
+
next if %{. ..}.include? file
|
45
|
+
require File.join(path,file)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,90 @@
|
|
1
|
+
module Sinatra
|
2
|
+
module Database
|
3
|
+
def self.registered(app)
|
4
|
+
app.set :mongo_connection, Mongo::Connection.new(app.settings.db_settings[:host],
|
5
|
+
app.settings.db_settings[:porty],
|
6
|
+
app.settings.db_settings[:options])
|
7
|
+
log.h_debug("Connected to MongoDB #{app.settings.mongo_connection}")
|
8
|
+
app.set :mongo_db, app.settings.mongo_connection.db(app.settings.db_settings[:db_name])
|
9
|
+
end
|
10
|
+
|
11
|
+
def db
|
12
|
+
settings.mongo_db
|
13
|
+
end
|
14
|
+
|
15
|
+
end
|
16
|
+
|
17
|
+
module Cache
|
18
|
+
def self.registered(app)
|
19
|
+
servers = []
|
20
|
+
app.settings.cache[:servers].each {|c|
|
21
|
+
servers << (c[:host] + ':' + (c[:port] || 11211).to_s)
|
22
|
+
}
|
23
|
+
app.set :cache, MemCache.new(app.settings.cache[:options])
|
24
|
+
app.settings.cache.servers = servers
|
25
|
+
log.h_debug("Connected to Memcached #{app.settings.cache.inspect}")
|
26
|
+
end
|
27
|
+
|
28
|
+
def mc
|
29
|
+
settings.cache
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
|
34
|
+
module Validation
|
35
|
+
module Extension
|
36
|
+
# Um, dragons... Fucking swarm... But I'll try to explain this anyway.
|
37
|
+
# We'll scan the defined settings.validation[:path] dir for dirs. Those found dirs
|
38
|
+
# will denote <resource>s. Then, we will scan the "resource" dirs for files.
|
39
|
+
# These files will represent one http <verb>.yaml each. And then, we create hierchy of
|
40
|
+
# validation schemas following this pattern:
|
41
|
+
# ::setting.validation[:module]::<resource>::<verb_schema>
|
42
|
+
# where the <verb_schema> equals <verb>.capitalize and contains parsed contents of <verb>.yaml file.
|
43
|
+
# Please note that I haven't used a single (.*_)eval even though I was terribly tempted.
|
44
|
+
# And I also documented this method. I'm so awesome, considerate and drunk, am I not?
|
45
|
+
# Uh, yea, and notice the nice cascade of 'end's on the end
|
46
|
+
def self.registered(app)
|
47
|
+
# Define the ::<schema_root> module
|
48
|
+
validation_module = Kernel.const_set(app.settings.validation[:module], Module.new)
|
49
|
+
schema_root = Dir.new(File.join(app.settings.root, app.settings.validation[:path]))
|
50
|
+
log.h_debug("Loading validation schemas from #{schema_root.path}");
|
51
|
+
# For each resource
|
52
|
+
schema_root.each do |resource_dir|
|
53
|
+
next if %w{.. .}.include? resource_dir
|
54
|
+
resource_name = resource_dir
|
55
|
+
resource_dir = File.join(schema_root, resource_dir)
|
56
|
+
# Create <schema_root>::<resource> module
|
57
|
+
validation_module.const_set(resource_name, Module.new {})
|
58
|
+
if File.directory?(resource_dir) then
|
59
|
+
Dir.new(resource_dir).each do |verb|
|
60
|
+
next if %w{.. .}.include? verb
|
61
|
+
# And create the <schema_root>::<resource>::<verb_schema> constant
|
62
|
+
validation_module.const_get(resource_name).const_set(/(\w+).yaml/.match(verb)[1].capitalize, YAML.load_file(File.join(resource_dir, verb)))
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
module Helpers
|
70
|
+
# Only a few dragons here. This method validates body of the request
|
71
|
+
# against a schema. If no schema was passed to the method, it will
|
72
|
+
# try to find it automagically
|
73
|
+
def validate!(schema = nil)
|
74
|
+
schema ||= Kernel.const_get(
|
75
|
+
settings.validation[:module]
|
76
|
+
).const_get(
|
77
|
+
/^\/(.*)\//.match(request.path)[1].capitalize
|
78
|
+
).const_get(
|
79
|
+
request.env['REQUEST_METHOD'].downcase.capitalize
|
80
|
+
)
|
81
|
+
res = Kwalify::Validator.new(schema).validate(request.body)
|
82
|
+
res.map! { |error|
|
83
|
+
error.to_s
|
84
|
+
}
|
85
|
+
error(1012, nil, res) unless res == []
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
require 'logger'
|
2
|
+
require 'mongo'
|
3
|
+
require 'memcache'
|
4
|
+
require 'sinatra/base'
|
5
|
+
require 'kwalify'
|
6
|
+
require 'active_support'
|
7
|
+
|
8
|
+
module Herbert
|
9
|
+
::Logger.class_eval do
|
10
|
+
# prefix all Herbert's log with [Herbert]
|
11
|
+
[:fatal, :error, :warn, :info, :debug].each do |type|
|
12
|
+
name = "h_" + type.to_s
|
13
|
+
define_method name do |message|
|
14
|
+
send(type, "[Herbert] " + message)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
# Plug it in, the monkey style :]
|
20
|
+
module ::Kernel
|
21
|
+
@@logger = Logger.new(STDOUT)
|
22
|
+
def log
|
23
|
+
@@logger
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
module Loader
|
28
|
+
$HERBERT_PATH = File.dirname(__FILE__)
|
29
|
+
log.h_info("Here comes Herbert. He's a berserker!")
|
30
|
+
# because order matters
|
31
|
+
%w{Utils Jsonify Configurator Error Services Ajaxify AppLogger Log Resource}.each {|file|
|
32
|
+
require $HERBERT_PATH + "/#{file}.rb"
|
33
|
+
}
|
34
|
+
|
35
|
+
def self.registered(app)
|
36
|
+
# Set some default
|
37
|
+
# TODO to external file?
|
38
|
+
app.set :log_requests, :db
|
39
|
+
app.enable :append_log_id # If logs go to Mongo, IDs will be appended to responses
|
40
|
+
## register the ;debug flag patch first to enable proper logging
|
41
|
+
app.register Herbert::Configurator::Prepatch
|
42
|
+
# the logger
|
43
|
+
log.level = app.development? ? Logger::DEBUG : Logger::INFO
|
44
|
+
# the extensions
|
45
|
+
app.register Herbert::Configurator
|
46
|
+
app.register Herbert::Configurator::Helpers
|
47
|
+
app.helpers Herbert::Configurator::Helpers
|
48
|
+
app.register Herbert::Error
|
49
|
+
app.helpers Herbert::Error::Helpers
|
50
|
+
app.register Sinatra::Jsonify
|
51
|
+
app.register Sinatra::Database
|
52
|
+
app.helpers Sinatra::Database
|
53
|
+
app.register Sinatra::Cache
|
54
|
+
app.helpers Sinatra::Cache
|
55
|
+
app.register Sinatra::Validation::Extension
|
56
|
+
app.helpers Sinatra::Validation::Helpers
|
57
|
+
app.register Herbert::Ajaxify
|
58
|
+
app.helpers Sinatra::Log
|
59
|
+
app.register Sinatra::Log::Extension
|
60
|
+
app.register Herbert::ResourceLoader
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
metadata
ADDED
@@ -0,0 +1,184 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: herbert
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
prerelease: false
|
5
|
+
segments:
|
6
|
+
- 0
|
7
|
+
- 0
|
8
|
+
- 1
|
9
|
+
version: 0.0.1
|
10
|
+
platform: ruby
|
11
|
+
authors:
|
12
|
+
- Pavel Kalvoda
|
13
|
+
autorequire:
|
14
|
+
bindir: bin
|
15
|
+
cert_chain: []
|
16
|
+
|
17
|
+
date: 2011-06-18 00:00:00 +02:00
|
18
|
+
default_executable:
|
19
|
+
dependencies:
|
20
|
+
- !ruby/object:Gem::Dependency
|
21
|
+
name: sinatra
|
22
|
+
prerelease: false
|
23
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
24
|
+
none: false
|
25
|
+
requirements:
|
26
|
+
- - "="
|
27
|
+
- !ruby/object:Gem::Version
|
28
|
+
segments:
|
29
|
+
- 1
|
30
|
+
- 2
|
31
|
+
- 6
|
32
|
+
version: 1.2.6
|
33
|
+
type: :runtime
|
34
|
+
version_requirements: *id001
|
35
|
+
- !ruby/object:Gem::Dependency
|
36
|
+
name: memcache-client
|
37
|
+
prerelease: false
|
38
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
39
|
+
none: false
|
40
|
+
requirements:
|
41
|
+
- - ">="
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
segments:
|
44
|
+
- 0
|
45
|
+
version: "0"
|
46
|
+
type: :runtime
|
47
|
+
version_requirements: *id002
|
48
|
+
- !ruby/object:Gem::Dependency
|
49
|
+
name: mongo
|
50
|
+
prerelease: false
|
51
|
+
requirement: &id003 !ruby/object:Gem::Requirement
|
52
|
+
none: false
|
53
|
+
requirements:
|
54
|
+
- - ">="
|
55
|
+
- !ruby/object:Gem::Version
|
56
|
+
segments:
|
57
|
+
- 0
|
58
|
+
version: "0"
|
59
|
+
type: :runtime
|
60
|
+
version_requirements: *id003
|
61
|
+
- !ruby/object:Gem::Dependency
|
62
|
+
name: syslogger
|
63
|
+
prerelease: false
|
64
|
+
requirement: &id004 !ruby/object:Gem::Requirement
|
65
|
+
none: false
|
66
|
+
requirements:
|
67
|
+
- - ">="
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
segments:
|
70
|
+
- 0
|
71
|
+
version: "0"
|
72
|
+
type: :runtime
|
73
|
+
version_requirements: *id004
|
74
|
+
- !ruby/object:Gem::Dependency
|
75
|
+
name: kwalify
|
76
|
+
prerelease: false
|
77
|
+
requirement: &id005 !ruby/object:Gem::Requirement
|
78
|
+
none: false
|
79
|
+
requirements:
|
80
|
+
- - "="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
segments:
|
83
|
+
- 0
|
84
|
+
- 7
|
85
|
+
- 2
|
86
|
+
version: 0.7.2
|
87
|
+
type: :runtime
|
88
|
+
version_requirements: *id005
|
89
|
+
- !ruby/object:Gem::Dependency
|
90
|
+
name: activesupport
|
91
|
+
prerelease: false
|
92
|
+
requirement: &id006 !ruby/object:Gem::Requirement
|
93
|
+
none: false
|
94
|
+
requirements:
|
95
|
+
- - ">="
|
96
|
+
- !ruby/object:Gem::Version
|
97
|
+
segments:
|
98
|
+
- 0
|
99
|
+
version: "0"
|
100
|
+
type: :runtime
|
101
|
+
version_requirements: *id006
|
102
|
+
- !ruby/object:Gem::Dependency
|
103
|
+
name: bson_ext
|
104
|
+
prerelease: false
|
105
|
+
requirement: &id007 !ruby/object:Gem::Requirement
|
106
|
+
none: false
|
107
|
+
requirements:
|
108
|
+
- - ">="
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
segments:
|
111
|
+
- 1
|
112
|
+
- 3
|
113
|
+
- 1
|
114
|
+
version: 1.3.1
|
115
|
+
type: :runtime
|
116
|
+
version_requirements: *id007
|
117
|
+
description: |
|
118
|
+
Herbert makes development of JSON REST API servers ridiculously simple.
|
119
|
+
It provides a bunch of useful helpers and conventions to speed up development.
|
120
|
+
Input validation, logs and advanced AJAX support are baked in.
|
121
|
+
Herbert is very lightweight and transparent, making it easy to use & modify.
|
122
|
+
|
123
|
+
email:
|
124
|
+
- me@pavelkalvoda.com
|
125
|
+
- pavel@drinkwithabraham.com
|
126
|
+
executables: []
|
127
|
+
|
128
|
+
extensions: []
|
129
|
+
|
130
|
+
extra_rdoc_files: []
|
131
|
+
|
132
|
+
files:
|
133
|
+
- .gitignore
|
134
|
+
- Gemfile
|
135
|
+
- Rakefile
|
136
|
+
- herbert.gemspec
|
137
|
+
- lib/file.rb
|
138
|
+
- lib/herbert.rb
|
139
|
+
- lib/herbert/Ajaxify.rb
|
140
|
+
- lib/herbert/AppLogger.rb
|
141
|
+
- lib/herbert/ApplicationError.rb
|
142
|
+
- lib/herbert/Configurator.rb
|
143
|
+
- lib/herbert/Error.rb
|
144
|
+
- lib/herbert/Jsonify.rb
|
145
|
+
- lib/herbert/Log.rb
|
146
|
+
- lib/herbert/Resource.rb
|
147
|
+
- lib/herbert/Services.rb
|
148
|
+
- lib/herbert/Utils.rb
|
149
|
+
- lib/herbert/loader.rb
|
150
|
+
- lib/herbert/version.rb
|
151
|
+
has_rdoc: true
|
152
|
+
homepage:
|
153
|
+
licenses: []
|
154
|
+
|
155
|
+
post_install_message:
|
156
|
+
rdoc_options: []
|
157
|
+
|
158
|
+
require_paths:
|
159
|
+
- lib
|
160
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
161
|
+
none: false
|
162
|
+
requirements:
|
163
|
+
- - ">="
|
164
|
+
- !ruby/object:Gem::Version
|
165
|
+
segments:
|
166
|
+
- 0
|
167
|
+
version: "0"
|
168
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
169
|
+
none: false
|
170
|
+
requirements:
|
171
|
+
- - ">="
|
172
|
+
- !ruby/object:Gem::Version
|
173
|
+
segments:
|
174
|
+
- 0
|
175
|
+
version: "0"
|
176
|
+
requirements: []
|
177
|
+
|
178
|
+
rubyforge_project:
|
179
|
+
rubygems_version: 1.3.7
|
180
|
+
signing_key:
|
181
|
+
specification_version: 3
|
182
|
+
summary: Sinatra-based toolset for creating JSON API servers backed by Mongo & Memcached
|
183
|
+
test_files: []
|
184
|
+
|