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,27 @@
1
+ module ServiceTemplate
2
+ module GrapeHelpers
3
+ def represent(data, with: nil, **args)
4
+ raise ArgumentError.new(":with option is required") if with.nil?
5
+
6
+ if data.respond_to?(:to_a)
7
+ return { data: data.map{ |item| with.new(item).to_hash(args) } }
8
+ else
9
+ return { data: with.new(data).to_hash(args)}
10
+ end
11
+ end
12
+
13
+ def present_error(code, message = '', reasons={})
14
+ ServiceTemplate::JsonError.new(code, message, reasons)
15
+ end
16
+
17
+ def permitted_params(options = {})
18
+ options = { include_missing: false }.merge(options)
19
+ declared(params, options)
20
+ end
21
+
22
+ # extend all endpoints to include this
23
+ Grape::Endpoint.send :include, self if defined?(Grape)
24
+ # rails 4 controller concern
25
+ extend ActiveSupport::Concern if defined?(Rails)
26
+ end
27
+ end
@@ -0,0 +1,45 @@
1
+ module ServiceTemplate
2
+ class Identity
3
+ def self.health
4
+ {
5
+ name: name,
6
+ hostname: hostname,
7
+ revision: revision,
8
+ pid: pid,
9
+ parent_pid: parent_pid,
10
+ platform: platform
11
+ }
12
+ end
13
+
14
+ def self.name
15
+ ENV['SERVICE_NAME'] || 'api-service'
16
+ end
17
+
18
+ def self.hostname
19
+ @hostname ||= `hostname`.strip
20
+ end
21
+
22
+ def self.revision
23
+ @revision ||= `git rev-parse HEAD`.strip
24
+ end
25
+
26
+ def self.pid
27
+ @pid ||= Process.pid
28
+ end
29
+
30
+ def self.parent_pid
31
+ @ppid ||= Process.ppid
32
+ end
33
+
34
+ def self.platform
35
+ {
36
+ version: platform_revision,
37
+ name: "ServiceTemplate"
38
+ }
39
+ end
40
+
41
+ def self.platform_revision
42
+ ServiceTemplate::VERSION
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,24 @@
1
+ module ServiceTemplate
2
+ class JsonError
3
+ def initialize(code, message, reasons={})
4
+ @code = code
5
+ @message = message
6
+ @reasons = reasons
7
+ end
8
+
9
+ def to_json(options = {})
10
+ to_h.to_json(options)
11
+ end
12
+
13
+ def to_h
14
+ e = {
15
+ error: {
16
+ code: @code,
17
+ message: @message
18
+ }
19
+ }
20
+ e[:error][:reasons] = @reasons if @reasons.present?
21
+ e
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,17 @@
1
+ module ServiceTemplate
2
+ class LogTransaction
3
+ class << self
4
+ def id
5
+ Thread.current[:service_template_tid].nil? ? Thread.current[:service_template_tid] = SecureRandom.hex(10) : Thread.current[:service_template_tid]
6
+ end
7
+
8
+ def id=(id)
9
+ Thread.current[:service_template_tid] = id
10
+ end
11
+
12
+ def clear
13
+ Thread.current[:service_template_tid] = nil
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,42 @@
1
+ module ServiceTemplate
2
+ class Logger
3
+ class << self
4
+ def name
5
+ [ServiceTemplate::Identity.name, ServiceTemplate::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/#{ServiceTemplate.env}.log",
20
+ layout: Logging.layouts.json
21
+ )
22
+
23
+ @logger = Logging.logger["[#{name}]"]
24
+ @logger.add_appenders 'stdout' unless ServiceTemplate.env.test?
25
+ @logger.add_appenders "log/#{ServiceTemplate.env}.log"
26
+ end
27
+
28
+ @logger
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
40
+ end
41
+ end
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
@@ -0,0 +1,17 @@
1
+ module ServiceTemplate
2
+ class Middleware
3
+ class AppMonitor
4
+ def initialize(app)
5
+ @app = app
6
+ end
7
+
8
+ def call(env)
9
+ if ["/health", "/health.json"].include? env['PATH_INFO']
10
+ [200, { 'Content-type' => 'application/json' }, [ServiceTemplate::Identity.health.to_json]]
11
+ else
12
+ @app.call(env)
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,32 @@
1
+ module ServiceTemplate
2
+ class Middleware
3
+ class Authentication
4
+ def initialize(app)
5
+ @app = app
6
+
7
+ if ENV['HEADER_PASSWORDS']
8
+ @allowed_passwords = ENV['HEADER_PASSWORDS'].split(',').map { |pw| pw.strip }.freeze
9
+ end
10
+ end
11
+
12
+ def call(env)
13
+ if authenticated_request?(env)
14
+ @app.call(env)
15
+ else
16
+ if @allowed_passwords
17
+ error_response = ServiceTemplate::JsonError.new('bad_password', 'bad password').to_json
18
+ else
19
+ error_response = ServiceTemplate::JsonError.new('not_configured', 'password not configured').to_json
20
+ end
21
+
22
+ [401, { 'Content-type' => 'application/json' }, Array.wrap(error_response)]
23
+ end
24
+
25
+ end
26
+
27
+ def authenticated_request?(env)
28
+ @allowed_passwords.include? env['HTTP_PASSWORD'] unless @allowed_passwords.nil?
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,15 @@
1
+ module ServiceTemplate
2
+ class Middleware
3
+ class DatabaseStats
4
+ def initialize(app)
5
+ @app = app
6
+ end
7
+
8
+ def call(env)
9
+ require 'service_template/active_record_extensions/notifications_subscriber'
10
+ status, headers, body = @app.call(env)
11
+ [status, headers, body]
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,67 @@
1
+ require 'service_template/param_sanitizer'
2
+
3
+ module ServiceTemplate
4
+ class Middleware
5
+ class Logger
6
+ include ServiceTemplate::ParamSanitizer
7
+
8
+ def initialize(app)
9
+ @app = app
10
+ end
11
+
12
+ def call(env)
13
+ # log the request
14
+ ServiceTemplate::Logger.logger.info format_request(env)
15
+
16
+ # process the request
17
+ status, headers, body = @app.call(env)
18
+
19
+ # log the response
20
+ ServiceTemplate::Logger.logger.debug format_response(status, headers, body)
21
+
22
+ # return the results
23
+ [status, headers, body]
24
+ ensure
25
+ # Clear the transaction id after each request
26
+ ServiceTemplate::LogTransaction.clear
27
+ end
28
+
29
+ private
30
+
31
+ def format_request(env)
32
+ request = Rack::Request.new(env)
33
+ params = request.params
34
+
35
+ begin
36
+ params = JSON.parse(request.body.read) if env['CONTENT_TYPE'] == 'application/json'
37
+ rescue
38
+ # do nothing, params is already set
39
+ end
40
+
41
+ request_data = {
42
+ method: request.request_method,
43
+ path: request.path_info,
44
+ query: filtered_query_string(request.query_string),
45
+ host: ServiceTemplate::Identity.hostname,
46
+ pid: ServiceTemplate::Identity.pid,
47
+ revision: ServiceTemplate::Identity.revision,
48
+ params: filtered_parameters(params),
49
+ remote_ip: request.ip
50
+ }
51
+ request_data[:user_id] = current_user.try(:id) if defined?(current_user)
52
+ { request: request_data }
53
+ end
54
+
55
+ def format_response(status, headers, body)
56
+ response_body = nil
57
+ begin
58
+ response_body = body.respond_to?(:body) ? body.body.map { |r| r } : nil
59
+ rescue
60
+ response_body = body.inspect
61
+ end
62
+
63
+ ServiceTemplate::Logger.response(status, headers, response_body)
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,42 @@
1
+ module ServiceTemplate
2
+ class Middleware
3
+ class RequestStats
4
+ def initialize(app)
5
+ @app = app
6
+ end
7
+
8
+ def normalize_path(path)
9
+ case
10
+ when path == '/'
11
+ 'root'
12
+ else
13
+ path.start_with?('/') ? path[1..-1] : path
14
+ end
15
+ end
16
+
17
+ def call(env)
18
+ # Mark the request time
19
+ start = Time.now
20
+
21
+ # Process the request
22
+ status, headers, body = @app.call(env)
23
+
24
+ # Mark the response time
25
+ stop = Time.now
26
+
27
+ # Calculate total response time
28
+ response_time = (stop - start) * 1000
29
+
30
+ request = Rack::Request.new(env)
31
+ path = normalize_path(request.path_info)
32
+
33
+ # Emit stats to StatsD
34
+ ServiceTemplate::Stats.emitter.timing('response_time', response_time)
35
+ ServiceTemplate::Stats.emitter.timing("path.#{ServiceTemplate::Stats.path_to_key(request.request_method, path)}.response_time", response_time)
36
+
37
+ # Return the results
38
+ [status, headers, body]
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,15 @@
1
+ module ServiceTemplate
2
+ class Entity < Grape::Entity
3
+ def self.inherited(subclass)
4
+ ActiveSupport::Deprecation.warn 'Use of ServiceTemplate::Entity is discouraged, please transition your code to Roar representers - https://github.com/bellycard/service_template/blob/master/docs/grape_entity_to_roar.md', caller
5
+ end
6
+
7
+ format_with :to_s do |val|
8
+ val.to_s
9
+ end
10
+
11
+ def object_type
12
+ object.class.name.underscore
13
+ end
14
+ end
15
+ 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 ServiceTemplate
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
@@ -0,0 +1,9 @@
1
+ require 'roar/decorator'
2
+ require 'roar/json/json_api'
3
+
4
+ module ServiceTemplate
5
+ class JsonApiRepresenter < Roar::Decorator
6
+ include Roar::JSON::JSONAPI
7
+
8
+ end
9
+ end
@@ -0,0 +1,30 @@
1
+ require 'action_dispatch/http/filter_parameters'
2
+
3
+ module ServiceTemplate
4
+ module ParamSanitizer
5
+ include ActionDispatch::Http::FilterParameters
6
+
7
+ mattr_accessor :filter_params
8
+
9
+ KV_REGEXP = '[^&;=]+'
10
+ PAIR_REGEXP = %r{(#{KV_REGEXP})=(#{KV_REGEXP})}
11
+
12
+ def filter_params
13
+ @@filter_params || []
14
+ end
15
+
16
+ def parameter_filter
17
+ parameter_filter_for(filter_params)
18
+ end
19
+
20
+ def filtered_parameters(params)
21
+ parameter_filter.filter(params)
22
+ end
23
+
24
+ def filtered_query_string(query_string)
25
+ query_string.gsub(PAIR_REGEXP) do |_|
26
+ parameter_filter.filter([[$1, $2]]).first.join("=")
27
+ end
28
+ end
29
+ end
30
+ end