napa 0.2.1 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +20 -0
- data/LICENSE +4 -2
- data/README.md +25 -2
- data/docs/quickstart.md +27 -26
- data/lib/napa.rb +6 -2
- data/lib/napa/active_record_extensions/notifications_subscriber.rb +17 -0
- data/lib/napa/active_record_extensions/stats.rb +1 -14
- data/lib/napa/cli.rb +5 -2
- data/lib/napa/generators/api_generator.rb +5 -1
- data/lib/napa/generators/migration_generator.rb +6 -2
- data/lib/napa/generators/scaffold_generator.rb +4 -3
- data/lib/napa/generators/templates/api/app/apis/%name_tableize%_api.rb.tt +4 -5
- data/lib/napa/generators/templates/api/app/models/%name_underscore%.rb.tt +0 -1
- data/lib/napa/generators/templates/api/spec/apis/%name_tableize%_api_spec.rb.tt +16 -0
- data/lib/napa/generators/templates/api/spec/models/%name_underscore%_spec.rb.tt +9 -0
- data/lib/napa/generators/templates/migration/%migration_filename%.rb.tt +1 -1
- data/lib/napa/generators/templates/scaffold/.gitignore.tt +2 -0
- data/lib/napa/generators/templates/scaffold/Gemfile.tt +2 -2
- data/lib/napa/generators/templates/scaffold/app.rb +1 -1
- data/lib/napa/generators/templates/scaffold/config.ru.tt +1 -1
- data/lib/napa/generators/templates/scaffold/config/database.yml.tt +1 -0
- data/lib/napa/generators/templates/scaffold/spec/apis/hello_api_spec.rb.tt +1 -1
- data/lib/napa/generators/templates/scaffold/spec/spec_helper.rb +15 -2
- data/lib/napa/grape_extenders.rb +6 -2
- data/lib/napa/grape_extensions/error_formatter.rb +1 -1
- data/lib/napa/grape_extensions/grape_helpers.rb +8 -1
- data/lib/napa/logger/logger.rb +10 -0
- data/lib/napa/logger/parseable.rb +37 -0
- data/lib/napa/middleware/app_monitor.rb +1 -1
- data/lib/napa/middleware/database_stats.rb +15 -0
- data/lib/napa/middleware/logger.rb +6 -11
- data/lib/napa/middleware/request_stats.rb +7 -5
- data/lib/napa/{grape_extensions → output_formatters}/entity.rb +0 -0
- data/lib/napa/output_formatters/include_nil.rb +16 -0
- data/lib/napa/{grape_extensions → output_formatters}/representer.rb +2 -2
- data/lib/napa/rspec_extensions/response_helpers.rb +17 -0
- data/lib/napa/stats.rb +21 -1
- data/lib/napa/version.rb +1 -1
- data/lib/tasks/db.rake +7 -0
- data/lib/tasks/git.rake +3 -0
- data/napa.gemspec +0 -1
- data/spec/generators/api_generator_spec.rb +63 -0
- data/spec/generators/migration_generator_spec.rb +27 -0
- data/spec/generators/scaffold_generator_spec.rb +90 -0
- data/spec/grape_extensions/error_formatter_spec.rb +8 -0
- data/spec/grape_extensions/include_nil_spec.rb +23 -0
- data/spec/logger/logger_spec.rb +14 -0
- data/spec/logger/parseable_spec.rb +16 -0
- data/spec/middleware/database_stats_spec.rb +64 -0
- data/spec/middleware/request_stats_spec.rb +4 -5
- data/spec/spec_helper.rb +26 -0
- data/spec/stats_spec.rb +25 -1
- metadata +25 -20
- data/spec/active_record_extensions/stats_spec.rb +0 -59
@@ -10,7 +10,7 @@ describe HelloApi do
|
|
10
10
|
describe 'GET /hello' do
|
11
11
|
it 'returns a hello world message' do
|
12
12
|
get '/hello'
|
13
|
-
expect(
|
13
|
+
expect(response_body).to eq({ message: 'Hello Wonderful World, from <%= app_name.classify %>!' }.to_json)
|
14
14
|
end
|
15
15
|
end
|
16
16
|
|
@@ -4,20 +4,33 @@ require 'webmock/rspec'
|
|
4
4
|
require 'rack/test'
|
5
5
|
require 'simplecov'
|
6
6
|
require 'factory_girl'
|
7
|
+
require 'napa/rspec_extensions/response_helpers'
|
7
8
|
|
8
|
-
FactoryGirl.definition_file_paths = %w
|
9
|
+
FactoryGirl.definition_file_paths = %w(./spec/factories)
|
9
10
|
FactoryGirl.find_definitions
|
10
|
-
SimpleCov.start
|
11
|
+
SimpleCov.start do
|
12
|
+
add_filter "/spec\/.*/"
|
13
|
+
add_filter "/vendor\/.*/"
|
14
|
+
end
|
11
15
|
|
12
16
|
require './app'
|
13
17
|
require 'database_cleaner'
|
14
18
|
|
19
|
+
SimpleCov.start do
|
20
|
+
add_filter "/spec\/.*/"
|
21
|
+
add_filter "/vendor\/.*/"
|
22
|
+
end
|
23
|
+
|
24
|
+
# fail once the test coverage gets below an accepted amount
|
25
|
+
# SimpleCov.minimum_coverage 90
|
26
|
+
|
15
27
|
# Requires supporting ruby files with custom matchers and macros, etc,
|
16
28
|
# in spec/support/ and its subdirectories.
|
17
29
|
Dir['./spec/support/**/*.rb'].each { |f| require f }
|
18
30
|
|
19
31
|
RSpec.configure do |config|
|
20
32
|
config.include FactoryGirl::Syntax::Methods
|
33
|
+
config.include Napa::RspecExtensions::ResponseHelpers
|
21
34
|
|
22
35
|
config.before(:suite) do
|
23
36
|
DatabaseCleaner.strategy = :transaction
|
data/lib/napa/grape_extenders.rb
CHANGED
@@ -6,10 +6,14 @@ module Napa
|
|
6
6
|
# if AR is being used, rescue from common AR errors
|
7
7
|
if defined?(::ActiveRecord)
|
8
8
|
modified_class.rescue_from ::ActiveRecord::RecordNotFound do |e|
|
9
|
-
|
9
|
+
err = Napa::JsonError.new(:record_not_found, 'record not found')
|
10
|
+
Napa::Logger.logger.debug Napa::Logger.response(404, {}, err)
|
11
|
+
rack_response(err.to_json, 404)
|
10
12
|
end
|
11
13
|
modified_class.rescue_from ::ActiveRecord::RecordInvalid do |e|
|
12
|
-
|
14
|
+
err = Napa::JsonError.new(:unprocessable_entity, e.message)
|
15
|
+
Napa::Logger.logger.debug Napa::Logger.response(422, {}, err)
|
16
|
+
rack_response(err.to_json, 422)
|
13
17
|
end
|
14
18
|
end
|
15
19
|
end
|
@@ -7,7 +7,7 @@ if defined?(Grape)
|
|
7
7
|
result = message.is_a?(Napa::JsonError) ? message : Napa::JsonError.new(:api_error, message)
|
8
8
|
|
9
9
|
if (options[:rescue_options] || {})[:backtrace] && backtrace && !backtrace.empty?
|
10
|
-
result = result.merge(backtrace: backtrace)
|
10
|
+
result = result.to_h.merge(backtrace: backtrace)
|
11
11
|
end
|
12
12
|
MultiJson.dump(result)
|
13
13
|
end
|
@@ -14,7 +14,14 @@ module Napa
|
|
14
14
|
Napa::JsonError.new(code, message)
|
15
15
|
end
|
16
16
|
|
17
|
+
def permitted_params(options = {})
|
18
|
+
options = { include_missing: false }.merge(options)
|
19
|
+
declared(params, options)
|
20
|
+
end
|
21
|
+
|
17
22
|
# extend all endpoints to include this
|
18
|
-
Grape::Endpoint.send :include, self
|
23
|
+
Grape::Endpoint.send :include, self if defined?(Grape)
|
24
|
+
# rails 4 controller concern
|
25
|
+
extend ActiveSupport::Concern if defined?(Rails)
|
19
26
|
end
|
20
27
|
end
|
data/lib/napa/logger/logger.rb
CHANGED
@@ -0,0 +1,37 @@
|
|
1
|
+
# override what is in logging gem to ALWAYS use a structured object, rather than a string
|
2
|
+
# original version of this: https://github.com/TwP/logging/blob/master/lib/logging/layouts/parseable.rb
|
3
|
+
module Logging
|
4
|
+
module Layouts
|
5
|
+
class Parseable < ::Logging::Layout
|
6
|
+
|
7
|
+
# Public: Take a given object and convert it into a format suitable for
|
8
|
+
# inclusion as a log message. The conversion allows the object to be more
|
9
|
+
# easily expressed in YAML or JSON form.
|
10
|
+
#
|
11
|
+
# If the object is an Exception, then this method will return a Hash
|
12
|
+
# containing the exception class name, message, and backtrace (if any).
|
13
|
+
#
|
14
|
+
# If the object is a string, wrap it in a hash.
|
15
|
+
#
|
16
|
+
# obj - The Object to format
|
17
|
+
#
|
18
|
+
# Returns the formatted Object.
|
19
|
+
#
|
20
|
+
def format_obj(obj)
|
21
|
+
case obj
|
22
|
+
when Exception
|
23
|
+
h = { class: obj.class.name,
|
24
|
+
message: obj.message }
|
25
|
+
h[:backtrace] = obj.backtrace if @backtrace && !obj.backtrace.nil?
|
26
|
+
h
|
27
|
+
when Time
|
28
|
+
iso8601_format(obj)
|
29
|
+
when String
|
30
|
+
{ text: obj }
|
31
|
+
else
|
32
|
+
obj
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end # Parseable
|
36
|
+
end # Layouts
|
37
|
+
end
|
@@ -6,7 +6,7 @@ module Napa
|
|
6
6
|
end
|
7
7
|
|
8
8
|
def call(env)
|
9
|
-
if ["/health", "/health.json"].include? env['
|
9
|
+
if ["/health", "/health.json"].include? env['PATH_INFO']
|
10
10
|
[200, { 'Content-type' => 'application/json' }, [Napa::Identity.health.to_json]]
|
11
11
|
else
|
12
12
|
@app.call(env)
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module Napa
|
2
|
+
class Middleware
|
3
|
+
class DatabaseStats
|
4
|
+
def initialize(app)
|
5
|
+
@app = app
|
6
|
+
end
|
7
|
+
|
8
|
+
def call(env)
|
9
|
+
require 'napa/active_record_extensions/notifications_subscriber'
|
10
|
+
status, headers, body = @app.call(env)
|
11
|
+
[status, headers, body]
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -34,13 +34,14 @@ module Napa
|
|
34
34
|
end
|
35
35
|
|
36
36
|
request_data = {
|
37
|
-
method:
|
38
|
-
path:
|
39
|
-
query:
|
37
|
+
method: request.request_method,
|
38
|
+
path: request.path_info,
|
39
|
+
query: request.query_string,
|
40
40
|
host: Napa::Identity.hostname,
|
41
41
|
pid: Napa::Identity.pid,
|
42
42
|
revision: Napa::Identity.revision,
|
43
|
-
params: params
|
43
|
+
params: params,
|
44
|
+
remote_ip: request.ip
|
44
45
|
}
|
45
46
|
request_data[:user_id] = current_user.try(:id) if defined?(current_user)
|
46
47
|
{ request: request_data }
|
@@ -54,13 +55,7 @@ module Napa
|
|
54
55
|
response_body = body.inspect
|
55
56
|
end
|
56
57
|
|
57
|
-
|
58
|
-
{
|
59
|
-
status: status,
|
60
|
-
headers: headers,
|
61
|
-
response: response_body
|
62
|
-
}
|
63
|
-
}
|
58
|
+
Napa::Logger.response(status, headers, response_body)
|
64
59
|
end
|
65
60
|
end
|
66
61
|
end
|
@@ -6,7 +6,7 @@ module Napa
|
|
6
6
|
end
|
7
7
|
|
8
8
|
def normalize_path(path)
|
9
|
-
case
|
9
|
+
case
|
10
10
|
when path == '/'
|
11
11
|
'root'
|
12
12
|
else
|
@@ -26,14 +26,16 @@ module Napa
|
|
26
26
|
|
27
27
|
# Calculate total response time
|
28
28
|
response_time = (stop - start) * 1000
|
29
|
-
|
29
|
+
|
30
30
|
request = Rack::Request.new(env)
|
31
31
|
path = normalize_path(request.path_info)
|
32
|
-
Thread.current[:stats_context] = "#{Napa::Identity.name}.http.#{request.request_method.downcase}.#{path}".gsub('/', '.')
|
33
32
|
|
34
33
|
# Emit stats to StatsD
|
35
|
-
Napa::Stats.emitter.increment(
|
36
|
-
Napa::Stats.emitter.timing(
|
34
|
+
Napa::Stats.emitter.increment('request_count')
|
35
|
+
Napa::Stats.emitter.timing('response_time', response_time)
|
36
|
+
Napa::Stats.emitter.increment("path.#{Napa::Stats.path_to_key(request.request_method, path)}.request_count")
|
37
|
+
Napa::Stats.emitter.timing("path.#{Napa::Stats.path_to_key(request.request_method, path)}.response_time", response_time)
|
38
|
+
|
37
39
|
# Return the results
|
38
40
|
[status, headers, body]
|
39
41
|
end
|
File without changes
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# include this in your representer, and you will always return all defined keys (even if their value is nil)
|
2
|
+
module Napa
|
3
|
+
module Representable
|
4
|
+
module IncludeNil
|
5
|
+
def self.included base
|
6
|
+
base.extend ClassMethods
|
7
|
+
end
|
8
|
+
|
9
|
+
module ClassMethods
|
10
|
+
def property(name, options={}, &block)
|
11
|
+
super(name, options.merge(render_nil: true), &block)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -5,8 +5,8 @@ require 'roar/representer/feature/coercion'
|
|
5
5
|
module Napa
|
6
6
|
class Representer < Roar::Decorator
|
7
7
|
include Roar::Representer::JSON
|
8
|
-
include Representable::Coercion
|
8
|
+
include ::Representable::Coercion
|
9
9
|
|
10
10
|
property :object_type, getter: lambda { |*| self.class.name.underscore }
|
11
11
|
end
|
12
|
-
end
|
12
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module Napa
|
2
|
+
module RspecExtensions
|
3
|
+
module ResponseHelpers
|
4
|
+
def parsed_response
|
5
|
+
Hashie::Mash.new(JSON.parse(last_response.body))
|
6
|
+
end
|
7
|
+
|
8
|
+
def response_code
|
9
|
+
last_response.status
|
10
|
+
end
|
11
|
+
|
12
|
+
def response_body
|
13
|
+
last_response.body
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
data/lib/napa/stats.rb
CHANGED
@@ -14,10 +14,30 @@ module Napa
|
|
14
14
|
|
15
15
|
# Create a new StatsD emitter with the service name as the namespace
|
16
16
|
# Defaults to localhost port 8125 if env vars are nil
|
17
|
-
@emitter = Statsd.new(ENV['STATSD_HOST'], ENV['STATSD_PORT']).tap { |sd| sd.namespace =
|
17
|
+
@emitter = Statsd.new(ENV['STATSD_HOST'], ENV['STATSD_PORT']).tap { |sd| sd.namespace = namespace }
|
18
18
|
end
|
19
19
|
@emitter
|
20
20
|
end
|
21
|
+
|
22
|
+
def namespace
|
23
|
+
environment = ENV['RACK_ENV'] || 'development'
|
24
|
+
|
25
|
+
if ENV['STATSD_API_KEY'].present?
|
26
|
+
"#{ENV['STATSD_API_KEY']}.#{Napa::Identity.name}.#{environment}"
|
27
|
+
else
|
28
|
+
"#{Napa::Identity.name}.#{environment}"
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def path_to_key(method, path)
|
33
|
+
# split the path on forward slash
|
34
|
+
# remove any elements that are empty
|
35
|
+
# replace any number strings with _
|
36
|
+
# join all parts with a .
|
37
|
+
# prepend with the method
|
38
|
+
# downcase the whole thing
|
39
|
+
"#{method}.#{path.split(/\//).reject{|p| p.empty?}.collect{|p| p.gsub(/\d+/,'_')}.join('.')}".downcase
|
40
|
+
end
|
21
41
|
end
|
22
42
|
end
|
23
43
|
end
|
data/lib/napa/version.rb
CHANGED
data/lib/tasks/db.rake
CHANGED
@@ -13,6 +13,13 @@ unless defined?(Rails)
|
|
13
13
|
Rake::Task["db:schema:dump"].invoke unless Napa.env.production?
|
14
14
|
end
|
15
15
|
|
16
|
+
desc 'Rollback to a previous migration. Go back multiple steps with STEP=x'
|
17
|
+
task :rollback => :environment do
|
18
|
+
ActiveRecord::Migration.verbose = true
|
19
|
+
step = ENV['STEP'] ? ENV['STEP'].to_i : 1
|
20
|
+
ActiveRecord::Migrator.rollback('db/migrate', step)
|
21
|
+
end
|
22
|
+
|
16
23
|
desc "Create the database"
|
17
24
|
task :create => :environment do
|
18
25
|
db = YAML.load(ERB.new(File.read('./config/database.yml')).result)[Napa.env]
|
data/lib/tasks/git.rake
CHANGED
@@ -7,6 +7,9 @@ namespace :git do
|
|
7
7
|
|
8
8
|
desc "Verify git repository is in a good state for deployment"
|
9
9
|
task :verify do
|
10
|
+
raise RuntimeError, "ENV['GITHUB_REPO'] is not defined" if github_repo.nil?
|
11
|
+
raise RuntimeError, "ENV['GITHUB_OAUTH_TOKEN'] is not defined" if ENV['GITHUB_OAUTH_TOKEN'].nil?
|
12
|
+
|
10
13
|
logger.info "Verifying git repository is in a good state"
|
11
14
|
|
12
15
|
# Be sure local HEAD exists on remote
|
data/napa.gemspec
CHANGED
@@ -0,0 +1,63 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'napa/generators/api_generator'
|
3
|
+
require 'napa/cli'
|
4
|
+
|
5
|
+
describe Napa::Generators::ApiGenerator do
|
6
|
+
let(:api_name) { 'foo' }
|
7
|
+
let(:test_api_directory) { 'spec/tmp' }
|
8
|
+
|
9
|
+
before do
|
10
|
+
described_class.any_instance.stub(:output_directory) { test_api_directory }
|
11
|
+
Napa::CLI::Base.new.generate("api", api_name)
|
12
|
+
end
|
13
|
+
|
14
|
+
after do
|
15
|
+
FileUtils.rm_rf(test_api_directory)
|
16
|
+
end
|
17
|
+
|
18
|
+
describe 'app' do
|
19
|
+
it 'creates an api class' do
|
20
|
+
expected_api_file = File.join(test_api_directory, 'app/apis/foos_api.rb')
|
21
|
+
api_code = File.read(expected_api_file)
|
22
|
+
|
23
|
+
expect(api_code).to match(/class FoosApi/)
|
24
|
+
end
|
25
|
+
|
26
|
+
it 'creates a model class' do
|
27
|
+
expected_model_file = File.join(test_api_directory, 'app/models/foo.rb')
|
28
|
+
model_code = File.read(expected_model_file)
|
29
|
+
|
30
|
+
expect(model_code).to match(/class Foo/)
|
31
|
+
end
|
32
|
+
|
33
|
+
it 'creates a representer class' do
|
34
|
+
expected_representer_file = File.join(test_api_directory, 'app/representers/foo_representer.rb')
|
35
|
+
representer_code = File.read(expected_representer_file)
|
36
|
+
|
37
|
+
expect(representer_code).to match(/class FooRepresenter/)
|
38
|
+
end
|
39
|
+
|
40
|
+
it 'representers should inherit from Napa::Representer' do
|
41
|
+
representer_file = File.join(test_api_directory, 'app/representers/foo_representer.rb')
|
42
|
+
require "./#{representer_file}"
|
43
|
+
expect(FooRepresenter.superclass).to be(Napa::Representer)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
describe 'spec' do
|
48
|
+
it 'creates an api spec' do
|
49
|
+
expected_api_file = File.join(test_api_directory, 'spec/apis/foos_api_spec.rb')
|
50
|
+
api_code = File.read(expected_api_file)
|
51
|
+
|
52
|
+
expect(api_code).to match(/describe FoosApi/)
|
53
|
+
end
|
54
|
+
|
55
|
+
it 'creates a model spec' do
|
56
|
+
expected_model_file = File.join(test_api_directory, 'spec/models/foo_spec.rb')
|
57
|
+
model_code = File.read(expected_model_file)
|
58
|
+
|
59
|
+
expect(model_code).to match(/describe Foo/)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'napa/generators/migration_generator'
|
3
|
+
require 'napa/cli'
|
4
|
+
|
5
|
+
describe Napa::Generators::MigrationGenerator do
|
6
|
+
|
7
|
+
let(:migration_name) { 'foo_bars' }
|
8
|
+
let(:test_migrations_directory) { 'spec/tmp' }
|
9
|
+
|
10
|
+
before do
|
11
|
+
described_class.any_instance.stub(:output_directory) { test_migrations_directory }
|
12
|
+
end
|
13
|
+
|
14
|
+
after do
|
15
|
+
FileUtils.rm_rf(test_migrations_directory)
|
16
|
+
end
|
17
|
+
|
18
|
+
it 'creates a camelized migration class' do
|
19
|
+
described_class.any_instance.stub(:migration_filename) { 'foo' }
|
20
|
+
Napa::CLI::Base.new.generate("migration", migration_name)
|
21
|
+
expected_migration_file = File.join(test_migrations_directory, 'foo.rb')
|
22
|
+
migration_code = File.read(expected_migration_file)
|
23
|
+
expect(migration_code).to match(/class FooBars/)
|
24
|
+
end
|
25
|
+
|
26
|
+
|
27
|
+
end
|