napa 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (44) hide show
  1. data/.gitignore +21 -0
  2. data/Gemfile +4 -0
  3. data/LICENSE +22 -0
  4. data/README.md +172 -0
  5. data/Rakefile +3 -0
  6. data/bin/napa +17 -0
  7. data/lib/generators/scaffold.rb +67 -0
  8. data/lib/generators/templates/scaffold/.env +6 -0
  9. data/lib/generators/templates/scaffold/.env.test +7 -0
  10. data/lib/generators/templates/scaffold/.gitignore.tpl +10 -0
  11. data/lib/generators/templates/scaffold/Gemfile +28 -0
  12. data/lib/generators/templates/scaffold/README.md +3 -0
  13. data/lib/generators/templates/scaffold/Rakefile +8 -0
  14. data/lib/generators/templates/scaffold/app/apis/hello_api.rb +13 -0
  15. data/lib/generators/templates/scaffold/app.rb +21 -0
  16. data/lib/generators/templates/scaffold/config/database.yml +18 -0
  17. data/lib/generators/templates/scaffold/config/initializers/active_record.rb +5 -0
  18. data/lib/generators/templates/scaffold/config/middleware/honeybadger.rb +7 -0
  19. data/lib/generators/templates/scaffold/config.ru +17 -0
  20. data/lib/generators/templates/scaffold/console +5 -0
  21. data/lib/generators/templates/scaffold/lib/password_protected_helpers.rb +17 -0
  22. data/lib/generators/templates/scaffold/spec/apis/hello_api_spec.rb +17 -0
  23. data/lib/generators/templates/scaffold/spec/factories/.gitkeep +0 -0
  24. data/lib/generators/templates/scaffold/spec/spec_helper.rb +35 -0
  25. data/lib/napa/activerecord.rb +21 -0
  26. data/lib/napa/grape_api.rb +15 -0
  27. data/lib/napa/identity.rb +38 -0
  28. data/lib/napa/logger/log_transaction.rb +17 -0
  29. data/lib/napa/logger/logger.rb +34 -0
  30. data/lib/napa/middleware/app_monitor.rb +17 -0
  31. data/lib/napa/middleware/logger.rb +69 -0
  32. data/lib/napa/version.rb +45 -0
  33. data/lib/napa.rb +37 -0
  34. data/lib/tasks/db.rake +87 -0
  35. data/lib/tasks/deploy.rake +13 -0
  36. data/lib/tasks/git.rake +50 -0
  37. data/lib/tasks/routes.rake +9 -0
  38. data/napa.gemspec +31 -0
  39. data/spec/identity_spec.rb +50 -0
  40. data/spec/logger/log_transaction_spec.rb +34 -0
  41. data/spec/spec_helper.rb +1 -0
  42. data/spec/version_spec.rb +40 -0
  43. data/tasks/version.rake +51 -0
  44. metadata +275 -0
