pliny 0.0.1.pre
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.
- 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
|