gemfather-stable 2.2.2
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.
- checksums.yaml +7 -0
- data/README.md +145 -0
- data/bin/gemfather +6 -0
- data/gem_template/%app_name%.gemspec.tt +32 -0
- data/gem_template/.gitignore.tt +26 -0
- data/gem_template/.rubocop-rspec.yml.tt +38 -0
- data/gem_template/.rubocop-ruby.yml.tt +54 -0
- data/gem_template/.rubocop.yml.tt +11 -0
- data/gem_template/Gemfile.tt +17 -0
- data/gem_template/Makefile.tt +21 -0
- data/gem_template/README.md.tt +180 -0
- data/gem_template/Rakefile.tt +6 -0
- data/gem_template/bin/console.tt +21 -0
- data/gem_template/bin/generate.tt +6 -0
- data/gem_template/bin/rubocop.tt +29 -0
- data/gem_template/lib/%app_name%.rb.tt +25 -0
- data/gem_template/lib/%app_short_name%/client.rb.tt +18 -0
- data/gem_template/lib/%app_short_name%/http_errors.rb.tt +20 -0
- data/gem_template/lib/%app_short_name%/railtie.rb.tt +5 -0
- data/gem_template/lib/%app_short_name%/version.rb.tt +5 -0
- data/gem_template/log/.keep.tt +0 -0
- data/gem_template/spec/spec_helper.rb.tt +35 -0
- data/lib/api_generator/client/config.rb +57 -0
- data/lib/api_generator/client/connection.rb +149 -0
- data/lib/api_generator/commands/generate.rb +56 -0
- data/lib/api_generator/helpers/utils.rb +56 -0
- data/lib/api_generator/middleware/error_code_middleware.rb +43 -0
- data/lib/api_generator/middleware/error_handler_middleware.rb +26 -0
- data/lib/api_generator/middleware/handle_unsuccessful_request_middleware.rb +36 -0
- data/lib/api_generator/middleware/http_errors.rb +21 -0
- data/lib/api_generator/middleware/raise_error_base.rb +11 -0
- data/lib/api_generator/middleware/raise_error_dsl.rb +36 -0
- data/lib/api_generator/middleware.rb +4 -0
- data/lib/api_generator/models/base.rb +15 -0
- data/lib/api_generator/models/data.rb +16 -0
- data/lib/api_generator/pagination/dsl.rb +32 -0
- data/lib/api_generator/pagination/limit_offset_relation.rb +24 -0
- data/lib/api_generator/pagination/page_relation.rb +21 -0
- data/lib/api_generator/pagination/relation.rb +114 -0
- data/lib/api_generator/pagination.rb +2 -0
- data/lib/api_generator/railtie.rb +24 -0
- data/lib/api_generator/services/base_create.rb +56 -0
- data/lib/api_generator/services/create_api.rb +87 -0
- data/lib/api_generator/services/create_model.rb +69 -0
- data/lib/api_generator/services/create_scaffold.rb +53 -0
- data/lib/api_generator/services/gemfather.rb +81 -0
- data/lib/api_generator/version.rb +3 -0
- data/lib/api_generator.rb +18 -0
- data/templates/api/action.erb +10 -0
- data/templates/api/action_paginate.erb +10 -0
- data/templates/api/api_module.erb +11 -0
- data/templates/api/include_helpers.erb +9 -0
- data/templates/api/include_namespace.erb +1 -0
- data/templates/api/path.erb +1 -0
- data/templates/models/request.erb +11 -0
- data/templates/models/response.erb +11 -0
- data/templates/specs/api_spec.erb +15 -0
- data/templates/specs/request_spec.erb +12 -0
- data/templates/specs/response_spec.erb +12 -0
- metadata +229 -0
File without changes
|
@@ -0,0 +1,35 @@
|
|
1
|
+
require 'bundler/setup'
|
2
|
+
require '<%= app_name %>'
|
3
|
+
require 'dotenv/load'
|
4
|
+
require 'vcr'
|
5
|
+
|
6
|
+
VCR.configure do |config|
|
7
|
+
config.cassette_library_dir = 'spec/fixtures/vcr_cassettes'
|
8
|
+
|
9
|
+
config.hook_into :faraday
|
10
|
+
|
11
|
+
config.before_record do |vcr|
|
12
|
+
vcr.response.body.force_encoding('UTF-8')
|
13
|
+
end
|
14
|
+
|
15
|
+
config.configure_rspec_metadata!
|
16
|
+
end
|
17
|
+
|
18
|
+
RSpec.configure do |config|
|
19
|
+
config.expect_with :rspec do |c|
|
20
|
+
c.syntax = :expect
|
21
|
+
end
|
22
|
+
|
23
|
+
config.order = :random
|
24
|
+
|
25
|
+
Kernel.srand config.seed
|
26
|
+
|
27
|
+
config.before do
|
28
|
+
<%= app_name_class %>::Client.configure do |configuration|
|
29
|
+
configuration.api_endpoint = 'http://<%= app_name %>.dev'
|
30
|
+
configuration.api_header = 'X-Source-Token'
|
31
|
+
configuration.api_token = 'TEST_TOKEN'
|
32
|
+
configuration.logger = Logger.new(Pathname.new(__dir__).join('..', 'log', 'test.log'))
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
module ApiGenerator
|
2
|
+
module Client
|
3
|
+
module Config
|
4
|
+
class ConfigurationError < StandardError; end
|
5
|
+
|
6
|
+
USER_AGENT = 'Ruby API Client'.freeze
|
7
|
+
OPEN_TIMEOUT = 3
|
8
|
+
READ_TIMEOUT = 3
|
9
|
+
MAX_RETRIES = 3
|
10
|
+
RETRY_INTERVAL = 1
|
11
|
+
RETRY_BACKOFF_FACTOR = 1
|
12
|
+
CONNECTION_ERROR = ApiGenerator::Middleware::HttpErrors::ConnectionError
|
13
|
+
|
14
|
+
# rubocop:disable Metrics/MethodLength
|
15
|
+
def self.extended(klass)
|
16
|
+
klass.extend Dry::Configurable
|
17
|
+
|
18
|
+
klass.class_eval do
|
19
|
+
setting :api_endpoint, reader: true
|
20
|
+
setting :user_agent, default: USER_AGENT, reader: true
|
21
|
+
|
22
|
+
setting :api_header, reader: true
|
23
|
+
setting :api_token, reader: true
|
24
|
+
|
25
|
+
setting :api_user, reader: true
|
26
|
+
setting :api_password, reader: true
|
27
|
+
|
28
|
+
setting :ssl_verify, reader: true
|
29
|
+
setting :ca_file, reader: true
|
30
|
+
|
31
|
+
setting :open_timeout, default: OPEN_TIMEOUT, reader: true
|
32
|
+
setting :read_timeout, default: READ_TIMEOUT, reader: true
|
33
|
+
setting :max_retries, default: MAX_RETRIES, reader: true
|
34
|
+
setting :retry_interval, default: RETRY_INTERVAL, reader: true
|
35
|
+
setting :retry_backoff_factor, default: RETRY_BACKOFF_FACTOR, reader: true
|
36
|
+
setting :logger, reader: true
|
37
|
+
|
38
|
+
setting :retriable_errors, default: [CONNECTION_ERROR], reader: true
|
39
|
+
end
|
40
|
+
end
|
41
|
+
# rubocop:enable Metrics/MethodLength
|
42
|
+
|
43
|
+
def check_config
|
44
|
+
raise ConfigurationError, 'API endpoint is not configured.' if config.api_endpoint.nil?
|
45
|
+
end
|
46
|
+
|
47
|
+
def inherit_config!(other_config)
|
48
|
+
config.to_h.each_key { |setting| config[setting] = other_config[setting] }
|
49
|
+
config
|
50
|
+
end
|
51
|
+
|
52
|
+
def same_config?(other_config)
|
53
|
+
config.to_h == other_config.to_h
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,149 @@
|
|
1
|
+
module ApiGenerator
|
2
|
+
module Client
|
3
|
+
# rubocop:disable Metrics/ClassLength
|
4
|
+
class Connection < SimpleDelegator
|
5
|
+
extend Config
|
6
|
+
|
7
|
+
class << self
|
8
|
+
def inherited(subclass)
|
9
|
+
subclass.extend Middleware::RaiseErrorDsl
|
10
|
+
|
11
|
+
error_namespace = begin
|
12
|
+
subclass.module_parent::HttpErrors
|
13
|
+
rescue NameError
|
14
|
+
ApiGenerator::Middleware::HttpErrors
|
15
|
+
end
|
16
|
+
|
17
|
+
subclass.setup_middleware(error_namespace)
|
18
|
+
|
19
|
+
super
|
20
|
+
end
|
21
|
+
|
22
|
+
def setup_middleware(error_namespace)
|
23
|
+
unsuccessful_request_middleware = Class.new(
|
24
|
+
Middleware::HandleUnsuccessfulRequestMiddleware,
|
25
|
+
)
|
26
|
+
unsuccessful_request_middleware.error_namespace = error_namespace if error_namespace
|
27
|
+
const_set(:HandleUnsuccessfulRequestMiddleware, unsuccessful_request_middleware)
|
28
|
+
|
29
|
+
error_handler_middleware = Class.new(Middleware::ErrorHandlerMiddleware)
|
30
|
+
error_handler_middleware.error_namespace = error_namespace if error_namespace
|
31
|
+
const_set(:ErrorHandlerMiddleware, error_handler_middleware)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
ACCEPT_HEADER = 'application/json'.freeze
|
36
|
+
|
37
|
+
attr_accessor :customization_block
|
38
|
+
|
39
|
+
def initialize(**settings, &blk)
|
40
|
+
if instance_of?(Connection)
|
41
|
+
raise LoadError, 'This class is abstract and is not intended to be initialized directly'
|
42
|
+
end
|
43
|
+
|
44
|
+
self.customization_block = blk if blk
|
45
|
+
|
46
|
+
config = self.class.inherit_config!(self.class.config)
|
47
|
+
settings.each { |setting, value| config[setting] = value }
|
48
|
+
self.class.check_config
|
49
|
+
|
50
|
+
super(connection)
|
51
|
+
end
|
52
|
+
|
53
|
+
private
|
54
|
+
|
55
|
+
# rubocop:disable Metrics/MethodLength
|
56
|
+
def connection
|
57
|
+
@connection ||= Faraday.new(url: self.class.api_endpoint) do |connection|
|
58
|
+
setup_auth!(connection)
|
59
|
+
|
60
|
+
setup_failed_req_handling!(connection)
|
61
|
+
setup_timeouts!(connection)
|
62
|
+
setup_user_agent!(connection)
|
63
|
+
setup_logger!(connection)
|
64
|
+
setup_retries!(connection)
|
65
|
+
setup_error_handling!(connection)
|
66
|
+
setup_error_code_middleware!(connection)
|
67
|
+
setup_ssl(connection)
|
68
|
+
|
69
|
+
customize_connection!(connection)
|
70
|
+
|
71
|
+
# When set last - req/res parsed as json in middleware
|
72
|
+
setup_json!(connection)
|
73
|
+
|
74
|
+
connection.adapter(Faraday.default_adapter)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
# rubocop:enable Metrics/MethodLength
|
78
|
+
|
79
|
+
# rubocop:disable Metrics/AbcSize
|
80
|
+
def setup_auth!(connection)
|
81
|
+
if self.class.api_token && self.class.api_header
|
82
|
+
connection.headers[self.class.api_header] = self.class.api_token
|
83
|
+
elsif self.class.api_user && self.class.api_password
|
84
|
+
connection.use(
|
85
|
+
Faraday::Request::BasicAuthentication,
|
86
|
+
self.class.api_user,
|
87
|
+
self.class.api_password,
|
88
|
+
)
|
89
|
+
end
|
90
|
+
end
|
91
|
+
# rubocop:enable Metrics/AbcSize
|
92
|
+
|
93
|
+
def setup_json!(connection)
|
94
|
+
connection.headers[:accept] = ACCEPT_HEADER
|
95
|
+
connection.request(:json)
|
96
|
+
connection.response(:json)
|
97
|
+
end
|
98
|
+
|
99
|
+
def setup_timeouts!(connection)
|
100
|
+
connection.options.open_timeout = self.class.open_timeout
|
101
|
+
connection.options.timeout = self.class.read_timeout
|
102
|
+
end
|
103
|
+
|
104
|
+
def setup_user_agent!(connection)
|
105
|
+
connection.headers[:user_agent] = self.class.user_agent
|
106
|
+
end
|
107
|
+
|
108
|
+
def setup_logger!(connection)
|
109
|
+
connection.response(:logger, self.class.logger) if self.class.logger
|
110
|
+
end
|
111
|
+
|
112
|
+
def setup_ssl(connection)
|
113
|
+
connection.ssl.verify = self.class.ssl_verify
|
114
|
+
connection.ssl.ca_file = self.class.ca_file
|
115
|
+
end
|
116
|
+
|
117
|
+
def setup_retries!(connection)
|
118
|
+
return if self.class.max_retries <= 0
|
119
|
+
|
120
|
+
connection.request(
|
121
|
+
:retry,
|
122
|
+
max: self.class.max_retries,
|
123
|
+
interval: self.class.retry_interval,
|
124
|
+
backoff_factor: self.class.retry_backoff_factor,
|
125
|
+
exceptions: self.class.retriable_errors,
|
126
|
+
)
|
127
|
+
end
|
128
|
+
|
129
|
+
def setup_failed_req_handling!(connection)
|
130
|
+
connection.use(self.class::HandleUnsuccessfulRequestMiddleware)
|
131
|
+
end
|
132
|
+
|
133
|
+
def setup_error_handling!(connection)
|
134
|
+
connection.use(self.class::ErrorHandlerMiddleware)
|
135
|
+
end
|
136
|
+
|
137
|
+
def setup_error_code_middleware!(connection)
|
138
|
+
connection.use(self.class::ErrorCodeMiddleware)
|
139
|
+
end
|
140
|
+
|
141
|
+
def customize_connection!(connection)
|
142
|
+
return unless customization_block
|
143
|
+
|
144
|
+
customization_block.call(connection)
|
145
|
+
end
|
146
|
+
end
|
147
|
+
# rubocop:enable Metrics/ClassLength
|
148
|
+
end
|
149
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
module ApiGenerator
|
2
|
+
module Commands
|
3
|
+
class Generate
|
4
|
+
# rubocop:disable Layout/LineLength
|
5
|
+
ARGS_REGEX = /(?<namespace>[\w\-_]+):(?<action>[\w\-_]+):(?<http_method>get|post|patch|put)/i.freeze
|
6
|
+
# rubocop:enable Layout/LineLength
|
7
|
+
|
8
|
+
def initialize(argv)
|
9
|
+
@argv = argv[0..1]
|
10
|
+
end
|
11
|
+
|
12
|
+
def call
|
13
|
+
service_args = [*parse_parameters(@argv), paginate(@argv)]
|
14
|
+
|
15
|
+
shutdown unless service_args[0..2].all?(&:present?)
|
16
|
+
|
17
|
+
Services::CreateScaffold.new(*service_args).call
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def usage
|
23
|
+
<<~USAGE
|
24
|
+
Generate API scaffold (api_namespace, models, specs)
|
25
|
+
Usage: bin/generate NAMESPACE:ACTION:HTTP_METHOD [--paginate]
|
26
|
+
Examples:
|
27
|
+
bin/generate deals:search:post --paginate
|
28
|
+
bin/generate persons:create:post
|
29
|
+
bin/generate deals:update:put
|
30
|
+
USAGE
|
31
|
+
end
|
32
|
+
|
33
|
+
def shutdown
|
34
|
+
puts(usage) || exit
|
35
|
+
end
|
36
|
+
|
37
|
+
def parse_parameters(args)
|
38
|
+
params = args.find { |arg| arg =~ ARGS_REGEX }
|
39
|
+
|
40
|
+
shutdown unless params
|
41
|
+
|
42
|
+
match_data = params.match(ARGS_REGEX)
|
43
|
+
|
44
|
+
[
|
45
|
+
match_data['namespace'],
|
46
|
+
match_data['action'],
|
47
|
+
match_data['http_method'],
|
48
|
+
]
|
49
|
+
end
|
50
|
+
|
51
|
+
def paginate(args)
|
52
|
+
args.find { |arg| arg == '--paginate' }
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
module ApiGenerator
|
2
|
+
module Helpers
|
3
|
+
module Utils
|
4
|
+
INDENTATION_REGEX = /^([ \t]*)(?=\S)/.freeze
|
5
|
+
WRITE_MODE = 'w'.freeze
|
6
|
+
|
7
|
+
def create_dir(dir)
|
8
|
+
FileUtils.mkdir_p(dir) && log(dir)
|
9
|
+
end
|
10
|
+
|
11
|
+
def log(path)
|
12
|
+
path.gsub!(root, '.') if path.match?(Regexp.new(root))
|
13
|
+
shell.say_status('create', path, :green)
|
14
|
+
end
|
15
|
+
|
16
|
+
def shell
|
17
|
+
Thor::Shell::Color.new
|
18
|
+
end
|
19
|
+
|
20
|
+
def copy_template(template, destination, data_binding)
|
21
|
+
content = render(template, data_binding)
|
22
|
+
destination.gsub!(/\.erb$/, '.rb')
|
23
|
+
|
24
|
+
File.write(destination, content, mode: WRITE_MODE) && log(destination)
|
25
|
+
end
|
26
|
+
|
27
|
+
def render(path, data_binding)
|
28
|
+
ERB.new(File.read(path)).result(data_binding)
|
29
|
+
end
|
30
|
+
|
31
|
+
def inject_after_str(file, str, insert)
|
32
|
+
text = File.read(file)
|
33
|
+
result = inject_after(text, str, insert)
|
34
|
+
File.write(file, result, mode: WRITE_MODE)
|
35
|
+
end
|
36
|
+
|
37
|
+
def inject_after(text, str, insert)
|
38
|
+
lines = text.each_line.to_a
|
39
|
+
regex = Regexp.new(str)
|
40
|
+
match_line_idx = lines.index { |line| line =~ regex }
|
41
|
+
|
42
|
+
return unless match_line_idx
|
43
|
+
|
44
|
+
line = lines[match_line_idx]
|
45
|
+
indented_insert = insert.each_line.map { |insert_line| indent_as(line, insert_line) }
|
46
|
+
lines.insert(match_line_idx + 1, *indented_insert)
|
47
|
+
lines.map(&:chomp).join("\n")
|
48
|
+
end
|
49
|
+
|
50
|
+
def indent_as(source, target)
|
51
|
+
indentation = INDENTATION_REGEX.match(source).captures.first.to_s
|
52
|
+
indentation + target
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
module ApiGenerator
|
2
|
+
module Middleware
|
3
|
+
class ErrorCodeMiddleware < Faraday::Response::Middleware
|
4
|
+
def self.inherited(subclass)
|
5
|
+
class << subclass
|
6
|
+
attr_accessor :error_mapping, :error_namespace
|
7
|
+
|
8
|
+
def add_error(code, exception)
|
9
|
+
error_mapping[code] = exception
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
subclass.error_mapping ||= {}
|
14
|
+
subclass.error_namespace ||= ApiGenerator::Middleware::HttpErrors
|
15
|
+
|
16
|
+
super
|
17
|
+
end
|
18
|
+
|
19
|
+
def on_complete(env)
|
20
|
+
error_class = error_class(env.status)
|
21
|
+
return unless error_class
|
22
|
+
|
23
|
+
raise(error_class.new(message(env), env.body))
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def message(env)
|
29
|
+
"Server returned #{env.status}: #{env.body}. Headers: #{env.response_headers.inspect}"
|
30
|
+
end
|
31
|
+
|
32
|
+
def error_class(code)
|
33
|
+
self.class.error_mapping.fetch(code) do
|
34
|
+
if code >= 500
|
35
|
+
self.class.error_namespace::ServerError
|
36
|
+
elsif code >= 400
|
37
|
+
self.class.error_namespace::ClientError
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module ApiGenerator
|
2
|
+
module Middleware
|
3
|
+
class ErrorHandlerMiddleware < Faraday::Middleware
|
4
|
+
def self.inherited(subclass)
|
5
|
+
class << subclass
|
6
|
+
attr_accessor :error_namespace
|
7
|
+
end
|
8
|
+
|
9
|
+
subclass.error_namespace ||= ApiGenerator::Middleware::HttpErrors
|
10
|
+
super
|
11
|
+
end
|
12
|
+
|
13
|
+
FARADAY_CONNECTION_ERRORS = [
|
14
|
+
Faraday::TimeoutError,
|
15
|
+
Faraday::ConnectionFailed,
|
16
|
+
Faraday::SSLError,
|
17
|
+
].freeze
|
18
|
+
|
19
|
+
def call(env)
|
20
|
+
@app.call(env)
|
21
|
+
rescue *FARADAY_CONNECTION_ERRORS => e
|
22
|
+
raise self.class.error_namespace::ConnectionError, e
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module ApiGenerator
|
2
|
+
module Middleware
|
3
|
+
class HandleUnsuccessfulRequestMiddleware < Faraday::Response::Middleware
|
4
|
+
def self.inherited(subclass)
|
5
|
+
class << subclass
|
6
|
+
attr_accessor :error_namespace
|
7
|
+
end
|
8
|
+
|
9
|
+
subclass.error_namespace ||= ApiGenerator::Middleware::HttpErrors
|
10
|
+
super
|
11
|
+
end
|
12
|
+
|
13
|
+
def on_complete(env)
|
14
|
+
response_body = env.body
|
15
|
+
|
16
|
+
return unless response_body.is_a?(Hash) && response_body['success'] == false
|
17
|
+
|
18
|
+
raise(
|
19
|
+
self.class.error_namespace::UnsuccessfulRequestError.new(
|
20
|
+
message(env), env.body,
|
21
|
+
),
|
22
|
+
)
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def message(env)
|
28
|
+
<<~MSG
|
29
|
+
Server is unable to process the request: #{env.request_body}.
|
30
|
+
Headers: #{env.request_headers.inspect}
|
31
|
+
Server returned: #{env.body}. Headers: #{env.response_headers.inspect}
|
32
|
+
MSG
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module ApiGenerator
|
2
|
+
module Middleware
|
3
|
+
module HttpErrors
|
4
|
+
class ConnectionError < StandardError; end
|
5
|
+
|
6
|
+
class BaseError < StandardError
|
7
|
+
attr_reader :body
|
8
|
+
|
9
|
+
def initialize(msg = nil, body = nil)
|
10
|
+
@body = body if body
|
11
|
+
super(msg)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
class ServerError < BaseError; end
|
16
|
+
class ClientError < BaseError; end
|
17
|
+
|
18
|
+
class UnsuccessfulRequestError < ClientError; end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module ApiGenerator
|
2
|
+
module Middleware
|
3
|
+
module RaiseErrorDsl
|
4
|
+
def self.extended(klass)
|
5
|
+
klass.singleton_class.class_eval { attr_accessor :error_namespace }
|
6
|
+
klass.error_namespace ||= if klass.module_parent.const_defined?(:HttpErrors)
|
7
|
+
klass.module_parent::HttpErrors
|
8
|
+
else
|
9
|
+
ApiGenerator::Middleware::HttpErrors
|
10
|
+
end
|
11
|
+
error_code_middleware = Class.new(ErrorCodeMiddleware)
|
12
|
+
error_code_middleware.error_namespace = klass.error_namespace
|
13
|
+
klass.const_set(:ErrorCodeMiddleware, error_code_middleware)
|
14
|
+
|
15
|
+
super
|
16
|
+
end
|
17
|
+
|
18
|
+
def on_status(code, error:)
|
19
|
+
self::ErrorCodeMiddleware.add_error(code, error_const(code, error))
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def error_const(code, error_name)
|
25
|
+
const_name = error_name.to_s.camelize
|
26
|
+
|
27
|
+
return error_namespace.const_get(const_name) if error_namespace.const_defined?(const_name)
|
28
|
+
|
29
|
+
error_namespace.const_set(
|
30
|
+
const_name,
|
31
|
+
Class.new(code >= 500 ? error_namespace::ServerError : error_namespace::ClientError),
|
32
|
+
)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module ApiGenerator
|
2
|
+
module Models
|
3
|
+
class Base < Hashie::Trash
|
4
|
+
include Hashie::Extensions::Dash::Coercion
|
5
|
+
include Hashie::Extensions::Dash::IndifferentAccess
|
6
|
+
include Hashie::Extensions::IgnoreUndeclared
|
7
|
+
|
8
|
+
def self.parse_time
|
9
|
+
->(time) { Time.parse(time) if time.present? }
|
10
|
+
end
|
11
|
+
|
12
|
+
private_class_method :parse_time
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module ApiGenerator
|
2
|
+
module Models
|
3
|
+
class Data < Base
|
4
|
+
property :namespace
|
5
|
+
property :action
|
6
|
+
property :http_method
|
7
|
+
property :paginate
|
8
|
+
property :app_name
|
9
|
+
property :app_short_name
|
10
|
+
|
11
|
+
def for_template
|
12
|
+
binding
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module ApiGenerator::Pagination
|
2
|
+
module Dsl
|
3
|
+
STRATEGIES = {
|
4
|
+
limit_offset: LimitOffsetRelation,
|
5
|
+
page: PageRelation,
|
6
|
+
}.freeze
|
7
|
+
|
8
|
+
# rubocop:disable Metrics/MethodLength
|
9
|
+
# @param [Array<Symbol] methods список методов для применения пагинации
|
10
|
+
# @param [Symbol, Class] with стратегия пагинации (например, :limit_offset, :page)
|
11
|
+
# @param data_key ключ, по которому можно получить массив данных в ответе
|
12
|
+
# @param [Proc] on_request
|
13
|
+
# коллбэк, вызываемый при запросе и принимающий как аргументы запрос и параметры
|
14
|
+
# @param [Hash] config дополнительная конфигурация для стратегии пагинации
|
15
|
+
def paginate(*methods, with:, data_key:, on_request:, **config)
|
16
|
+
relation_class = STRATEGIES.fetch(with, with)
|
17
|
+
|
18
|
+
prepend(
|
19
|
+
Module.new do
|
20
|
+
methods.each do |method|
|
21
|
+
define_method(method) do |*args|
|
22
|
+
relation_class.new(config.merge(data_key: data_key)) do |relation|
|
23
|
+
super(*args) { |req| on_request.call(req, **relation.request_options) }
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end,
|
28
|
+
)
|
29
|
+
end
|
30
|
+
# rubocop:enable Metrics/MethodLength
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
class ApiGenerator::Pagination::LimitOffsetRelation < ApiGenerator::Pagination::Relation
|
2
|
+
def limit(limit)
|
3
|
+
expand(limit: limit)
|
4
|
+
end
|
5
|
+
|
6
|
+
def offset(offset)
|
7
|
+
expand(offset: offset)
|
8
|
+
end
|
9
|
+
|
10
|
+
private
|
11
|
+
|
12
|
+
def next_relation
|
13
|
+
limit = config[:limit]&.call(response) || size
|
14
|
+
offset = config[:offset]&.call(response) || options[:offset].to_i
|
15
|
+
|
16
|
+
expand(offset: offset + limit)
|
17
|
+
end
|
18
|
+
|
19
|
+
def last_page?
|
20
|
+
return size < config[:limit].call(response) if config[:limit]
|
21
|
+
|
22
|
+
data.empty?
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
class ApiGenerator::Pagination::PageRelation < ApiGenerator::Pagination::Relation
|
2
|
+
def page(page)
|
3
|
+
expand(page: page)
|
4
|
+
end
|
5
|
+
|
6
|
+
def per(size)
|
7
|
+
expand(per_page: size)
|
8
|
+
end
|
9
|
+
|
10
|
+
private
|
11
|
+
|
12
|
+
def next_relation
|
13
|
+
expand(page: [options[:page].to_i, 1].max + 1)
|
14
|
+
end
|
15
|
+
|
16
|
+
def last_page?
|
17
|
+
return data.size < config[:per_page].call(response) if config[:per_page]
|
18
|
+
|
19
|
+
data.empty?
|
20
|
+
end
|
21
|
+
end
|