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