@@ -0,0 +1,34 @@
1
+ module Napa
2
+ class Logger
3
+ class << self
4
+ def name
5
+ [Napa::Identity.name, Napa::LogTransaction.id].join('-')
6
+ end
7
+
8
+ def logger=(logger)
9
+ @logger = logger
10
+ end
11
+
12
+ def logger
13
+ unless @logger
14
+ Logging.appenders.stdout(
15
+ 'stdout',
16
+ :layout => Logging.layouts.json
17
+ )
18
+ Logging.appenders.file(
19
+ "log/#{Napa.env}.log",
20
+ :layout => Logging.layouts.json
21
+ )
22
+
23
+ @logger = Logging.logger["[#{name}]"]
24
+ unless Napa.env.test?
25
+ @logger.add_appenders 'stdout'
26
+ end
27
+ @logger.add_appenders "log/#{Napa.env}.log"
28
+ end
29
+
30
+ @logger
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,17 @@
1
+ module Napa
2
+ class Middleware
3
+ class AppMonitor
4
+ def initialize(app)
5
+ @app = app
6
+ end
7
+
8
+ def call(env)
9
+ if env['REQUEST_PATH'] == '/health'
10
+ [200, {'Content-type' => 'application/json'}, [Napa::Identity.health.to_json]]
11
+ else
12
+ @app.call(env)
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,69 @@
1
+ module Napa
2
+ class Middleware
3
+ class Logger
4
+ def initialize(app)
5
+ @app = app
6
+ end
7
+
8
+ def call(env)
9
+ # set transaction_id
10
+ transaction_id=SecureRandom.uuid
11
+
12
+ # log the request
13
+ Napa::Logger.logger.debug format_request(env)
14
+
15
+ # process the request
16
+ status, headers, body = @app.call(env)
17
+
18
+ # log the response
19
+ Napa::Logger.logger.debug format_response(status, headers, body)
20
+
21
+ # return the results
22
+ [status, headers, body]
23
+ ensure
24
+ # Clear the transaction id after each request
25
+ Napa::LogTransaction.clear
26
+ end
27
+
28
+ private
29
+ def format_request(env)
30
+ request = Rack::Request.new(env)
31
+ params = request.params
32
+ begin
33
+ params = JSON.parse(request.body.read) if env['CONTENT_TYPE'] == 'application/json'
34
+ rescue
35
+ # do nothing, params is already set
36
+ end
37
+
38
+ request_data = {
39
+ method: env['REQUEST_METHOD'],
40
+ path: env['PATH_INFO'],
41
+ query: env['QUERY_STRING'],
42
+ host: Napa::Identity.hostname,
43
+ pid: Napa::Identity.pid,
44
+ revision: Napa::Identity.revision,
45
+ params: params
46
+ }
47
+ request_data[:user_id] = current_user.try(:id) if defined?(current_user)
48
+ {request: request_data}
49
+ end
50
+
51
+ def format_response(status, headers, body)
52
+ response_body = nil
53
+ begin
54
+ response_body = body.respond_to?(:body) ? body.body.map{|r| r} : nil
55
+ rescue
56
+ response_body = body.inspect
57
+ end
58
+
59
+ {response:
60
+ {
61
+ status: status,
62
+ headers: headers,
63
+ response: response_body
64
+ }
65
+ }
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,45 @@
1
+ module Napa
2
+ VERSION = "0.1.0"
3
+
4
+ class Version
5
+ class << self
6
+ def next_major
7
+ self.next_level(:major)
8
+ end
9
+
10
+ def next_minor
11
+ self.next_level(:minor)
12
+ end
13
+
14
+ def next_patch
15
+ self.next_level(:patch)
16
+ end
17
+
18
+ def next_level(level)
19
+ raise "Unidentified Level" unless [:major,:minor,:patch].include?(level)
20
+
21
+ parts = Napa::VERSION.split('.').map{|p| p.to_i}
22
+
23
+ if level == :major
24
+ parts[0] += 1
25
+ significant_index = 1
26
+ end
27
+
28
+ if level == :minor
29
+ parts[1] += 1
30
+ significant_index = 2
31
+ end
32
+
33
+ if level == :patch
34
+ parts[2] += 1
35
+ significant_index = 3
36
+ end
37
+
38
+ parts.map.with_index{|p,i| parts[i] = 0 if i >= significant_index}
39
+
40
+ parts.join('.')
41
+ end
42
+
43
+ end
44
+ end
45
+ end
data/lib/napa.rb ADDED
@@ -0,0 +1,37 @@
1
+ # require external libraries
2
+ require 'rake'
3
+ require 'dotenv'
4
+ require 'logging'
5
+ require 'octokit'
6
+ require 'grape'
7
+
8
+ # require internal files
9
+ require "napa/version"
10
+ require "napa/logger/logger"
11
+ require "napa/logger/log_transaction"
12
+ require "napa/identity"
13
+ require "napa/middleware/logger"
14
+ require "napa/middleware/app_monitor"
15
+ require "napa/activerecord"
16
+ require "napa/grape_api"
17
+ require "generators/scaffold"
18
+
19
+ # load rake tasks if Rake installed
20
+ if defined?(Rake)
21
+ load 'tasks/git.rake'
22
+ load 'tasks/deploy.rake'
23
+ load 'tasks/routes.rake'
24
+ load 'tasks/db.rake'
25
+ end
26
+
27
+ module Napa
28
+ class << self
29
+ def env
30
+ @_env ||= ActiveSupport::StringInquirer.new(ENV["RACK_ENV"] || "development")
31
+ end
32
+
33
+ def env=(environment)
34
+ @_env = ActiveSupport::StringInquirer.new(environment)
35
+ end
36
+ end
37
+ end
data/lib/tasks/db.rake ADDED
@@ -0,0 +1,87 @@
1
+ task :environment do
2
+ require 'erb'
3
+ require './app'
4
+
5
+ raise "ActiveRecord Not Found" unless Module.const_defined?(:ActiveRecord)
6
+ end
7
+
8
+ namespace :db do
9
+ desc "Migrate the database through scripts in db/migrate. Target specific version with VERSION=x"
10
+ task :migrate => :environment do
11
+ ActiveRecord::Migrator.migrate('db/migrate', ENV["VERSION"] ? ENV["VERSION"].to_i : nil )
12
+ Rake::Task["db:schema:dump"].invoke
13
+ end
14
+
15
+ desc "Create the database"
16
+ task :create => :environment do
17
+ db = YAML.load(ERB.new(File.read('./config/database.yml')).result)[Napa.env]
18
+ adapter = db.merge({'database'=> 'mysql'})
19
+ ActiveRecord::Base.establish_connection(adapter)
20
+ ActiveRecord::Base.connection.create_database(db.fetch('database'))
21
+ end
22
+
23
+ desc "Delete the database"
24
+ task :drop => :environment do
25
+ db = YAML.load(ERB.new(File.read('./config/database.yml')).result)[Napa.env]
26
+ adapter = db.merge({'database'=> 'mysql'})
27
+ ActiveRecord::Base.establish_connection(adapter)
28
+ ActiveRecord::Base.connection.drop_database(db.fetch('database'))
29
+ end
30
+
31
+ desc "Create the test database"
32
+ task :reset => :environment do
33
+ Rake::Task["db:drop"].invoke
34
+ Rake::Task["db:create"].invoke
35
+ Rake::Task["db:schema:load"].invoke
36
+ end
37
+
38
+ namespace :generate do
39
+ desc "Generate a migration with given name. Specify migration name with NAME=my_migration_name"
40
+ task :migration => :environment do
41
+ raise "Please specify desired migration name with NAME=my_migration_name" unless ENV['NAME']
42
+
43
+ # Find migration name from env
44
+ migration_name = ENV['NAME'].strip.chomp
45
+
46
+ # Define migrations path (needed later)
47
+ migrations_path = './db/migrate'
48
+
49
+ # Find the highest existing migration version or set to 1
50
+ if (existing_migrations = Dir[File.join(migrations_path, '*.rb')]).length > 0
51
+ version = File.basename(existing_migrations.sort.reverse.first)[/^(\d+)_/,1].to_i + 1
52
+ else
53
+ version = 1
54
+ end
55
+
56
+ # Use the migration template to fill the body of the migration
57
+ migration_content = Napa::ActiveRecord.migration_template(migration_name.camelize)
58
+
59
+ # Generate migration filename
60
+ migration_filename = "#{"%03d" % version}_#{migration_name}.rb"
61
+
62
+ # Write the migration
63
+ File.open(File.join(migrations_path, migration_filename), "w+") do |migration|
64
+ migration.puts migration_content
65
+ end
66
+
67
+ # Done!
68
+ puts "Successfully created migration #{migration_filename}"
69
+ end
70
+ end
71
+
72
+ namespace :schema do
73
+ desc "Create a db/schema.rb file that can be portably used against any DB supported by AR"
74
+ task :dump => :environment do
75
+ require 'active_record/schema_dumper'
76
+ File.open(ENV['SCHEMA'] || "db/schema.rb", "w") do |file|
77
+ ActiveRecord::SchemaDumper.dump(ActiveRecord::Base.connection, file)
78
+ end
79
+ end
80
+
81
+ desc "Load a schema.rb file into the database"
82
+ task :load => :environment do
83
+ file = ENV['SCHEMA'] || "db/schema.rb"
84
+ load(file)
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,13 @@
1
+ namespace :deploy do
2
+ desc "Deploy to production"
3
+ task :production do
4
+ Rake::Task["git:verify"].invoke
5
+ Rake::Task["git:set_tag"].invoke
6
+ end
7
+
8
+ desc "Deploy to staging"
9
+ task :staging do
10
+ Rake::Task["git:verify"].invoke
11
+ Rake::Task["git:set_tag"].invoke("staging")
12
+ end
13
+ end
@@ -0,0 +1,50 @@
1
+ namespace :git do
2
+ client = Octokit::Client.new(:oauth_token => ENV['GITHUB_OAUTH_TOKEN'])
3
+ logger = Logger.new(STDOUT)
4
+
5
+ github_repo = ENV['GITHUB_REPO']
6
+ local_sha = `git rev-parse HEAD`.strip
7
+
8
+ desc "Verify git repository is in a good state for deployment"
9
+ task :verify do
10
+ logger.info "Verifying git repository is in a good state"
11
+
12
+ # Be sure local HEAD exists on remote
13
+ begin
14
+ remote_commit = client.commit(github_repo, local_sha)
15
+ rescue Octokit::NotFound
16
+ raise RuntimeError, "Local commit #{local_sha} does not exist on remote. Be sure to push your changes."
17
+ end
18
+
19
+ # Check for uncommited changes
20
+ unless(system("git diff --quiet HEAD"))
21
+ raise RuntimeError, "You have uncommited changes. Either commit or stash them before continuing."
22
+ end
23
+
24
+ # Check for untracked files
25
+ unless(`git status --porcelain | grep '^??' | wc -l`.strip == "0")
26
+ raise RuntimeError, "You have untracked files. Either commit or remove them before continuing."
27
+ end
28
+ end
29
+
30
+ desc "Set tag, which triggers deploy"
31
+ task :set_tag, :tag do |t, args|
32
+ tag = args[:tag] || "production"
33
+ logger.info "Setting #{tag} tag on github"
34
+
35
+ # Update ref, create ref if it doesn't exist
36
+ begin
37
+ client.update_ref(
38
+ github_repo,
39
+ "tags/#{tag}",
40
+ local_sha
41
+ )
42
+ rescue Octokit::UnprocessableEntity
43
+ client.create_ref(
44
+ github_repo,
45
+ "tags/#{tag}",
46
+ local_sha
47
+ )
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,9 @@
1
+ desc "display all routes"
2
+ task :routes do
3
+ grape_apis = ObjectSpace.each_object(Class).select { |klass| klass < Grape::API }
4
+ grape_apis.each do |api|
5
+ api.routes.each do |r|
6
+ puts "#{r}"
7
+ end
8
+ end
9
+ end
data/napa.gemspec ADDED
@@ -0,0 +1,31 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require File.expand_path('../lib/napa/version', __FILE__)
3
+
4
+ Gem::Specification.new do |gem|
5
+ gem.authors = ["Darby Frey"]
6
+ gem.email = ["darby@bellycard.com"]
7
+ gem.description = %q{A simple framework for building APIs with Grape}
8
+ gem.summary = %q{A simple framework for building APIs with Grape}
9
+ gem.homepage = ""
10
+
11
+ gem.files = `git ls-files`.split($\)
12
+ gem.executables << 'napa'
13
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
14
+ gem.name = "napa"
15
+ gem.require_paths = ["lib"]
16
+ gem.version = Napa::VERSION
17
+
18
+
19
+ gem.add_dependency 'rake'
20
+ gem.add_dependency 'logging'
21
+ gem.add_dependency 'dotenv'
22
+ gem.add_dependency 'octokit', '~> 1.25'
23
+ gem.add_dependency 'virtus', '0.5.5'
24
+ gem.add_dependency 'grape'
25
+ gem.add_dependency 'grape-swagger'
26
+ gem.add_dependency 'unicorn'
27
+
28
+ gem.add_development_dependency "rspec"
29
+ gem.add_development_dependency "pry"
30
+ gem.add_development_dependency "git"
31
+ end
@@ -0,0 +1,50 @@
1
+ require 'spec_helper'
2
+ require 'napa/identity'
3
+
4
+ describe Napa::Identity do
5
+ context "#name" do
6
+ it "returns 'api-service' if no ENV['SERVICE_NAME'] is set" do
7
+ ENV['SERVICE_NAME'] = nil
8
+ Napa::Identity.name.should == 'api-service'
9
+ end
10
+
11
+ it "returns the ENV['SERVICE_NAME'] when specified" do
12
+ ENV['SERVICE_NAME'] = nil
13
+ ENV['SERVICE_NAME'] = 'my-service'
14
+ Napa::Identity.name.should == 'my-service'
15
+ end
16
+ end
17
+
18
+ context "#hostname" do
19
+ it "returns the value of the hostname system call and doesn't make a second system call" do
20
+ Napa::Identity.should_receive(:`).with("hostname").and_return("system-hostname")
21
+ Napa::Identity.hostname.should == 'system-hostname'
22
+
23
+ Napa::Identity.should_not_receive(:`).with("hostname")
24
+ Napa::Identity.hostname.should == 'system-hostname'
25
+ end
26
+ end
27
+
28
+ context "#revision" do
29
+ it "returns the value of the 'git rev-parse HEAD' system call and doesn't make a second system call" do
30
+ Napa::Identity.should_receive(:`).with("git rev-parse HEAD").and_return("12345")
31
+ Napa::Identity.revision.should == '12345'
32
+
33
+ Napa::Identity.should_not_receive(:`).with("git rev-parse HEAD")
34
+ Napa::Identity.revision.should == '12345'
35
+ end
36
+ end
37
+
38
+ context "#pid" do
39
+ it "returns the process ID value" do
40
+ Process.stub(:pid).and_return(112233)
41
+ Napa::Identity.pid.should == 112233
42
+ end
43
+ end
44
+
45
+ context "#platform_revision" do
46
+ it "returns the current version of the platform gem" do
47
+ Napa::Identity.platform_revision.should == Napa::VERSION
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,34 @@
1
+ require 'spec_helper'
2
+ require 'napa/logger/log_transaction'
3
+
4
+ describe Napa::LogTransaction do
5
+ before(:each) do
6
+ Napa::LogTransaction.clear
7
+ end
8
+
9
+ context "#id" do
10
+ it "returns the current transaction id if it has been set" do
11
+ id = SecureRandom.hex(10)
12
+ Thread.current[:napa_tid] = id
13
+ Napa::LogTransaction.id.should == id
14
+ end
15
+
16
+ it "sets and returns a new id if the transaction id hasn't been set" do
17
+ Napa::LogTransaction.id.should_not be_nil
18
+ end
19
+
20
+ it "allows the id to be overridden by a setter" do
21
+ Napa::LogTransaction.id.should_not be_nil
22
+ Napa::LogTransaction.id = 'foo'
23
+ Napa::LogTransaction.id.should == 'foo'
24
+ end
25
+ end
26
+
27
+ context "#clear" do
28
+ it "sets the id to nil" do
29
+ Napa::LogTransaction.id.should_not be_nil
30
+ Napa::LogTransaction.clear
31
+ Thread.current[:napa_tid].should be_nil
32
+ end
33
+ end
34
+ end
@@ -0,0 +1 @@
1
+ require 'napa'
@@ -0,0 +1,40 @@
1
+ require 'spec_helper'
2
+ require 'napa/version'
3
+
4
+ describe Napa::Version do
5
+ context "#major_bump" do
6
+ it "should set the major revision value, and the rest should be 0" do
7
+ stub_const("Napa::VERSION", "1.2.3")
8
+ Napa::Version.next_major.should == "2.0.0"
9
+ end
10
+
11
+ it "should set the major revision value, and the rest should be 0" do
12
+ stub_const("Napa::VERSION", "5.0.0")
13
+ Napa::Version.next_major.should == "6.0.0"
14
+ end
15
+ end
16
+
17
+ context "#minor_bump" do
18
+ it "should set the minor revision value, leaving the major value unchanged and the patch value to 0" do
19
+ stub_const("Napa::VERSION", "1.2.3")
20
+ Napa::Version.next_minor.should == "1.3.0"
21
+ end
22
+
23
+ it "should set the minor revision value, leaving the major value unchanged and the patch value to 0" do
24
+ stub_const("Napa::VERSION", "0.5.0")
25
+ Napa::Version.next_minor.should == "0.6.0"
26
+ end
27
+ end
28
+
29
+ context "patch_bump" do
30
+ it "should set the patch revision value, leaving the major and minor values unchanged" do
31
+ stub_const("Napa::VERSION", "1.2.3")
32
+ Napa::Version.next_patch.should == "1.2.4"
33
+ end
34
+
35
+ it "should set the patch revision value, leaving the major and minor values unchanged" do
36
+ stub_const("Napa::VERSION", "5.5.5")
37
+ Napa::Version.next_patch.should == "5.5.6"
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,51 @@
1
+ desc "bump the gem version"
2
+ namespace :version do
3
+ namespace :bump do
4
+
5
+ task :major do
6
+ @new_version = Napa::Version.next_major
7
+ execute_version_bump
8
+ end
9
+
10
+ task :minor do
11
+ @new_version = Napa::Version.next_minor
12
+ execute_version_bump
13
+ end
14
+
15
+ task :patch do
16
+ @new_version = Napa::Version.next_patch
17
+ execute_version_bump
18
+ end
19
+
20
+ def execute_version_bump
21
+ if !clean_staging_area?
22
+ system "git status"
23
+ raise "Unclean staging area! Be sure to commit or .gitignore everything first. See `git status` above."
24
+ else
25
+ require 'git'
26
+ git = Git.open('.')
27
+
28
+ write_update
29
+ git.add('lib/napa/version.rb')
30
+ git.commit("Version bump: #{release_tag}")
31
+ git.add_tag(release_tag)
32
+ git.push(git.remote('origin'), git.branch, release_tag) if git.remote('origin')
33
+ puts "Version bumped: #{release_tag}"
34
+ end
35
+ end
36
+
37
+ def write_update
38
+ filedata = File.read('lib/napa/version.rb')
39
+ changed_filedata = filedata.gsub("VERSION = \"#{Napa::VERSION}\"\n", "VERSION = \"#{@new_version}\"\n")
40
+ File.open('lib/napa/version.rb',"w"){|file| file.puts changed_filedata}
41
+ end
42
+
43
+ def clean_staging_area?
44
+ `git ls-files --deleted --modified --others --exclude-standard` == ""
45
+ end
46
+
47
+ def release_tag
48
+ "v#{@new_version}"
49
+ end
50
+ end
51
+ end