pliny 0.0.1.pre

Sign up to get free protection for your applications and to get access to all the features.
Files changed (46) hide show
  1. checksums.yaml +7 -0
  2. data/bin/pliny-generate +6 -0
  3. data/lib/pliny.rb +21 -0
  4. data/lib/pliny/commands/generator.rb +197 -0
  5. data/lib/pliny/config_helpers.rb +24 -0
  6. data/lib/pliny/errors.rb +109 -0
  7. data/lib/pliny/extensions/instruments.rb +38 -0
  8. data/lib/pliny/log.rb +84 -0
  9. data/lib/pliny/middleware/cors.rb +46 -0
  10. data/lib/pliny/middleware/request_id.rb +42 -0
  11. data/lib/pliny/middleware/request_store.rb +14 -0
  12. data/lib/pliny/middleware/rescue_errors.rb +24 -0
  13. data/lib/pliny/middleware/timeout.rb +25 -0
  14. data/lib/pliny/middleware/versioning.rb +64 -0
  15. data/lib/pliny/request_store.rb +22 -0
  16. data/lib/pliny/router.rb +15 -0
  17. data/lib/pliny/tasks.rb +3 -0
  18. data/lib/pliny/tasks/db.rake +116 -0
  19. data/lib/pliny/tasks/test.rake +8 -0
  20. data/lib/pliny/templates/endpoint.erb +30 -0
  21. data/lib/pliny/templates/endpoint_acceptance_test.erb +40 -0
  22. data/lib/pliny/templates/endpoint_scaffold.erb +49 -0
  23. data/lib/pliny/templates/endpoint_scaffold_acceptance_test.erb +55 -0
  24. data/lib/pliny/templates/endpoint_test.erb +16 -0
  25. data/lib/pliny/templates/mediator.erb +22 -0
  26. data/lib/pliny/templates/mediator_test.erb +5 -0
  27. data/lib/pliny/templates/migration.erb +9 -0
  28. data/lib/pliny/templates/model.erb +5 -0
  29. data/lib/pliny/templates/model_migration.erb +10 -0
  30. data/lib/pliny/templates/model_test.erb +5 -0
  31. data/lib/pliny/utils.rb +31 -0
  32. data/lib/pliny/version.rb +3 -0
  33. data/test/commands/generator_test.rb +147 -0
  34. data/test/errors_test.rb +24 -0
  35. data/test/extensions/instruments_test.rb +34 -0
  36. data/test/log_test.rb +27 -0
  37. data/test/middleware/cors_test.rb +42 -0
  38. data/test/middleware/request_id_test.rb +28 -0
  39. data/test/middleware/request_store_test.rb +25 -0
  40. data/test/middleware/rescue_errors_test.rb +41 -0
  41. data/test/middleware/timeout_test.rb +32 -0
  42. data/test/middleware/versioning_test.rb +63 -0
  43. data/test/request_store_test.rb +25 -0
  44. data/test/router_test.rb +39 -0
  45. data/test/test_helper.rb +18 -0
  46. 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,14 @@
1
+ module Pliny::Middleware
2
+ class RequestStore
3
+ def initialize(app, options={})
4
+ @app = app
5
+ @store = options[:store] || Pliny::RequestStore
6
+ end
7
+
8
+ def call(env)
9
+ @store.clear!
10
+ @store.seed(env)
11
+ @app.call(env)
12
+ end
13
+ end
14
+ 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
@@ -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
@@ -0,0 +1,3 @@
1
+ Dir[File.expand_path("../tasks", __FILE__) + "/*.rake"].sort.each do |f|
2
+ load(f)
3
+ end
@@ -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,8 @@
1
+ require "rake/testtask"
2
+
3
+ Rake::TestTask.new do |task|
4
+ task.libs << "lib"
5
+ task.libs << "test"
6
+ task.name = :test
7
+ task.test_files = FileList["test/**/*_test.rb"]
8
+ 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