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.
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