service_template 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (105) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +21 -0
  3. data/.rubocop.yml +23 -0
  4. data/.travis.yml +13 -0
  5. data/CHANGELOG.md +64 -0
  6. data/Gemfile +4 -0
  7. data/LICENSE +24 -0
  8. data/README.md +217 -0
  9. data/Rakefile +9 -0
  10. data/bin/service_template +5 -0
  11. data/lib/service_template.rb +55 -0
  12. data/lib/service_template/active_record_extensions/notifications_subscriber.rb +17 -0
  13. data/lib/service_template/active_record_extensions/seeder.rb +14 -0
  14. data/lib/service_template/active_record_extensions/stats.rb +37 -0
  15. data/lib/service_template/authentication.rb +8 -0
  16. data/lib/service_template/cli.rb +111 -0
  17. data/lib/service_template/deploy.rb +98 -0
  18. data/lib/service_template/gem_dependency.rb +37 -0
  19. data/lib/service_template/generators.rb +3 -0
  20. data/lib/service_template/generators/api_generator.rb +30 -0
  21. data/lib/service_template/generators/readme_generator.rb +47 -0
  22. data/lib/service_template/generators/scaffold_generator.rb +29 -0
  23. data/lib/service_template/generators/templates/api/app/apis/%name_tableize%_api.rb.tt +40 -0
  24. data/lib/service_template/generators/templates/api/app/models/%name_underscore%.rb.tt +2 -0
  25. data/lib/service_template/generators/templates/api/app/representers/%name_underscore%_representer.rb.tt +4 -0
  26. data/lib/service_template/generators/templates/api/spec/apis/%name_tableize%_api_spec.rb.tt +16 -0
  27. data/lib/service_template/generators/templates/api/spec/models/%name_underscore%_spec.rb.tt +9 -0
  28. data/lib/service_template/generators/templates/readme/README.md.tt +55 -0
  29. data/lib/service_template/generators/templates/readme/spec/docs/readme_spec.rb +7 -0
  30. data/lib/service_template/generators/templates/scaffold/.env.development.tt +9 -0
  31. data/lib/service_template/generators/templates/scaffold/.env.test.tt +10 -0
  32. data/lib/service_template/generators/templates/scaffold/.gitignore.tt +13 -0
  33. data/lib/service_template/generators/templates/scaffold/.rubocop.yml +24 -0
  34. data/lib/service_template/generators/templates/scaffold/.ruby-version.tt +1 -0
  35. data/lib/service_template/generators/templates/scaffold/Gemfile.tt +29 -0
  36. data/lib/service_template/generators/templates/scaffold/README.md +3 -0
  37. data/lib/service_template/generators/templates/scaffold/Rakefile +21 -0
  38. data/lib/service_template/generators/templates/scaffold/app.rb +19 -0
  39. data/lib/service_template/generators/templates/scaffold/app/apis/application_api.rb +9 -0
  40. data/lib/service_template/generators/templates/scaffold/app/apis/hello_api.rb.tt +10 -0
  41. data/lib/service_template/generators/templates/scaffold/config.ru.tt +21 -0
  42. data/lib/service_template/generators/templates/scaffold/config/database.yml.tt +19 -0
  43. data/lib/service_template/generators/templates/scaffold/config/initializers/active_record.rb +5 -0
  44. data/lib/service_template/generators/templates/scaffold/db/schema.rb +11 -0
  45. data/lib/service_template/generators/templates/scaffold/lib/.keep +0 -0
  46. data/lib/service_template/generators/templates/scaffold/log/.keep +0 -0
  47. data/lib/service_template/generators/templates/scaffold/spec/apis/hello_api_spec.rb.tt +17 -0
  48. data/lib/service_template/generators/templates/scaffold/spec/factories/.gitkeep +0 -0
  49. data/lib/service_template/generators/templates/scaffold/spec/spec_helper.rb +47 -0
  50. data/lib/service_template/grape_extenders.rb +30 -0
  51. data/lib/service_template/grape_extensions/error_formatter.rb +18 -0
  52. data/lib/service_template/grape_extensions/grape_helpers.rb +27 -0
  53. data/lib/service_template/identity.rb +45 -0
  54. data/lib/service_template/json_error.rb +24 -0
  55. data/lib/service_template/logger/log_transaction.rb +17 -0
  56. data/lib/service_template/logger/logger.rb +42 -0
  57. data/lib/service_template/logger/parseable.rb +37 -0
  58. data/lib/service_template/middleware/app_monitor.rb +17 -0
  59. data/lib/service_template/middleware/authentication.rb +32 -0
  60. data/lib/service_template/middleware/database_stats.rb +15 -0
  61. data/lib/service_template/middleware/logger.rb +67 -0
  62. data/lib/service_template/middleware/request_stats.rb +42 -0
  63. data/lib/service_template/output_formatters/entity.rb +15 -0
  64. data/lib/service_template/output_formatters/include_nil.rb +16 -0
  65. data/lib/service_template/output_formatters/json_api_representer.rb +9 -0
  66. data/lib/service_template/param_sanitizer.rb +30 -0
  67. data/lib/service_template/rspec_extensions/response_helpers.rb +46 -0
  68. data/lib/service_template/setup.rb +36 -0
  69. data/lib/service_template/sortable_api.rb +17 -0
  70. data/lib/service_template/stats.rb +43 -0
  71. data/lib/service_template/stats_d_timer.rb +26 -0
  72. data/lib/service_template/version.rb +45 -0
  73. data/lib/tasks/deploy.rake +11 -0
  74. data/lib/tasks/routes.rake +11 -0
  75. data/service_template.gemspec +42 -0
  76. data/spec/active_record_extensions/filter_by_hash_spec.rb +23 -0
  77. data/spec/active_record_extensions/seeder_spec.rb +13 -0
  78. data/spec/authentication_spec.rb +17 -0
  79. data/spec/deprecations/application_api_spec.rb +19 -0
  80. data/spec/deprecations/entity_spec.rb +9 -0
  81. data/spec/deprecations/filter_by_hash_spec.rb +9 -0
  82. data/spec/deprecations/napa_setup_spec.rb +52 -0
  83. data/spec/generators/api_generator_spec.rb +63 -0
  84. data/spec/generators/migration_generator_spec.rb +105 -0
  85. data/spec/generators/readme_generator_spec.rb +35 -0
  86. data/spec/generators/scaffold_generator_spec.rb +90 -0
  87. data/spec/grape_extenders_spec.rb +50 -0
  88. data/spec/grape_extensions/error_formatter_spec.rb +29 -0
  89. data/spec/grape_extensions/include_nil_spec.rb +23 -0
  90. data/spec/identity_spec.rb +50 -0
  91. data/spec/json_error_spec.rb +33 -0
  92. data/spec/logger/log_transaction_spec.rb +34 -0
  93. data/spec/logger/logger_spec.rb +14 -0
  94. data/spec/logger/parseable_spec.rb +16 -0
  95. data/spec/middleware/authentication_spec.rb +54 -0
  96. data/spec/middleware/database_stats_spec.rb +64 -0
  97. data/spec/middleware/request_stats_spec.rb +21 -0
  98. data/spec/sortable_api_spec.rb +56 -0
  99. data/spec/spec_helper.rb +45 -0
  100. data/spec/stats_d_timer_spec.rb +23 -0
  101. data/spec/stats_spec.rb +66 -0
  102. data/spec/version_spec.rb +40 -0
  103. data/tasks/spec.rake +9 -0
  104. data/tasks/version.rake +51 -0
  105. metadata +456 -0
