service_template 0.5.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 (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