napa 0.2.1 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (55) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +20 -0
  3. data/LICENSE +4 -2
  4. data/README.md +25 -2
  5. data/docs/quickstart.md +27 -26
  6. data/lib/napa.rb +6 -2
  7. data/lib/napa/active_record_extensions/notifications_subscriber.rb +17 -0
  8. data/lib/napa/active_record_extensions/stats.rb +1 -14
  9. data/lib/napa/cli.rb +5 -2
  10. data/lib/napa/generators/api_generator.rb +5 -1
  11. data/lib/napa/generators/migration_generator.rb +6 -2
  12. data/lib/napa/generators/scaffold_generator.rb +4 -3
  13. data/lib/napa/generators/templates/api/app/apis/%name_tableize%_api.rb.tt +4 -5
  14. data/lib/napa/generators/templates/api/app/models/%name_underscore%.rb.tt +0 -1
  15. data/lib/napa/generators/templates/api/spec/apis/%name_tableize%_api_spec.rb.tt +16 -0
  16. data/lib/napa/generators/templates/api/spec/models/%name_underscore%_spec.rb.tt +9 -0
  17. data/lib/napa/generators/templates/migration/%migration_filename%.rb.tt +1 -1
  18. data/lib/napa/generators/templates/scaffold/.gitignore.tt +2 -0
  19. data/lib/napa/generators/templates/scaffold/Gemfile.tt +2 -2
  20. data/lib/napa/generators/templates/scaffold/app.rb +1 -1
  21. data/lib/napa/generators/templates/scaffold/config.ru.tt +1 -1
  22. data/lib/napa/generators/templates/scaffold/config/database.yml.tt +1 -0
  23. data/lib/napa/generators/templates/scaffold/spec/apis/hello_api_spec.rb.tt +1 -1
  24. data/lib/napa/generators/templates/scaffold/spec/spec_helper.rb +15 -2
  25. data/lib/napa/grape_extenders.rb +6 -2
  26. data/lib/napa/grape_extensions/error_formatter.rb +1 -1
  27. data/lib/napa/grape_extensions/grape_helpers.rb +8 -1
  28. data/lib/napa/logger/logger.rb +10 -0
  29. data/lib/napa/logger/parseable.rb +37 -0
  30. data/lib/napa/middleware/app_monitor.rb +1 -1
  31. data/lib/napa/middleware/database_stats.rb +15 -0
  32. data/lib/napa/middleware/logger.rb +6 -11
  33. data/lib/napa/middleware/request_stats.rb +7 -5
  34. data/lib/napa/{grape_extensions → output_formatters}/entity.rb +0 -0
  35. data/lib/napa/output_formatters/include_nil.rb +16 -0
  36. data/lib/napa/{grape_extensions → output_formatters}/representer.rb +2 -2
  37. data/lib/napa/rspec_extensions/response_helpers.rb +17 -0
  38. data/lib/napa/stats.rb +21 -1
  39. data/lib/napa/version.rb +1 -1
  40. data/lib/tasks/db.rake +7 -0
  41. data/lib/tasks/git.rake +3 -0
  42. data/napa.gemspec +0 -1
  43. data/spec/generators/api_generator_spec.rb +63 -0
  44. data/spec/generators/migration_generator_spec.rb +27 -0
  45. data/spec/generators/scaffold_generator_spec.rb +90 -0
  46. data/spec/grape_extensions/error_formatter_spec.rb +8 -0
  47. data/spec/grape_extensions/include_nil_spec.rb +23 -0
  48. data/spec/logger/logger_spec.rb +14 -0
  49. data/spec/logger/parseable_spec.rb +16 -0
  50. data/spec/middleware/database_stats_spec.rb +64 -0
  51. data/spec/middleware/request_stats_spec.rb +4 -5
  52. data/spec/spec_helper.rb +26 -0
  53. data/spec/stats_spec.rb +25 -1
  54. metadata +25 -20
  55. 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(last_response.body).to eq({ message: 'Hello Wonderful World, from <%= app_name.classify %>!' }.to_json)
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{./spec/factories}
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
@@ -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
- rack_response(Napa::JsonError.new(:record_not_found, 'record not found').to_json, 404)
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
- rack_response(Napa::JsonError.new(:unprocessable_entity, e.message).to_json, 422)
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
@@ -27,6 +27,16 @@ module Napa
27
27
 
28
28
  @logger
29
29
  end
30
+
31
+ def response(status, headers, body)
32
+ { response:
33
+ {
34
+ status: status,
35
+ headers: headers,
36
+ response: body
37
+ }
38
+ }
39
+ end
30
40
  end
31
41
  end
32
42
  end
@@ -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['REQUEST_PATH']
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: env['REQUEST_METHOD'],
38
- path: env['PATH_INFO'],
39
- query: env['QUERY_STRING'],
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
- { response:
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(Thread.current[:stats_context] + '.requests')
36
- Napa::Stats.emitter.timing(Thread.current[:stats_context] + '.response_time', response_time)
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
@@ -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
@@ -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 = Napa::Identity.name }
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
@@ -1,5 +1,5 @@
1
1
  module Napa
2
- VERSION = '0.2.1'
2
+ VERSION = '0.3.0'
3
3
 
4
4
  class Version
5
5
  class << self
@@ -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]
@@ -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
@@ -25,7 +25,6 @@ Gem::Specification.new do |gem|
25
25
  gem.add_dependency 'grape'
26
26
  gem.add_dependency 'grape-swagger'
27
27
  gem.add_dependency 'roar'
28
- gem.add_dependency 'unicorn'
29
28
  gem.add_dependency 'statsd-ruby'
30
29
  gem.add_dependency 'racksh'
31
30
 
@@ -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