napa 0.2.1 → 0.3.0
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 +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
|