fakeit 0.6.2 → 0.8.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.circleci/config.yml +1 -1
- data/.rubocop.yml +2 -49
- data/CHANGELOG.md +176 -0
- data/Gemfile.lock +45 -36
- data/README.md +29 -1
- data/bin/fakeit +4 -0
- data/docs/random.md +1 -1
- data/fakeit.gemspec +7 -4
- data/lib/fakeit.rb +3 -2
- data/lib/fakeit/app/app_builder.rb +23 -0
- data/lib/fakeit/app/helpers/body_parser.rb +32 -0
- data/lib/fakeit/app/helpers/response_builder.rb +19 -0
- data/lib/fakeit/app/options.rb +10 -0
- data/lib/fakeit/app/routes/config_route.rb +36 -0
- data/lib/fakeit/app/routes/openapi_route.rb +52 -0
- data/lib/fakeit/core_extensions/reference.rb +9 -0
- data/lib/fakeit/core_extensions/schema.rb +1 -3
- data/lib/fakeit/logger.rb +1 -2
- data/lib/fakeit/middleware/recorder.rb +2 -6
- data/lib/fakeit/openapi/example/array_example.rb +5 -15
- data/lib/fakeit/openapi/example/integer_example.rb +4 -12
- data/lib/fakeit/openapi/example/number_example.rb +6 -19
- data/lib/fakeit/openapi/example/string_example.rb +19 -23
- data/lib/fakeit/openapi/loader.rb +2 -1
- data/lib/fakeit/openapi/operation.rb +7 -21
- data/lib/fakeit/openapi/reference_error.rb +6 -0
- data/lib/fakeit/openapi/specification.rb +1 -1
- data/lib/fakeit/validation/validator.rb +4 -12
- data/lib/fakeit/version.rb +1 -1
- metadata +63 -16
- data/lib/fakeit/app/app.rb +0 -63
- data/lib/fakeit/app/body_parser.rb +0 -30
- data/lib/fakeit/core_extensions/path_item_finder.rb +0 -14
data/docs/random.md
CHANGED
@@ -11,7 +11,7 @@ The following Openapi properties are supported in random response generation
|
|
11
11
|
| |minItems|Default: `1`|
|
12
12
|
| |maxItems|Default: `minItems + 2`|
|
13
13
|
|string|enum| |
|
14
|
-
| |pattern| |
|
14
|
+
| |pattern|Random generated once and cached for performance concern|
|
15
15
|
| |format=uri| |
|
16
16
|
| |format=uuid| |
|
17
17
|
| |format=guid| |
|
data/fakeit.gemspec
CHANGED
@@ -22,20 +22,23 @@ Gem::Specification.new do |spec|
|
|
22
22
|
spec.executables = 'fakeit'
|
23
23
|
spec.require_paths = ['lib']
|
24
24
|
|
25
|
-
spec.required_ruby_version = '>=
|
25
|
+
spec.required_ruby_version = '>= 3.0.0'
|
26
26
|
|
27
27
|
spec.add_development_dependency 'bundler', '~> 2.0'
|
28
28
|
spec.add_development_dependency 'byebug', '~> 11.0'
|
29
29
|
spec.add_development_dependency 'rack-test', '~> 1.1'
|
30
|
-
spec.add_development_dependency 'rake', '~>
|
30
|
+
spec.add_development_dependency 'rake', '~> 13.0'
|
31
31
|
spec.add_development_dependency 'rspec', '~> 3.0'
|
32
|
-
spec.add_development_dependency 'rubocop', '~>
|
32
|
+
spec.add_development_dependency 'rubocop', '~> 1.10'
|
33
|
+
spec.add_development_dependency 'rubocop-rake', '~> 0.5'
|
33
34
|
spec.add_development_dependency 'simplecov', '~> 0.18'
|
34
35
|
|
35
36
|
spec.add_dependency 'faker', '2.13.0'
|
36
|
-
spec.add_dependency 'openapi_parser', '0.
|
37
|
+
spec.add_dependency 'openapi_parser', '0.12.1'
|
37
38
|
spec.add_dependency 'rack', '~> 2.0'
|
38
39
|
spec.add_dependency 'rack-cors', '~> 1.0'
|
39
40
|
spec.add_dependency 'rainbow', '~> 3.0'
|
41
|
+
spec.add_dependency 'regexp-examples', '1.5.1'
|
40
42
|
spec.add_dependency 'slop', '~> 4.8'
|
43
|
+
spec.add_dependency 'webrick', '~> 1.7'
|
41
44
|
end
|
data/lib/fakeit.rb
CHANGED
@@ -4,17 +4,18 @@ require 'open-uri'
|
|
4
4
|
require 'base64'
|
5
5
|
require 'openapi_parser'
|
6
6
|
require 'faker'
|
7
|
+
require 'regexp-examples'
|
7
8
|
require 'rack'
|
8
9
|
require 'logger'
|
9
10
|
require 'rainbow'
|
10
11
|
|
11
|
-
Dir.glob(File.join(File.dirname(__FILE__), 'fakeit', '**/*.rb')).
|
12
|
+
Dir.glob(File.join(File.dirname(__FILE__), 'fakeit', '**/*.rb')).each { require _1 }
|
12
13
|
|
13
14
|
module Fakeit
|
14
15
|
class << self
|
15
16
|
def build(spec_file, options)
|
16
17
|
Rack::Builder.new do
|
17
|
-
run App.
|
18
|
+
run App::AppBuilder.new(spec_file, options).build
|
18
19
|
end
|
19
20
|
end
|
20
21
|
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module Fakeit
|
2
|
+
module App
|
3
|
+
class AppBuilder
|
4
|
+
def initialize(spec_file, options)
|
5
|
+
@config_route = Routes::ConfigRoute.new(options)
|
6
|
+
@openapi_route = Routes::OpenapiRoute.new(spec_file)
|
7
|
+
end
|
8
|
+
|
9
|
+
def build
|
10
|
+
proc do |env|
|
11
|
+
request = Rack::Request.new(env)
|
12
|
+
|
13
|
+
case request.path_info
|
14
|
+
when '/__fakeit_config__'
|
15
|
+
@config_route.call(request)
|
16
|
+
else
|
17
|
+
@openapi_route.call(request, @config_route.options)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module Fakeit
|
2
|
+
module App
|
3
|
+
module Helpers
|
4
|
+
class BodyParser
|
5
|
+
class << self
|
6
|
+
def parse(request)
|
7
|
+
case request.media_type
|
8
|
+
when %r{^application/.*json}
|
9
|
+
{ media_type: request.media_type, data: parse_json(request.body.read) }
|
10
|
+
when 'multipart/form-data'
|
11
|
+
{ media_type: request.media_type, data: parse_form_data(request.params) }
|
12
|
+
else
|
13
|
+
{ media_type: request.media_type, data: request.body.read }
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def parse_json(body)
|
20
|
+
body.empty? ? {} : JSON.parse(body)
|
21
|
+
rescue StandardError
|
22
|
+
raise Fakeit::Validation::ValidationError, 'Invalid json payload'
|
23
|
+
end
|
24
|
+
|
25
|
+
def parse_form_data(params)
|
26
|
+
params.transform_values { |v| v.instance_of?(Hash) && v[:tempfile] ? v[:tempfile].read : v }
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Fakeit
|
2
|
+
module App
|
3
|
+
module Helpers
|
4
|
+
class ResponseBuilder
|
5
|
+
class << self
|
6
|
+
def error(code, err) = [code, { 'Content-Type' => 'application/json' }, [{ message: err.message }.to_json]]
|
7
|
+
|
8
|
+
def not_found = [404, {}, ['Not Found']]
|
9
|
+
|
10
|
+
def method_not_allowed = [405, {}, ['Method Not Allowed']]
|
11
|
+
|
12
|
+
def unsupported_media_type = [415, {}, ['Unsupported Media Type']]
|
13
|
+
|
14
|
+
def ok(body) = [200, { 'Content-Type' => 'application/json' }, [body.to_json]]
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
data/lib/fakeit/app/options.rb
CHANGED
@@ -14,6 +14,16 @@ module Fakeit
|
|
14
14
|
def use_static?(type: nil, property: nil)
|
15
15
|
@static || @static_types.include?(type) || @static_properties.include?(property)
|
16
16
|
end
|
17
|
+
|
18
|
+
def to_hash
|
19
|
+
{
|
20
|
+
permissive: @permissive,
|
21
|
+
use_example: @use_example,
|
22
|
+
static: @static,
|
23
|
+
static_types: @static_types,
|
24
|
+
static_properties: @static_properties
|
25
|
+
}
|
26
|
+
end
|
17
27
|
end
|
18
28
|
end
|
19
29
|
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module Fakeit
|
2
|
+
module App
|
3
|
+
module Routes
|
4
|
+
class ConfigRoute
|
5
|
+
attr_reader :options
|
6
|
+
|
7
|
+
def initialize(options) = @options = options
|
8
|
+
|
9
|
+
def call(request)
|
10
|
+
case [request.request_method, request.media_type]
|
11
|
+
in ['GET', _]
|
12
|
+
Fakeit::App::Helpers::ResponseBuilder.ok(@options.to_hash)
|
13
|
+
in ['PUT', 'application/json']
|
14
|
+
update(request)
|
15
|
+
in ['PUT', _]
|
16
|
+
Fakeit::App::Helpers::ResponseBuilder.unsupported_media_type
|
17
|
+
else
|
18
|
+
Fakeit::App::Helpers::ResponseBuilder.method_not_allowed
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def update(request)
|
25
|
+
body = Fakeit::App::Helpers::BodyParser.parse(request)[:data]
|
26
|
+
@options = Fakeit::App::Options.new(**body.transform_keys(&:to_sym))
|
27
|
+
|
28
|
+
Fakeit::App::Helpers::ResponseBuilder.ok(@options.to_hash)
|
29
|
+
rescue ArgumentError => e
|
30
|
+
Logger.warn(Rainbow(e.message).red)
|
31
|
+
Fakeit::App::Helpers::ResponseBuilder.error(422, e)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
module Fakeit
|
2
|
+
module App
|
3
|
+
module Routes
|
4
|
+
class OpenapiRoute
|
5
|
+
def initialize(spec_file) = @specification = Fakeit::Openapi::Specification.new(spec_file)
|
6
|
+
|
7
|
+
def call(request, options)
|
8
|
+
@specification
|
9
|
+
.operation(request.request_method.downcase.to_sym, request.path_info, options)
|
10
|
+
.then { _1 ? handle(_1, request, options) : Fakeit::App::Helpers::ResponseBuilder.not_found }
|
11
|
+
end
|
12
|
+
|
13
|
+
private
|
14
|
+
|
15
|
+
def handle(operation, request, options)
|
16
|
+
validate(operation, request)
|
17
|
+
response(operation)
|
18
|
+
rescue Fakeit::Validation::ValidationError => e
|
19
|
+
Logger.warn(Rainbow(e.message).red)
|
20
|
+
options.permissive ? response(operation) : Fakeit::App::Helpers::ResponseBuilder.error(418, e)
|
21
|
+
end
|
22
|
+
|
23
|
+
def response(operation) = [operation.status, operation.headers, [operation.body]]
|
24
|
+
|
25
|
+
def validate(operation, request)
|
26
|
+
operation.validate(
|
27
|
+
body: Helpers::BodyParser.parse(request),
|
28
|
+
params: parse_query(request.query_string),
|
29
|
+
headers: headers(request)
|
30
|
+
)
|
31
|
+
end
|
32
|
+
|
33
|
+
def headers(request)
|
34
|
+
request
|
35
|
+
.each_header
|
36
|
+
.select { |k, _| k.start_with? 'HTTP_' }
|
37
|
+
.map { |k, v| [k.sub(/^HTTP_/, '').split('_').map(&:capitalize).join('-'), v] }
|
38
|
+
.to_h
|
39
|
+
end
|
40
|
+
|
41
|
+
def parse_query(query_string)
|
42
|
+
rack_query = Rack::Utils.parse_nested_query(query_string)
|
43
|
+
cgi_query = CGI.parse(query_string)
|
44
|
+
|
45
|
+
rack_query.merge(cgi_query.slice(*rack_query.keys)) do |_, oldval, newval|
|
46
|
+
newval.is_a?(Array) && newval.size > 1 ? newval : oldval
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
data/lib/fakeit/logger.rb
CHANGED
@@ -1,9 +1,7 @@
|
|
1
1
|
module Fakeit
|
2
2
|
module Middleware
|
3
3
|
class Recorder
|
4
|
-
def initialize(app)
|
5
|
-
@app = app
|
6
|
-
end
|
4
|
+
def initialize(app) = @app = app
|
7
5
|
|
8
6
|
def call(env)
|
9
7
|
env
|
@@ -20,9 +18,7 @@ module Fakeit
|
|
20
18
|
&.tap { |body| body.rewind }
|
21
19
|
end
|
22
20
|
|
23
|
-
def log_response(response)
|
24
|
-
Logger.info("Response body: #{response[2].first}")
|
25
|
-
end
|
21
|
+
def log_response(response) = Logger.info("Response body: #{response[2].first}")
|
26
22
|
end
|
27
23
|
end
|
28
24
|
end
|
@@ -33,25 +33,15 @@ module Fakeit
|
|
33
33
|
end
|
34
34
|
end
|
35
35
|
|
36
|
-
def add_depth(example_options)
|
37
|
-
{ **example_options, depth: example_options[:depth] + 1 }
|
38
|
-
end
|
36
|
+
def add_depth(example_options) = { **example_options, depth: example_options[:depth] + 1 }
|
39
37
|
|
40
|
-
def need_retry?(item, result, retries)
|
41
|
-
uniqueItems && result.include?(item) && retries.positive?
|
42
|
-
end
|
38
|
+
def need_retry?(item, result, retries) = uniqueItems && result.include?(item) && retries.positive?
|
43
39
|
|
44
|
-
def non_empty_size
|
45
|
-
[min_array, 1].max
|
46
|
-
end
|
40
|
+
def non_empty_size = [min_array, 1].max
|
47
41
|
|
48
|
-
def min_array
|
49
|
-
minItems || 1
|
50
|
-
end
|
42
|
+
def min_array = minItems || 1
|
51
43
|
|
52
|
-
def max_array(depth)
|
53
|
-
maxItems || min_array + (depth > 1 ? 2 : 9)
|
54
|
-
end
|
44
|
+
def max_array(depth) = maxItems || min_array + (depth > 1 ? 2 : 9)
|
55
45
|
end
|
56
46
|
end
|
57
47
|
end
|
@@ -29,21 +29,13 @@ module Fakeit
|
|
29
29
|
end
|
30
30
|
end
|
31
31
|
|
32
|
-
def int_rand_begin
|
33
|
-
min_int / int_multiple + int_rand_begin_adjust
|
34
|
-
end
|
32
|
+
def int_rand_begin = min_int / int_multiple + int_rand_begin_adjust
|
35
33
|
|
36
|
-
def int_rand_end
|
37
|
-
max_int / int_multiple
|
38
|
-
end
|
34
|
+
def int_rand_end = max_int / int_multiple
|
39
35
|
|
40
|
-
def int_rand_begin_adjust
|
41
|
-
(min_int % int_multiple).zero? ? 0 : 1
|
42
|
-
end
|
36
|
+
def int_rand_begin_adjust = (min_int % int_multiple).zero? ? 0 : 1
|
43
37
|
|
44
|
-
def int_multiple
|
45
|
-
multipleOf || 1
|
46
|
-
end
|
38
|
+
def int_multiple = multipleOf || 1
|
47
39
|
|
48
40
|
def min_int
|
49
41
|
if minimum
|
@@ -14,35 +14,22 @@ module Fakeit
|
|
14
14
|
|
15
15
|
private
|
16
16
|
|
17
|
-
def static_number_example
|
18
|
-
(num_rand_end * num_multiple)
|
19
|
-
.then { multipleOf ? _1 : _1.round(2) }
|
20
|
-
end
|
17
|
+
def static_number_example = (num_rand_end * num_multiple).then { multipleOf ? _1 : _1.round(2) }
|
21
18
|
|
22
19
|
def random_number_example
|
23
20
|
(Faker::Number.between(from: num_rand_begin, to: num_rand_end) * num_multiple)
|
24
21
|
.then { multipleOf ? _1 : _1.round(2) }
|
25
22
|
end
|
26
23
|
|
27
|
-
def num_rand_begin
|
28
|
-
multipleOf ? (min_num / multipleOf).ceil : min_num
|
29
|
-
end
|
24
|
+
def num_rand_begin = multipleOf ? (min_num / multipleOf).ceil : min_num
|
30
25
|
|
31
|
-
def num_rand_end
|
32
|
-
multipleOf ? (max_num / multipleOf).floor : max_num
|
33
|
-
end
|
26
|
+
def num_rand_end = multipleOf ? (max_num / multipleOf).floor : max_num
|
34
27
|
|
35
|
-
def num_multiple
|
36
|
-
multipleOf || 1
|
37
|
-
end
|
28
|
+
def num_multiple = multipleOf || 1
|
38
29
|
|
39
|
-
def min_num
|
40
|
-
(minimum || MIN_NUM).to_f.ceil(2)
|
41
|
-
end
|
30
|
+
def min_num = (minimum || MIN_NUM).to_f.ceil(2)
|
42
31
|
|
43
|
-
def max_num
|
44
|
-
(maximum || MAX_NUM).to_f.floor(2)
|
45
|
-
end
|
32
|
+
def max_num = (maximum || MAX_NUM).to_f.floor(2)
|
46
33
|
end
|
47
34
|
end
|
48
35
|
end
|
@@ -27,6 +27,8 @@ module Fakeit
|
|
27
27
|
}.freeze
|
28
28
|
|
29
29
|
def string_example(example_options)
|
30
|
+
@string_pattern ||= Regexp.new(pattern) if pattern
|
31
|
+
|
30
32
|
if example_options[:use_static][type: 'string', property: example_options[:property]]
|
31
33
|
static_string_example
|
32
34
|
else
|
@@ -39,7 +41,7 @@ module Fakeit
|
|
39
41
|
def static_string_example
|
40
42
|
fixed_faker do
|
41
43
|
if enum then enum.to_a.first
|
42
|
-
elsif pattern then
|
44
|
+
elsif pattern then static_string_pattern
|
43
45
|
elsif format then static_string_format
|
44
46
|
elsif length_constraint then static_string_with_length
|
45
47
|
else 'string'
|
@@ -56,43 +58,37 @@ module Fakeit
|
|
56
58
|
|
57
59
|
def random_string_example
|
58
60
|
if enum then enum.to_a.sample
|
59
|
-
elsif pattern then
|
61
|
+
elsif pattern then random_string_pattern
|
60
62
|
elsif format then random_string_format
|
61
63
|
elsif length_constraint then string_with_length
|
62
64
|
else Faker::Book.title
|
63
65
|
end
|
64
66
|
end
|
65
67
|
|
66
|
-
def static_string_with_length
|
67
|
-
'1' * max_string_length
|
68
|
-
end
|
68
|
+
def static_string_with_length = '1' * max_string_length
|
69
69
|
|
70
|
-
def static_string_format
|
71
|
-
(STATIC_FORMAT_HANDLERS[format] || method(:unknown_format))[]
|
72
|
-
end
|
70
|
+
def static_string_format = (STATIC_FORMAT_HANDLERS[format] || method(:unknown_format))[]
|
73
71
|
|
74
|
-
def
|
75
|
-
|
72
|
+
def static_string_pattern
|
73
|
+
@static_string_pattern ||= @string_pattern.examples(
|
74
|
+
max_repeater_variance: 1, max_group_results: 1, max_results_limit: 1
|
75
|
+
).first
|
76
76
|
end
|
77
77
|
|
78
|
-
def
|
79
|
-
Faker::Internet.username(specifier: min_string_length..max_string_length)
|
80
|
-
end
|
78
|
+
def length_constraint = minLength || maxLength
|
81
79
|
|
82
|
-
def min_string_length
|
83
|
-
minLength || 0
|
84
|
-
end
|
80
|
+
def string_with_length = Faker::Internet.username(specifier: min_string_length..max_string_length)
|
85
81
|
|
86
|
-
def
|
87
|
-
maxLength || min_string_length + 10
|
88
|
-
end
|
82
|
+
def min_string_length = minLength || 0
|
89
83
|
|
90
|
-
def
|
91
|
-
|
92
|
-
|
84
|
+
def max_string_length = maxLength || min_string_length + 10
|
85
|
+
|
86
|
+
def random_string_format = (RANDOM_FORMAT_HANDLERS[format] || method(:unknown_format))[]
|
87
|
+
|
88
|
+
def random_string_pattern = @random_string_pattern ||= @string_pattern.random_example(max_repeater_variance: 1)
|
93
89
|
|
94
90
|
def unknown_format
|
95
|
-
|
91
|
+
Logger.info("Unknown string format: #{format}")
|
96
92
|
'Unknown string format'
|
97
93
|
end
|
98
94
|
end
|