herbert 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|
+
|