pliny 0.0.1.pre
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/bin/pliny-generate +6 -0
- data/lib/pliny.rb +21 -0
- data/lib/pliny/commands/generator.rb +197 -0
- data/lib/pliny/config_helpers.rb +24 -0
- data/lib/pliny/errors.rb +109 -0
- data/lib/pliny/extensions/instruments.rb +38 -0
- data/lib/pliny/log.rb +84 -0
- data/lib/pliny/middleware/cors.rb +46 -0
- data/lib/pliny/middleware/request_id.rb +42 -0
- data/lib/pliny/middleware/request_store.rb +14 -0
- data/lib/pliny/middleware/rescue_errors.rb +24 -0
- data/lib/pliny/middleware/timeout.rb +25 -0
- data/lib/pliny/middleware/versioning.rb +64 -0
- data/lib/pliny/request_store.rb +22 -0
- data/lib/pliny/router.rb +15 -0
- data/lib/pliny/tasks.rb +3 -0
- data/lib/pliny/tasks/db.rake +116 -0
- data/lib/pliny/tasks/test.rake +8 -0
- data/lib/pliny/templates/endpoint.erb +30 -0
- data/lib/pliny/templates/endpoint_acceptance_test.erb +40 -0
- data/lib/pliny/templates/endpoint_scaffold.erb +49 -0
- data/lib/pliny/templates/endpoint_scaffold_acceptance_test.erb +55 -0
- data/lib/pliny/templates/endpoint_test.erb +16 -0
- data/lib/pliny/templates/mediator.erb +22 -0
- data/lib/pliny/templates/mediator_test.erb +5 -0
- data/lib/pliny/templates/migration.erb +9 -0
- data/lib/pliny/templates/model.erb +5 -0
- data/lib/pliny/templates/model_migration.erb +10 -0
- data/lib/pliny/templates/model_test.erb +5 -0
- data/lib/pliny/utils.rb +31 -0
- data/lib/pliny/version.rb +3 -0
- data/test/commands/generator_test.rb +147 -0
- data/test/errors_test.rb +24 -0
- data/test/extensions/instruments_test.rb +34 -0
- data/test/log_test.rb +27 -0
- data/test/middleware/cors_test.rb +42 -0
- data/test/middleware/request_id_test.rb +28 -0
- data/test/middleware/request_store_test.rb +25 -0
- data/test/middleware/rescue_errors_test.rb +41 -0
- data/test/middleware/timeout_test.rb +32 -0
- data/test/middleware/versioning_test.rb +63 -0
- data/test/request_store_test.rb +25 -0
- data/test/router_test.rb +39 -0
- data/test/test_helper.rb +18 -0
- metadata +252 -0
@@ -0,0 +1,46 @@
|
|
1
|
+
module Pliny::Middleware
|
2
|
+
class CORS
|
3
|
+
|
4
|
+
ALLOW_METHODS =
|
5
|
+
%w( GET POST PUT PATCH DELETE OPTIONS ).freeze
|
6
|
+
ALLOW_HEADERS =
|
7
|
+
%w( * Content-Type Accept AUTHORIZATION Cache-Control ).freeze
|
8
|
+
EXPOSE_HEADERS =
|
9
|
+
%w( Cache-Control Content-Language Content-Type Expires Last-Modified Pragma ).freeze
|
10
|
+
|
11
|
+
def initialize(app)
|
12
|
+
@app = app
|
13
|
+
end
|
14
|
+
|
15
|
+
def call(env)
|
16
|
+
# preflight request: render a stub 200 with the CORS headers
|
17
|
+
if cors_request?(env) && env["REQUEST_METHOD"] == "OPTIONS"
|
18
|
+
[200, cors_headers(env), [""]]
|
19
|
+
else
|
20
|
+
status, headers, response = @app.call(env)
|
21
|
+
|
22
|
+
# regualar CORS request: append CORS headers to response
|
23
|
+
if cors_request?(env)
|
24
|
+
headers.merge!(cors_headers(env))
|
25
|
+
end
|
26
|
+
|
27
|
+
[status, headers, response]
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def cors_request?(env)
|
32
|
+
env.has_key?("HTTP_ORIGIN")
|
33
|
+
end
|
34
|
+
|
35
|
+
def cors_headers(env)
|
36
|
+
{
|
37
|
+
'Access-Control-Allow-Origin' => env["HTTP_ORIGIN"],
|
38
|
+
'Access-Control-Allow-Methods' => ALLOW_METHODS.join(', '),
|
39
|
+
'Access-Control-Allow-Headers' => ALLOW_HEADERS.join(', '),
|
40
|
+
'Access-Control-Allow-Credentials' => "true",
|
41
|
+
'Access-Control-Max-Age' => "1728000",
|
42
|
+
'Access-Control-Expose-Headers' => EXPOSE_HEADERS.join(', ')
|
43
|
+
}
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# please add changes here to core's Instruments as well
|
2
|
+
|
3
|
+
module Pliny::Middleware
|
4
|
+
class RequestID
|
5
|
+
UUID_PATTERN =
|
6
|
+
/\A[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}\Z/
|
7
|
+
|
8
|
+
def initialize(app)
|
9
|
+
@app = app
|
10
|
+
end
|
11
|
+
|
12
|
+
def call(env)
|
13
|
+
request_ids = [SecureRandom.uuid] + extract_request_ids(env)
|
14
|
+
|
15
|
+
# make ID of the request accessible to consumers down the stack
|
16
|
+
env["REQUEST_ID"] = request_ids[0]
|
17
|
+
|
18
|
+
# Extract request IDs from incoming headers as well. Can be used for
|
19
|
+
# identifying a request across a number of components in SOA.
|
20
|
+
env["REQUEST_IDS"] = request_ids
|
21
|
+
|
22
|
+
status, headers, response = @app.call(env)
|
23
|
+
|
24
|
+
# tag all responses with a request ID
|
25
|
+
headers["Request-Id"] = request_ids[0]
|
26
|
+
|
27
|
+
[status, headers, response]
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def extract_request_ids(env)
|
33
|
+
request_ids = []
|
34
|
+
if env["HTTP_REQUEST_ID"]
|
35
|
+
request_ids = env["HTTP_REQUEST_ID"].split(",")
|
36
|
+
request_ids.map! { |id| id.strip }
|
37
|
+
request_ids.select! { |id| id =~ UUID_PATTERN }
|
38
|
+
end
|
39
|
+
request_ids
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module Pliny::Middleware
|
2
|
+
class RescueErrors
|
3
|
+
def initialize(app)
|
4
|
+
@app = app
|
5
|
+
end
|
6
|
+
|
7
|
+
def call(env)
|
8
|
+
@app.call(env)
|
9
|
+
rescue Pliny::Errors::Error => e
|
10
|
+
render(e, env)
|
11
|
+
rescue Exception => e
|
12
|
+
# Pliny.log_exception(e)
|
13
|
+
render(Pliny::Errors::InternalServerError.new, env)
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
def render(e, env)
|
19
|
+
headers = { "Content-Type" => "application/json; charset=utf-8" }
|
20
|
+
error = { id: e.id, message: e.message, status: e.status }
|
21
|
+
[e.status, headers, [MultiJson.encode(error)]]
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
require "timeout"
|
2
|
+
|
3
|
+
module Pliny::Middleware
|
4
|
+
# Requires that Pliny::Middleware::RescueErrors is nested above it.
|
5
|
+
class Timeout
|
6
|
+
def initialize(app, options={})
|
7
|
+
@app = app
|
8
|
+
@timeout = options[:timeout] || 45
|
9
|
+
end
|
10
|
+
|
11
|
+
def call(env)
|
12
|
+
::Timeout.timeout(@timeout, RequestTimeout) do
|
13
|
+
@app.call(env)
|
14
|
+
end
|
15
|
+
rescue RequestTimeout
|
16
|
+
# Pliny::Sample.measure "requests.timeouts"
|
17
|
+
raise Pliny::Errors::ServiceUnavailable, "Timeout reached."
|
18
|
+
end
|
19
|
+
|
20
|
+
# use a custom Timeout class so it can't be rescued accidentally by
|
21
|
+
# internal calls
|
22
|
+
class RequestTimeout < Exception
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
require 'http_accept'
|
2
|
+
|
3
|
+
module Pliny::Middleware
|
4
|
+
class Versioning
|
5
|
+
def initialize(app, options={})
|
6
|
+
@app = app
|
7
|
+
@default = options[:default] || raise("missing=default")
|
8
|
+
@app_name = options[:app_name] || raise("missing=app_name")
|
9
|
+
end
|
10
|
+
|
11
|
+
def call(env)
|
12
|
+
Pliny.log middleware: :versioning, id: env["REQUEST_ID"] do
|
13
|
+
detect_api_version(env)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def detect_api_version(env)
|
20
|
+
media_types = HTTPAccept.parse(env["HTTP_ACCEPT"])
|
21
|
+
|
22
|
+
version = nil
|
23
|
+
media_types.map! do |media_type|
|
24
|
+
if accept_headers.include?(media_type.format)
|
25
|
+
if !media_type.params["version"]
|
26
|
+
error = { id: :bad_version, message: <<-eos }
|
27
|
+
Please specify a version along with the MIME type. For example, `Accept: application/vnd.#{@app_name}+json; version=1`.
|
28
|
+
eos
|
29
|
+
return [400, { "Content-Type" => "application/json; charset=utf-8" },
|
30
|
+
[MultiJson.encode(error)]]
|
31
|
+
end
|
32
|
+
|
33
|
+
if !version
|
34
|
+
version = media_type.params["version"]
|
35
|
+
end
|
36
|
+
|
37
|
+
# replace the MIME with a simplified version for easier
|
38
|
+
# parsing down the stack
|
39
|
+
media_type.format = "application/json"
|
40
|
+
media_type.params.delete("version")
|
41
|
+
end
|
42
|
+
media_type.to_s
|
43
|
+
end
|
44
|
+
env["HTTP_ACCEPT"] = media_types.map(&:to_s).join(", ")
|
45
|
+
|
46
|
+
version ||= @default
|
47
|
+
set_api_version(env, version)
|
48
|
+
@app.call(env)
|
49
|
+
end
|
50
|
+
|
51
|
+
def set_api_version(env, version)
|
52
|
+
# API modules will look for the version in env
|
53
|
+
env["HTTP_X_API_VERSION"] = version
|
54
|
+
end
|
55
|
+
|
56
|
+
def accept_headers
|
57
|
+
[
|
58
|
+
"application/vnd.#{@app_name}",
|
59
|
+
"application/vnd.#{@app_name}+json",
|
60
|
+
"application/*+json",
|
61
|
+
]
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module Pliny
|
2
|
+
module RequestStore
|
3
|
+
def self.clear!
|
4
|
+
Thread.current[:request_store] = {}
|
5
|
+
end
|
6
|
+
|
7
|
+
def self.seed(env)
|
8
|
+
store[:request_id] =
|
9
|
+
env["REQUEST_IDS"] ? env["REQUEST_IDS"].join(",") : nil
|
10
|
+
|
11
|
+
# a global context that evolves over the lifetime of the request, and is
|
12
|
+
# used to tag all log messages that it produces
|
13
|
+
store[:log_context] = {
|
14
|
+
request_id: store[:request_id]
|
15
|
+
}
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.store
|
19
|
+
Thread.current[:request_store] ||= {}
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
data/lib/pliny/router.rb
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
require 'sinatra/router'
|
2
|
+
|
3
|
+
module Pliny
|
4
|
+
class Router < Sinatra::Router
|
5
|
+
|
6
|
+
# yield to a builder block in which all defined apps will only respond for
|
7
|
+
# the given version
|
8
|
+
def version(*versions, &block)
|
9
|
+
condition = lambda { |env|
|
10
|
+
versions.include?(env["HTTP_X_API_VERSION"])
|
11
|
+
}
|
12
|
+
with_conditions(condition, &block)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
data/lib/pliny/tasks.rb
ADDED
@@ -0,0 +1,116 @@
|
|
1
|
+
require "sequel"
|
2
|
+
require "sequel/extensions/migration"
|
3
|
+
require "uri"
|
4
|
+
|
5
|
+
require "pliny/utils"
|
6
|
+
|
7
|
+
namespace :db do
|
8
|
+
desc "Run database migrations"
|
9
|
+
task :migrate do
|
10
|
+
next if Dir["./db/migrate/*.rb"].empty?
|
11
|
+
database_urls.each do |database_url|
|
12
|
+
db = Sequel.connect(database_url)
|
13
|
+
Sequel::Migrator.apply(db, "./db/migrate")
|
14
|
+
puts "Migrated `#{name_from_uri(database_url)}`"
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
desc "Rollback the database"
|
19
|
+
task :rollback do
|
20
|
+
next if Dir["./db/migrate/*.rb"].empty?
|
21
|
+
database_urls.each do |database_url|
|
22
|
+
db = Sequel.connect(database_url)
|
23
|
+
Sequel::Migrator.apply(db, "./db/migrate", -1)
|
24
|
+
puts "Rolled back `#{name_from_uri(database_url)}`"
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
desc "Nuke the database (drop all tables)"
|
29
|
+
task :nuke do
|
30
|
+
database_urls.each do |database_url|
|
31
|
+
db = Sequel.connect(database_url)
|
32
|
+
db.tables.each do |table|
|
33
|
+
db.run(%{DROP TABLE "#{table}"})
|
34
|
+
end
|
35
|
+
puts "Nuked `#{name_from_uri(database_url)}`"
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
desc "Reset the database"
|
40
|
+
task :reset => [:nuke, :migrate]
|
41
|
+
|
42
|
+
desc "Create the database"
|
43
|
+
task :create do
|
44
|
+
db = Sequel.connect("postgres://localhost/postgres")
|
45
|
+
database_urls.each do |database_url|
|
46
|
+
exists = false
|
47
|
+
name = name_from_uri(database_url)
|
48
|
+
begin
|
49
|
+
db.run(%{CREATE DATABASE "#{name}"})
|
50
|
+
rescue Sequel::DatabaseError
|
51
|
+
raise unless $!.message =~ /already exists/
|
52
|
+
exists = true
|
53
|
+
end
|
54
|
+
puts "Created `#{name}`" if !exists
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
desc "Drop the database"
|
59
|
+
task :drop do
|
60
|
+
db = Sequel.connect("postgres://localhost/postgres")
|
61
|
+
database_urls.each do |database_url|
|
62
|
+
name = name_from_uri(database_url)
|
63
|
+
db.run(%{DROP DATABASE IF EXISTS "#{name}"})
|
64
|
+
puts "Dropped `#{name}`"
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
namespace :schema do
|
69
|
+
desc "Load the database schema"
|
70
|
+
task :load do
|
71
|
+
schema = File.read("./db/schema.sql")
|
72
|
+
database_urls.each do |database_url|
|
73
|
+
db = Sequel.connect(database_url)
|
74
|
+
db.run(schema)
|
75
|
+
puts "Loaded `#{name_from_uri(database_url)}`"
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
desc "Dump the database schema"
|
80
|
+
task :dump do
|
81
|
+
database_url = database_urls.first
|
82
|
+
`pg_dump -i -s -x -O -f ./db/schema.sql #{database_url}`
|
83
|
+
puts "Dumped `#{name_from_uri(database_url)}` to db/schema.sql"
|
84
|
+
end
|
85
|
+
|
86
|
+
desc "Merges migrations into schema and removes them"
|
87
|
+
task :merge => ["db:setup", "db:schema:load", "db:migrate", "db:schema:dump"] do
|
88
|
+
FileUtils.rm Dir["./db/migrate/*.rb"]
|
89
|
+
puts "Removed migrations"
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
desc "Setup the database"
|
94
|
+
task :setup => [:drop, :create]
|
95
|
+
|
96
|
+
private
|
97
|
+
|
98
|
+
def database_urls
|
99
|
+
if ENV["DATABASE_URL"]
|
100
|
+
[ENV["DATABASE_URL"]]
|
101
|
+
else
|
102
|
+
%w(.env .env.test).map { |env_file|
|
103
|
+
env_path = "./#{env_file}"
|
104
|
+
if File.exists?(env_path)
|
105
|
+
Pliny::Utils.parse_env(env_path)["DATABASE_URL"]
|
106
|
+
else
|
107
|
+
nil
|
108
|
+
end
|
109
|
+
}.compact
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
def name_from_uri(uri)
|
114
|
+
URI.parse(uri).path[1..-1]
|
115
|
+
end
|
116
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module Endpoints
|
2
|
+
class <%= plural_class_name %> < Base
|
3
|
+
namespace "<%= url_path %>" do
|
4
|
+
before do
|
5
|
+
content_type :json
|
6
|
+
end
|
7
|
+
|
8
|
+
get do
|
9
|
+
"[]"
|
10
|
+
end
|
11
|
+
|
12
|
+
post do
|
13
|
+
status 201
|
14
|
+
"{}"
|
15
|
+
end
|
16
|
+
|
17
|
+
get "/:id" do
|
18
|
+
"{}"
|
19
|
+
end
|
20
|
+
|
21
|
+
patch "/:id" do |id|
|
22
|
+
"{}"
|
23
|
+
end
|
24
|
+
|
25
|
+
delete "/:id" do |id|
|
26
|
+
"{}"
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe Endpoints::<%= plural_class_name %> do
|
4
|
+
include Committee::Test::Methods
|
5
|
+
include Rack::Test::Methods
|
6
|
+
|
7
|
+
def app
|
8
|
+
Routes
|
9
|
+
end
|
10
|
+
|
11
|
+
it "GET <%= url_path %>" do
|
12
|
+
get "<%= url_path %>"
|
13
|
+
last_response.status.should eq(200)
|
14
|
+
last_response.body.should eq("[]")
|
15
|
+
end
|
16
|
+
|
17
|
+
it "POST <%= url_path %>/:id" do
|
18
|
+
post "<%= url_path %>"
|
19
|
+
last_response.status.should eq(201)
|
20
|
+
last_response.body.should eq("{}")
|
21
|
+
end
|
22
|
+
|
23
|
+
it "GET <%= url_path %>/:id" do
|
24
|
+
get "<%= url_path %>/123"
|
25
|
+
last_response.status.should eq(200)
|
26
|
+
last_response.body.should eq("{}")
|
27
|
+
end
|
28
|
+
|
29
|
+
it "PATCH <%= url_path %>/:id" do
|
30
|
+
patch "<%= url_path %>/123"
|
31
|
+
last_response.status.should eq(200)
|
32
|
+
last_response.body.should eq("{}")
|
33
|
+
end
|
34
|
+
|
35
|
+
it "DELETE <%= url_path %>/:id" do
|
36
|
+
delete "<%= url_path %>/123"
|
37
|
+
last_response.status.should eq(200)
|
38
|
+
last_response.body.should eq("{}")
|
39
|
+
end
|
40
|
+
end
|