@@ -0,0 +1,17 @@
1
+ if defined?(ActiveRecord)
2
+ ActiveSupport::Notifications.subscribe 'sql.active_record' do |name, start, finish, id, payload|
3
+ if payload[:sql].downcase.match(/(select|update|insert|delete)(.+)/i)
4
+ table, action = ServiceTemplate::ActiveRecordStats.extract_sql_content(payload[:sql])
5
+ end
6
+
7
+ if table
8
+ ServiceTemplate::Stats.emitter.timing(
9
+ "sql.query_time",
10
+ (finish - start) * 1000)
11
+
12
+ ServiceTemplate::Stats.emitter.timing(
13
+ "sql.table.#{table}.#{action.downcase}.query_time",
14
+ (finish - start) * 1000)
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,14 @@
1
+ if defined?(ActiveRecord)
2
+ module ServiceTemplate
3
+ class ActiveRecordSeeder
4
+ def initialize(seed_file)
5
+ @seed_file = seed_file
6
+ end
7
+
8
+ def load_seed
9
+ raise "Seed file '#{@seed_file}' does not exist" unless File.file?(@seed_file)
10
+ load @seed_file
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,37 @@
1
+ if defined?(ActiveSupport) && defined?(ActiveRecord)
2
+ module ServiceTemplate
3
+ module ActiveRecordStats
4
+ SQL_INSERT_DELETE_PARSER_REGEXP = /^(?<action>\w+)\s(\w+)\s\W*(?<table>\w+)/
5
+ SQL_SELECT_REGEXP = /select .*? FROM \W*(?<table>\w+)/i
6
+ SQL_UPDATE_REGEXP = /update \W*(?<table>\w+)/i
7
+
8
+ # Returns the table and query type
9
+ def self.extract_from_sql_inserts_deletes(query)
10
+ m = query.match(SQL_INSERT_DELETE_PARSER_REGEXP)
11
+ [m[:table], m[:action]]
12
+ end
13
+
14
+ def self.extract_sql_selects(query)
15
+ m = query.match(SQL_SELECT_REGEXP)
16
+ [m[:table], 'SELECT']
17
+ end
18
+
19
+ def self.extract_sql_updates(query)
20
+ m = query.match(SQL_UPDATE_REGEXP)
21
+ [m[:table], 'UPDATE']
22
+ end
23
+
24
+ def self.extract_sql_content(query)
25
+ table = action = nil
26
+ if query.match(SQL_UPDATE_REGEXP)
27
+ table, action = extract_sql_updates(query)
28
+ elsif query.match(SQL_SELECT_REGEXP)
29
+ table, action = extract_sql_selects(query)
30
+ elsif query.match(SQL_INSERT_DELETE_PARSER_REGEXP)
31
+ table, action = extract_from_sql_inserts_deletes(query)
32
+ end
33
+ [table, action]
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,8 @@
1
+ module ServiceTemplate
2
+ class Authentication
3
+ def self.password_header
4
+ raise 'header_password_not_configured' unless ENV['HEADER_PASSWORD']
5
+ { 'Password' => ENV['HEADER_PASSWORD'] }
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,111 @@
1
+ require 'thor'
2
+ require 'service_template/generators'
3
+ require 'service_template/deploy'
4
+ require 'service_template/version'
5
+
6
+ ServiceTemplate.load_environment if defined?(Dotenv)
7
+
8
+ module ServiceTemplate
9
+ class CLI
10
+ class Generate < Thor
11
+ register(
12
+ Generators::ApiGenerator,
13
+ 'api',
14
+ 'api <api_name>',
15
+ 'Create a Grape API, Model and Entity'
16
+ )
17
+
18
+ register(
19
+ Generators::ReadmeGenerator,
20
+ 'readme',
21
+ 'readme',
22
+ 'Create a formatted README'
23
+ )
24
+ end
25
+ end
26
+
27
+ class CLI
28
+ class Base < Thor
29
+ desc "version", "Shows the ServiceTemplate version number"
30
+ def version
31
+ say ServiceTemplate::VERSION
32
+ end
33
+
34
+ desc 'console [environment]', 'Start the ServiceTemplate console'
35
+ options aliases: 'c'
36
+ def console(environment = nil)
37
+ ServiceTemplate.env = environment || 'development'
38
+
39
+ require 'racksh/init'
40
+
41
+ begin
42
+ require "pry"
43
+ interpreter = Pry
44
+ rescue LoadError
45
+ require "irb"
46
+ require "irb/completion"
47
+ interpreter = IRB
48
+ # IRB uses ARGV and does not expect these arguments.
49
+ ARGV.delete('console')
50
+ ARGV.delete(environment) if environment
51
+ end
52
+
53
+ Rack::Shell.init
54
+
55
+ $0 = "#{$0} console"
56
+ interpreter.start
57
+ end
58
+
59
+ desc 'server', "Start the ServiceTemplate server"
60
+ options aliases: 's'
61
+ def server
62
+ puts "ServiceTemplate server starting..."
63
+
64
+ require 'pty'
65
+ exit = "... ServiceTemplate server exited!"
66
+
67
+ begin
68
+ PTY.spawn('shotgun') do |stdout, stdin, pid|
69
+ begin
70
+ Signal.trap('INT') { Process.kill('INT', pid) }
71
+ stdout.each { |line| puts line }
72
+ rescue Errno::EIO
73
+ puts exit
74
+ end
75
+ end
76
+ rescue PTY::ChildExited
77
+ puts exit
78
+ end
79
+ end
80
+
81
+ desc 'deploy [target]', 'Deploys A Service to a given target (i.e. production, staging, etc.)'
82
+ method_options :force => :boolean, :revision => :string, :confirm => :boolean
83
+ def deploy(environment)
84
+ if options[:confirm] || yes?('Are you sure you want to deploy this service?', Thor::Shell::Color::YELLOW)
85
+ deploy = ServiceTemplate::Deploy.new(environment, force: options[:force], revision: options[:revision])
86
+ if deploy.deployable?
87
+ say(deploy.deploy!, Thor::Shell::Color::GREEN)
88
+ else
89
+ say("Deploy Failed:\n#{deploy.errors.join("\n")}", Thor::Shell::Color::RED)
90
+ end
91
+ end
92
+ end
93
+
94
+ register(
95
+ Generators::ScaffoldGenerator,
96
+ 'new',
97
+ 'new <app_name> [app_path]',
98
+ 'Create a scaffold for a new ServiceTemplate service'
99
+ )
100
+
101
+ desc "generate api <api_name>", "Create a Grape API, Model and Representer"
102
+ subcommand "generate api", ServiceTemplate::CLI::Generate
103
+
104
+ desc "generate migration <migration_name> [field[:type][:index] field[:type][:index]]", "Create a Database Migration"
105
+ subcommand "generate", ServiceTemplate::CLI::Generate
106
+
107
+ desc "generate readme", "Create a formatted README"
108
+ subcommand "generate readme", ServiceTemplate::CLI::Generate
109
+ end
110
+ end
111
+ end
@@ -0,0 +1,98 @@
1
+ require 'git'
2
+ require 'octokit'
3
+
4
+ module ServiceTemplate
5
+ class Deploy
6
+ attr_reader :errors, :github_login
7
+
8
+ def initialize(environment, revision: nil, force: false, github_repo: nil, github_token: nil)
9
+ ServiceTemplate.load_environment
10
+
11
+ @github_repo = github_repo || ENV['GITHUB_REPO']
12
+ @github_token = github_token || ENV['GITHUB_OAUTH_TOKEN']
13
+ @environment = environment
14
+ @revision = revision || local_head_revision
15
+ @force = force
16
+
17
+ @errors = []
18
+ end
19
+
20
+ def deploy!
21
+ if deployable?
22
+ set_github_tag
23
+ "#{@revision} tagged as #{@environment} by #{@github_login} at #{Time.now.to_s(:long)}"
24
+ else
25
+ "Deploy error(s): #{@errors.join(' --- ')}"
26
+ end
27
+ end
28
+
29
+ def set_github_tag
30
+ begin
31
+ github_client.update_ref(
32
+ @github_repo,
33
+ "tags/#{@environment}",
34
+ @revision,
35
+ @force
36
+ )
37
+ rescue Octokit::UnprocessableEntity
38
+ github_client.create_ref(
39
+ @github_repo,
40
+ "tags/#{@environment}",
41
+ @revision
42
+ )
43
+ end
44
+ end
45
+
46
+ def local_repo
47
+ @local_repo ||= Git.open('.')
48
+ end
49
+
50
+ def local_repo_status
51
+ @local_repo_status ||= local_repo.status
52
+ end
53
+
54
+ def github_client
55
+ return @github_client if @github_client
56
+ @errors << "ENV['GITHUB_REPO'] is not defined" if @github_repo.nil?
57
+ @errors << "ENV['GITHUB_OAUTH_TOKEN'] is not defined" if @github_token.nil?
58
+
59
+ client = Octokit::Client.new(access_token: @github_token)
60
+ begin
61
+ @github_login = client.login
62
+ return @github_client = client
63
+ rescue Octokit::Unauthorized
64
+ @errors << "Access denied for GITHUB_OAUTH_TOKEN"
65
+ end
66
+ end
67
+
68
+ def local_head_revision
69
+ local_repo.object('HEAD').sha
70
+ end
71
+
72
+ def deployable?
73
+ revision_exists_on_github?
74
+ any_local_uncommited_changes?
75
+ any_local_untracked_files?
76
+
77
+ @errors.empty? || @force
78
+ end
79
+
80
+ def revision_exists_on_github?
81
+ begin
82
+ github_client.commit(@github_repo, @revision)
83
+ rescue Octokit::NotFound
84
+ @errors << "Revision #{@revision} does not exist on #{@github_repo}, make sure you've merged your changes."
85
+ end
86
+ end
87
+
88
+ def any_local_uncommited_changes?
89
+ changes = local_repo_status.changed.collect{|change| change[0]}
90
+ @errors << "#{changes.to_sentence} have been changed and not committed." if changes.any?
91
+ end
92
+
93
+ def any_local_untracked_files?
94
+ changes = local_repo_status.untracked.collect{|change| change[0]}
95
+ @errors << "#{changes.to_sentence} file(s) have been added and are not tracked." if changes.any?
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,37 @@
1
+ require 'active_support/json'
2
+
3
+ module ServiceTemplate
4
+ class GemDependency
5
+ def self.log_all
6
+ ServiceTemplate::Logger.logger.info(gems: list_all.as_json)
7
+ end
8
+
9
+ def self.list_all
10
+ Gem.loaded_specs.map { |spec| new(spec).to_hash }
11
+ end
12
+
13
+ def initialize(spec)
14
+ @spec = spec[1]
15
+ end
16
+
17
+ def name
18
+ @spec.name
19
+ end
20
+
21
+ def version
22
+ @spec.version.to_s
23
+ end
24
+
25
+ def git_version
26
+ @spec.git_version
27
+ end
28
+
29
+ def to_hash
30
+ {}.tap do |h|
31
+ h[:name] = name
32
+ h[:version] = version
33
+ h[:git_version] = git_version if git_version
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,3 @@
1
+ require 'service_template/generators/scaffold_generator'
2
+ require 'service_template/generators/api_generator'
3
+ require 'service_template/generators/readme_generator'
@@ -0,0 +1,30 @@
1
+ require 'thor'
2
+ require 'active_support/all'
3
+
4
+ module ServiceTemplate
5
+ module Generators
6
+ class ApiGenerator < Thor::Group
7
+ include Thor::Actions
8
+ argument :name
9
+
10
+ def name_underscore
11
+ name.underscore
12
+ end
13
+
14
+ def name_tableize
15
+ name.tableize
16
+ end
17
+
18
+ def output_directory
19
+ '.'
20
+ end
21
+
22
+ def api
23
+ self.class.source_root "#{File.dirname(__FILE__)}/templates/api"
24
+ say 'Generating api...'
25
+ directory '.', output_directory
26
+ say 'Done!', :green
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,47 @@
1
+ require 'thor'
2
+ require 'active_support/all'
3
+ require 'service_template/setup'
4
+ require 'service_template/identity'
5
+ require 'dotenv'
6
+
7
+ module ServiceTemplate
8
+ module Generators
9
+ class ReadmeGenerator < Thor::Group
10
+ include Thor::Actions
11
+
12
+ def load_environment
13
+ ServiceTemplate.load_environment
14
+ end
15
+
16
+ def service_name
17
+ ServiceTemplate::Identity.name
18
+ end
19
+
20
+ def routes
21
+ routes = ""
22
+
23
+ if defined? ApplicationApi
24
+ ApplicationApi.routes.each do |api|
25
+ method = api.route_method.ljust(10)
26
+ path = api.route_path.ljust(40)
27
+ description = api.route_description
28
+ routes += " #{method} #{path} # #{description}"
29
+ end
30
+ end
31
+
32
+ routes
33
+ end
34
+
35
+ def output_directory
36
+ '.'
37
+ end
38
+
39
+ def readme
40
+ self.class.source_root "#{File.dirname(__FILE__)}/templates/readme"
41
+ say 'Generating readme...'
42
+ directory '.', output_directory
43
+ say 'Done!', :green
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,29 @@
1
+ require 'thor'
2
+ require 'active_support/core_ext/string'
3
+
4
+ module ServiceTemplate
5
+ module Generators
6
+ class ScaffoldGenerator < Thor::Group
7
+ include Thor::Actions
8
+
9
+ source_root "#{File.dirname(__FILE__)}/templates/scaffold"
10
+
11
+ argument :app_name
12
+ argument :app_path, optional: true
13
+ class_option :database, default: 'mysql', aliases: '-d', desc: 'Preconfigure for selected database (options: mysql/postgres/pg)'
14
+
15
+ def generate
16
+ say 'Generating scaffold...'
17
+
18
+ @database_gem = ['pg','postgres'].include?(options[:database]) ? 'pg' : 'mysql2'
19
+ @database_adapter = ['pg','postgres'].include?(options[:database]) ? 'postgresql' : 'mysql2'
20
+ @database_encoding = ['pg','postgres'].include?(options[:database]) ? 'unicode' : 'utf8'
21
+ @database_user = ['pg','postgres'].include?(options[:database]) ? '' : 'root'
22
+
23
+ directory ".", (app_path || app_name)
24
+
25
+ say 'Done!', :green
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,40 @@
1
+ class <%= name.classify.pluralize %>Api < Grape::API
2
+ desc 'Get a list of <%= name.underscore.tableize %>'
3
+ params do
4
+ optional :ids, type: Array, desc: 'Array of <%= name.underscore %> ids'
5
+ end
6
+ get do
7
+ <%= name.underscore.tableize %> = params[:ids] ? <%= name.classify %>.where(id: params[:ids]) : <%= name.classify %>.all
8
+ represent <%= name.underscore.tableize %>, with: <%= name.classify %>Representer
9
+ end
10
+
11
+ desc 'Create an <%= name.underscore %>'
12
+ params do
13
+ end
14
+
15
+ post do
16
+ <%= name.underscore %> = <%= name.classify %>.create!(permitted_params)
17
+ represent <%= name.underscore %>, with: <%= name.classify %>Representer
18
+ end
19
+
20
+ params do
21
+ requires :id, desc: 'ID of the <%= name.underscore %>'
22
+ end
23
+ route_param :id do
24
+ desc 'Get an <%= name.underscore %>'
25
+ get do
26
+ <%= name.underscore %> = <%= name.classify %>.find(params[:id])
27
+ represent <%= name.underscore %>, with: <%= name.classify %>Representer
28
+ end
29
+
30
+ desc 'Update an <%= name.underscore %>'
31
+ params do
32
+ end
33
+ put do
34
+ # fetch <%= name.underscore %> record and update attributes. exceptions caught in app.rb
35
+ <%= name.underscore %> = <%= name.classify %>.find(params[:id])
36
+ <%= name.underscore %>.update_attributes!(permitted_params)
37
+ represent <%= name.underscore %>, with: <%= name.classify %>Representer
38
+ end
39
+ end
40
+ end