randrizer 0.0.1
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/.gitignore +12 -0
- data/.rubocop.yml +107 -0
- data/.ruby-version +1 -0
- data/Gemfile +5 -0
- data/LICENCE.txt +21 -0
- data/README.md +82 -0
- data/exe/randrizer-json-schema +9 -0
- data/lib/randrizer/cli.rb +26 -0
- data/lib/randrizer/drivers/json_schema/driver.rb +71 -0
- data/lib/randrizer/drivers/json_schema/typegen.rb +123 -0
- data/lib/randrizer/drivers/json_schema.rb +3 -0
- data/lib/randrizer/generator.rb +12 -0
- data/lib/randrizer/types/bool.rb +26 -0
- data/lib/randrizer/types/const.rb +21 -0
- data/lib/randrizer/types/dict.rb +33 -0
- data/lib/randrizer/types/float.rb +31 -0
- data/lib/randrizer/types/int.rb +31 -0
- data/lib/randrizer/types/list.rb +28 -0
- data/lib/randrizer/types/nullable.rb +27 -0
- data/lib/randrizer/types/one_of.rb +29 -0
- data/lib/randrizer/types/optional.rb +30 -0
- data/lib/randrizer/types/skip.rb +17 -0
- data/lib/randrizer/types/string.rb +45 -0
- data/lib/randrizer/types/string_sequence.rb +38 -0
- data/lib/randrizer/types/type_builder.rb +25 -0
- data/lib/randrizer/types.rb +18 -0
- data/lib/randrizer/version.rb +5 -0
- data/lib/randrizer.rb +5 -0
- data/randrizer.gemspec +33 -0
- metadata +157 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: be06076a3c75b00ab51471e44ef3f7b6794423f9c5dbe4f9ff9b01efb4493fb6
|
4
|
+
data.tar.gz: cea424a0ca6abd167ec418afbb7952c346304252f1da937f13d5eb85373ade6c
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 656b7243cc9ac2be824c6060a27ed88940316f57a5acd93140b3a99043283443130bc34b774c9ed0e43867beb6b54cf941dbebf46b1509b114dad37b4d30be41
|
7
|
+
data.tar.gz: c5ca2206521f575429fbf281d45072f3025ba3ec5c367d971c55663209db339b7f7f0d8b173f3db1a0ceac4b6b3c6058bf162457523761138227d2ba1030b7ba
|
data/.gitignore
ADDED
data/.rubocop.yml
ADDED
@@ -0,0 +1,107 @@
|
|
1
|
+
require: rubocop-rspec
|
2
|
+
|
3
|
+
AllCops:
|
4
|
+
TargetRubyVersion: 2.6.5
|
5
|
+
DisplayCopNames: true
|
6
|
+
DisplayStyleGuide: true
|
7
|
+
ExtraDetails: true
|
8
|
+
Exclude:
|
9
|
+
- .*/**/*
|
10
|
+
- vendor/**/*
|
11
|
+
- bin/**/*
|
12
|
+
|
13
|
+
Lint/UnneededCopDisableDirective:
|
14
|
+
Severity: error
|
15
|
+
|
16
|
+
Lint/AmbiguousBlockAssociation:
|
17
|
+
Exclude:
|
18
|
+
- "spec/**/*"
|
19
|
+
|
20
|
+
Metrics/AbcSize:
|
21
|
+
Max: 59
|
22
|
+
|
23
|
+
Metrics/CyclomaticComplexity:
|
24
|
+
Max: 10
|
25
|
+
|
26
|
+
Metrics/PerceivedComplexity:
|
27
|
+
Max: 9
|
28
|
+
|
29
|
+
Metrics/ClassLength:
|
30
|
+
Enabled: false
|
31
|
+
|
32
|
+
Metrics/MethodLength:
|
33
|
+
Max: 58
|
34
|
+
|
35
|
+
Metrics/LineLength:
|
36
|
+
Max: 90
|
37
|
+
Exclude:
|
38
|
+
- 'Gemfile'
|
39
|
+
|
40
|
+
Metrics/BlockLength:
|
41
|
+
Enabled: false
|
42
|
+
|
43
|
+
Metrics/ModuleLength:
|
44
|
+
Enabled: false
|
45
|
+
|
46
|
+
Metrics/ParameterLists:
|
47
|
+
Max: 10
|
48
|
+
CountKeywordArgs: false
|
49
|
+
|
50
|
+
Naming/FileName:
|
51
|
+
Exclude:
|
52
|
+
- 'Gemfile'
|
53
|
+
|
54
|
+
Style/Documentation:
|
55
|
+
Enabled: false
|
56
|
+
|
57
|
+
Style/StringLiterals:
|
58
|
+
EnforcedStyle: "double_quotes"
|
59
|
+
Exclude:
|
60
|
+
- Gemfile
|
61
|
+
|
62
|
+
Layout/IndentFirstArrayElement:
|
63
|
+
EnforcedStyle: consistent
|
64
|
+
|
65
|
+
Layout/IndentFirstHashElement:
|
66
|
+
EnforcedStyle: consistent
|
67
|
+
|
68
|
+
Style/YodaCondition:
|
69
|
+
Enabled: true
|
70
|
+
|
71
|
+
RSpec/ExampleLength:
|
72
|
+
Enabled: false
|
73
|
+
|
74
|
+
RSpec/MultipleExpectations:
|
75
|
+
Max: 10
|
76
|
+
|
77
|
+
RSpec/NestedGroups:
|
78
|
+
Max: 8
|
79
|
+
|
80
|
+
RSpec/ImplicitSubject:
|
81
|
+
Enabled: false
|
82
|
+
|
83
|
+
RSpec/NamedSubject:
|
84
|
+
Enabled: false
|
85
|
+
|
86
|
+
RSpec/LetSetup:
|
87
|
+
Enabled: false
|
88
|
+
|
89
|
+
RSpec/ContextWording:
|
90
|
+
Prefixes:
|
91
|
+
- when
|
92
|
+
- with
|
93
|
+
- without
|
94
|
+
- if
|
95
|
+
- unless
|
96
|
+
- for
|
97
|
+
- and
|
98
|
+
|
99
|
+
Naming/UncommunicativeMethodParamName:
|
100
|
+
AllowedNames:
|
101
|
+
# These are the default allowed names, set by Rubocop
|
102
|
+
- io
|
103
|
+
- id
|
104
|
+
# These are some custom names that we want to allow, since they aren't
|
105
|
+
# uncommunicative - they're actually rather meaningful!
|
106
|
+
- to
|
107
|
+
- "_"
|
data/.ruby-version
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
2.7.0
|
data/Gemfile
ADDED
data/LICENCE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2020 Vito Giuliani
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,82 @@
|
|
1
|
+
# Randrizer
|
2
|
+
|
3
|
+
Randrizer is a small library that allows consumers to generate random data
|
4
|
+
for testing.
|
5
|
+
|
6
|
+
```ruby
|
7
|
+
irb(main):001:0> definition = Randrizer::Types::Dict[{
|
8
|
+
Randrizer::Types::Const["hello"] => Randrizer::Types::String[],
|
9
|
+
Randrizer::Types::Const["world"] => Randrizer::Types::Int[min: 5, max: 10],
|
10
|
+
Randrizer::Types::Const["nested"] => Randrizer::Types::Dict[
|
11
|
+
{
|
12
|
+
Randrizer::Types::Const["nested1"] => Randrizer::Types::Float[],
|
13
|
+
Randrizer::Types::Optional[inner_type: Randrizer::Types::Const["nested2"]] =>
|
14
|
+
Randrizer::Types::Int[]
|
15
|
+
}
|
16
|
+
]
|
17
|
+
}]
|
18
|
+
|
19
|
+
irb(main):002:0> definition.eval
|
20
|
+
=> {"hello"=>"XN3#tQd1%5#HWXI*p9zJD!tV^\\e%Wgais]vJQRNp$Z60FymI[=8~xy0IdVrmSb)me59zbqJjTbgZsv1NQA",
|
21
|
+
"world"=>6,
|
22
|
+
"nested"=>{"nested1"=>2274302953.724733}}
|
23
|
+
```
|
24
|
+
|
25
|
+
## Use cases
|
26
|
+
|
27
|
+
* Fuzzy testing: create massive amounts of random data for your application
|
28
|
+
to consume. Are the inputs sanitised correctly? Are invalid inputs accepted?
|
29
|
+
* Performance testing: generate random data to be fed to your application to
|
30
|
+
measure throughput and performance
|
31
|
+
* Anonymisation: replace your production data with random values
|
32
|
+
|
33
|
+
## Drivers
|
34
|
+
|
35
|
+
Currently there's a single driver out of the box for simple JSON schema files.
|
36
|
+
|
37
|
+
```json
|
38
|
+
$ cat json_schema.json
|
39
|
+
{
|
40
|
+
"$schema": "http://json-schema.org/draft-06/schema#",
|
41
|
+
"properties": {
|
42
|
+
"resource": {
|
43
|
+
"description": "A type",
|
44
|
+
"type": "string",
|
45
|
+
"enum": ["type 1", "type 2"]
|
46
|
+
},
|
47
|
+
"amount": {
|
48
|
+
"description": "Amount in cents",
|
49
|
+
"type": "integer",
|
50
|
+
"minimum": 0
|
51
|
+
},
|
52
|
+
"the_date": {
|
53
|
+
"description": "Birthday",
|
54
|
+
"type": "string",
|
55
|
+
"format": "date"
|
56
|
+
},
|
57
|
+
"country": {
|
58
|
+
"description": "Country code",
|
59
|
+
"type": "string",
|
60
|
+
"minLength": 2,
|
61
|
+
"maxLength": 2
|
62
|
+
}
|
63
|
+
},
|
64
|
+
"type": "object",
|
65
|
+
"required": [
|
66
|
+
"resource",
|
67
|
+
"amount",
|
68
|
+
"the_date",
|
69
|
+
"country"
|
70
|
+
]
|
71
|
+
}
|
72
|
+
```
|
73
|
+
|
74
|
+
```json
|
75
|
+
$ randrizer-json-schema json_schema.json
|
76
|
+
{
|
77
|
+
"resource": "type 1",
|
78
|
+
"amount": 302365828,
|
79
|
+
"the_date": "2157-09-13",
|
80
|
+
"country": "g-"
|
81
|
+
}
|
82
|
+
```
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "randrizer/version"
|
4
|
+
require "randrizer"
|
5
|
+
|
6
|
+
module Randrizer
|
7
|
+
class Cli
|
8
|
+
def self.run
|
9
|
+
return help if ARGV.empty?
|
10
|
+
|
11
|
+
fname = ARGV[0]
|
12
|
+
unless File.exist?(fname)
|
13
|
+
raise ArgumentError, "#{fname} does not exist or cannot be accessed"
|
14
|
+
end
|
15
|
+
|
16
|
+
input_json = File.read(fname)
|
17
|
+
type_tree = Drivers::JSONSchema::Driver.for(content: input_json).type_tree
|
18
|
+
output = type_tree.eval
|
19
|
+
puts JSON.pretty_generate(output)
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.help
|
23
|
+
STDOUT.write("Randrizer #{Randrizer::VERSION}\n")
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "json"
|
4
|
+
|
5
|
+
require "randrizer/drivers/json_schema/typegen"
|
6
|
+
|
7
|
+
module Randrizer
|
8
|
+
module Drivers
|
9
|
+
module JSONSchema
|
10
|
+
class Driver
|
11
|
+
# JSON Schemas are much more complex than one would initially imagine. This is a
|
12
|
+
# very simple driver for basic schemas, but not all the attributes and properties
|
13
|
+
# of types are respected.
|
14
|
+
# TODO: support for list and dict datatypes
|
15
|
+
|
16
|
+
attr_reader :path
|
17
|
+
|
18
|
+
def self.for(content:)
|
19
|
+
parsed_content = JSON.parse(content)
|
20
|
+
new(parsed_content)
|
21
|
+
end
|
22
|
+
|
23
|
+
def initialize(parsed_schema)
|
24
|
+
@json = parsed_schema
|
25
|
+
end
|
26
|
+
|
27
|
+
def type_tree
|
28
|
+
dict_items = []
|
29
|
+
|
30
|
+
required_keys = json.fetch("required", [])
|
31
|
+
json.fetch("properties", []).map do |key, attrs|
|
32
|
+
key_type = Types::Const[key]
|
33
|
+
root_type = attrs["type"]
|
34
|
+
value_type = gen_value_type(root_type, attrs)
|
35
|
+
|
36
|
+
unless required_keys.include?(key_type)
|
37
|
+
key_type = Types::Optional[inner_type: key_type]
|
38
|
+
end
|
39
|
+
|
40
|
+
dict_items.append([key_type, value_type])
|
41
|
+
end
|
42
|
+
|
43
|
+
Types::Dict[dict_items]
|
44
|
+
end
|
45
|
+
|
46
|
+
private
|
47
|
+
|
48
|
+
attr_reader :json
|
49
|
+
|
50
|
+
def gen_value_type(type, attrs)
|
51
|
+
case type
|
52
|
+
when "integer"
|
53
|
+
Typegen.t_integer(attrs)
|
54
|
+
when "number"
|
55
|
+
Typegen.t_number(attrs)
|
56
|
+
when "boolean"
|
57
|
+
Typegen.t_boolean(attrs)
|
58
|
+
when "string"
|
59
|
+
Typegen.t_string(attrs)
|
60
|
+
when "null"
|
61
|
+
Typegen.t_null(attrs)
|
62
|
+
when Array
|
63
|
+
Types::OneOf.build(type.map { |sub_t| gen_value_type(sub_t, attrs) })
|
64
|
+
else
|
65
|
+
raise "Unsupported type: #{type}"
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,123 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "randrizer/types"
|
4
|
+
|
5
|
+
module Randrizer
|
6
|
+
module Drivers
|
7
|
+
module JSONSchema
|
8
|
+
class Typegen
|
9
|
+
class << self
|
10
|
+
MONTH_INT_TYPE =
|
11
|
+
Types::OneOf[(1..12).map { |i| Types::Const.build(i.to_s.rjust(2, "0")) }]
|
12
|
+
DAY_INT_TYPE =
|
13
|
+
Types::OneOf[(1..31).map { |i| Types::Const.build(i.to_s.rjust(2, "0")) }]
|
14
|
+
HOUR_INT_TYPE =
|
15
|
+
Types::OneOf[(0..23).map { |i| Types::Const.build(i.to_s.rjust(2, "0")) }]
|
16
|
+
MIN_SEC_INT_TYPE =
|
17
|
+
Types::OneOf[(0..59).map { |i| Types::Const.build(i.to_s.rjust(2, "0")) }]
|
18
|
+
|
19
|
+
DATE_SEQUENCE = [
|
20
|
+
Types::Int.build(min: 1970, max: 2200),
|
21
|
+
Types::Const["-"],
|
22
|
+
MONTH_INT_TYPE,
|
23
|
+
Types::Const["-"],
|
24
|
+
DAY_INT_TYPE
|
25
|
+
].freeze
|
26
|
+
|
27
|
+
TIMEZONE_SEQUENCE = Types::OneOf.build([
|
28
|
+
Types::Const["Z"],
|
29
|
+
Types::StringSequence.build([
|
30
|
+
Types::OneOf.build([Types::Const["+"], Types::Const["-"]]),
|
31
|
+
HOUR_INT_TYPE,
|
32
|
+
Types::Const[":"],
|
33
|
+
MIN_SEC_INT_TYPE
|
34
|
+
])
|
35
|
+
])
|
36
|
+
|
37
|
+
TIME_SEQUENCE = [
|
38
|
+
HOUR_INT_TYPE,
|
39
|
+
Types::Const[":"],
|
40
|
+
MIN_SEC_INT_TYPE,
|
41
|
+
Types::Const[":"],
|
42
|
+
MIN_SEC_INT_TYPE,
|
43
|
+
Types::Optional.build(inner_type: TIMEZONE_SEQUENCE)
|
44
|
+
].freeze
|
45
|
+
|
46
|
+
EMAIL_SEQUENCE = [
|
47
|
+
Types::String.build(valid_chars: Types::String::CHARS_ALL_LETTERS + "._+"),
|
48
|
+
Types::Const["@"],
|
49
|
+
Types::String.build(valid_chars: Types::String::CHARS_ALL_LETTERS + "._")
|
50
|
+
].freeze
|
51
|
+
|
52
|
+
def t_null(_attrs)
|
53
|
+
# `Types::Nullable` would require an inner type, as this will never change
|
54
|
+
# we can just return a constant null value.
|
55
|
+
Types::Const[nil]
|
56
|
+
end
|
57
|
+
|
58
|
+
def t_boolean(_attrs)
|
59
|
+
Types::Bool[]
|
60
|
+
end
|
61
|
+
|
62
|
+
def t_number(attrs)
|
63
|
+
# TODO: support multipleOf
|
64
|
+
|
65
|
+
restrictions = {}
|
66
|
+
restrictions[:min] = attrs.fetch("minimum", -999_999_999)
|
67
|
+
restrictions[:max] = attrs.fetch("maximum", 999_999_999)
|
68
|
+
|
69
|
+
restrictions[:min] += 1 if attrs.fetch("exclusiveMinimum", false)
|
70
|
+
restrictions[:max] -= 1 if attrs.fetch("exclusiveMaximum", false)
|
71
|
+
|
72
|
+
Types::Int.build(**restrictions)
|
73
|
+
end
|
74
|
+
|
75
|
+
def t_integer(attrs)
|
76
|
+
t_number(attrs)
|
77
|
+
end
|
78
|
+
|
79
|
+
def t_string_with_format(_attrs, format)
|
80
|
+
# https://json-schema.org/understanding-json-schema/reference/string.html#built-in-formats
|
81
|
+
|
82
|
+
case format
|
83
|
+
when "date"
|
84
|
+
Types::StringSequence.build(DATE_SEQUENCE)
|
85
|
+
when "time"
|
86
|
+
Types::StringSequence.build(TIME_SEQUENCE)
|
87
|
+
when "date-time"
|
88
|
+
Types::StringSequence.build([
|
89
|
+
DATE_SEQUENCE,
|
90
|
+
Types::Const["T"],
|
91
|
+
TIME_SEQUENCE
|
92
|
+
].flatten)
|
93
|
+
when "email"
|
94
|
+
Types::StringSequence.build(EMAIL_SEQUENCE)
|
95
|
+
else
|
96
|
+
raise "Format not supported: #{format}"
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
def t_string(attrs)
|
101
|
+
# TODO: support String patterns format specs
|
102
|
+
|
103
|
+
if attrs.include?("enum")
|
104
|
+
return Types::OneOf[
|
105
|
+
attrs["enum"].map { |e| Types::Const[e] }
|
106
|
+
]
|
107
|
+
end
|
108
|
+
|
109
|
+
if attrs.include?("format")
|
110
|
+
return t_string_with_format(attrs, attrs["format"])
|
111
|
+
end
|
112
|
+
|
113
|
+
props = {}
|
114
|
+
props[:min_length] = attrs["minLength"].to_i if attrs.include?("minLength")
|
115
|
+
props[:max_length] = attrs["maxLength"].to_i if attrs.include?("maxLength")
|
116
|
+
|
117
|
+
Types::String.build(**props)
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "randrizer/types/type_builder"
|
4
|
+
|
5
|
+
module Randrizer
|
6
|
+
module Types
|
7
|
+
class Bool
|
8
|
+
include TypeBuilder
|
9
|
+
|
10
|
+
PRESENCE_MAYBE = 0.5
|
11
|
+
|
12
|
+
def initialize(true_prob: PRESENCE_MAYBE)
|
13
|
+
@true_prob = true_prob
|
14
|
+
end
|
15
|
+
|
16
|
+
def validate!
|
17
|
+
raise ValidationError("true_prob must be < 1.0") if @true_prob > 1.0
|
18
|
+
raise ValidationError("true_prob must be > 0.0") if @true_prob < 0.0
|
19
|
+
end
|
20
|
+
|
21
|
+
def eval
|
22
|
+
rand > (1.0 - @true_prob)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "randrizer/types/type_builder"
|
4
|
+
|
5
|
+
module Randrizer
|
6
|
+
module Types
|
7
|
+
class Const
|
8
|
+
include TypeBuilder
|
9
|
+
|
10
|
+
def initialize(params)
|
11
|
+
@params = params
|
12
|
+
end
|
13
|
+
|
14
|
+
def validate!; end
|
15
|
+
|
16
|
+
def eval
|
17
|
+
@params
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "randrizer/types/skip"
|
4
|
+
require "randrizer/types/type_builder"
|
5
|
+
|
6
|
+
module Randrizer
|
7
|
+
module Types
|
8
|
+
class Dict
|
9
|
+
include TypeBuilder
|
10
|
+
|
11
|
+
def initialize(keys_def)
|
12
|
+
@keys_def = keys_def
|
13
|
+
end
|
14
|
+
|
15
|
+
def validate!; end
|
16
|
+
|
17
|
+
def eval
|
18
|
+
@keys_def.each_with_object({}) do |(key_type, value_type), hash|
|
19
|
+
key = key_type.eval
|
20
|
+
value = value_type.eval
|
21
|
+
|
22
|
+
next if key == SKIP || value == SKIP
|
23
|
+
|
24
|
+
hash[key] = value
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def empty?
|
29
|
+
@keys_def.empty?
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "randrizer/types/type_builder"
|
4
|
+
|
5
|
+
module Randrizer
|
6
|
+
module Types
|
7
|
+
class Float
|
8
|
+
include TypeBuilder
|
9
|
+
|
10
|
+
DEFAULT_MIN = 0.0
|
11
|
+
DEFAULT_MAX = 9_999_999_999.0
|
12
|
+
|
13
|
+
def initialize(min: DEFAULT_MIN, max: DEFAULT_MAX)
|
14
|
+
@min = min
|
15
|
+
@max = max
|
16
|
+
end
|
17
|
+
|
18
|
+
def validate!
|
19
|
+
return if @max >= @min
|
20
|
+
|
21
|
+
raise ValidationError("invalid min/max configuration")
|
22
|
+
end
|
23
|
+
|
24
|
+
def eval
|
25
|
+
return @min if @min == @max
|
26
|
+
|
27
|
+
rand * (@max - @min) + @min
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "randrizer/types/type_builder"
|
4
|
+
|
5
|
+
module Randrizer
|
6
|
+
module Types
|
7
|
+
class Int
|
8
|
+
include TypeBuilder
|
9
|
+
|
10
|
+
DEFAULT_MIN = 0
|
11
|
+
DEFAULT_MAX = 9_999_999_999
|
12
|
+
|
13
|
+
def initialize(min: DEFAULT_MIN, max: DEFAULT_MAX)
|
14
|
+
@min = min
|
15
|
+
@max = max
|
16
|
+
end
|
17
|
+
|
18
|
+
def validate!
|
19
|
+
return if @max >= @min
|
20
|
+
|
21
|
+
raise ValidationError("invalid min/max configuration")
|
22
|
+
end
|
23
|
+
|
24
|
+
def eval
|
25
|
+
return @min if @min == @max
|
26
|
+
|
27
|
+
rand(@min..@max)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "randrizer/types/skip"
|
4
|
+
require "randrizer/types/type_builder"
|
5
|
+
|
6
|
+
module Randrizer
|
7
|
+
module Types
|
8
|
+
class List
|
9
|
+
include TypeBuilder
|
10
|
+
|
11
|
+
def initialize(list_def)
|
12
|
+
@list_def = list_def
|
13
|
+
end
|
14
|
+
|
15
|
+
def validate!
|
16
|
+
!@list_def.nil?
|
17
|
+
end
|
18
|
+
|
19
|
+
def eval
|
20
|
+
@list_def.map(&:eval).reject { |evaluated| evaluated == SKIP }
|
21
|
+
end
|
22
|
+
|
23
|
+
def empty?
|
24
|
+
@list_def.empty?
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "randrizer/types/type_builder"
|
4
|
+
|
5
|
+
module Randrizer
|
6
|
+
module Types
|
7
|
+
class Nullable
|
8
|
+
include TypeBuilder
|
9
|
+
|
10
|
+
def initialize(null_prob:, inner_type:)
|
11
|
+
@null_prob = null_prob
|
12
|
+
@inner_type = inner_type
|
13
|
+
end
|
14
|
+
|
15
|
+
def validate!
|
16
|
+
raise ValidationError("null_prob must be < 1.0") if @null_prob > 1.0
|
17
|
+
raise ValidationError("null_prob must be > 0.0") if @null_prob < 0.0
|
18
|
+
end
|
19
|
+
|
20
|
+
def eval
|
21
|
+
return nil if rand > (1.0 - @null_prob)
|
22
|
+
|
23
|
+
@inner_type.eval
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "randrizer/types/type_builder"
|
4
|
+
|
5
|
+
module Randrizer
|
6
|
+
module Types
|
7
|
+
class OneOf
|
8
|
+
include TypeBuilder
|
9
|
+
|
10
|
+
def initialize(list_def)
|
11
|
+
@list_def = list_def
|
12
|
+
end
|
13
|
+
|
14
|
+
def validate!
|
15
|
+
!@list_def.nil?
|
16
|
+
end
|
17
|
+
|
18
|
+
def eval
|
19
|
+
@list_def.sample.eval
|
20
|
+
end
|
21
|
+
|
22
|
+
def length
|
23
|
+
@list_def.length
|
24
|
+
end
|
25
|
+
|
26
|
+
alias count length
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "randrizer/types/skip"
|
4
|
+
require "randrizer/types/type_builder"
|
5
|
+
|
6
|
+
module Randrizer
|
7
|
+
module Types
|
8
|
+
class Optional
|
9
|
+
include TypeBuilder
|
10
|
+
|
11
|
+
PRESENCE_MAYBE = 0.5
|
12
|
+
|
13
|
+
def initialize(inner_type:, presence_prob: PRESENCE_MAYBE)
|
14
|
+
@inner_type = inner_type
|
15
|
+
@presence_prob = presence_prob
|
16
|
+
end
|
17
|
+
|
18
|
+
def validate!
|
19
|
+
raise ValidationError("presence_prob must be < 1.0") if @presence_prob > 1.0
|
20
|
+
raise ValidationError("presence_prob must be > 0.0") if @presence_prob < 0.0
|
21
|
+
end
|
22
|
+
|
23
|
+
def eval
|
24
|
+
return SKIP if rand > @presence_prob
|
25
|
+
|
26
|
+
@inner_type.eval
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "randrizer/types/type_builder"
|
4
|
+
|
5
|
+
module Randrizer
|
6
|
+
module Types
|
7
|
+
class String
|
8
|
+
include TypeBuilder
|
9
|
+
|
10
|
+
CHARS_NUMBERS = "0123456789"
|
11
|
+
CHARS_SYMBOLS = " !\"£$%^&()=-*/[]#\\~"
|
12
|
+
CHARS_LOWERCASE_LETTERS = "abcdfeghijklmnopqrstuvwxyz"
|
13
|
+
CHARS_UPPERCASE_LETTERS = CHARS_LOWERCASE_LETTERS.upcase
|
14
|
+
CHARS_ALL_LETTERS = CHARS_LOWERCASE_LETTERS + CHARS_UPPERCASE_LETTERS
|
15
|
+
|
16
|
+
DEFAULT_VALID_CHARS =
|
17
|
+
CHARS_NUMBERS +
|
18
|
+
CHARS_ALL_LETTERS +
|
19
|
+
CHARS_SYMBOLS
|
20
|
+
|
21
|
+
DEFAULT_MIN_LENGTH = 0
|
22
|
+
DEFAULT_MAX_LENGTH = 99
|
23
|
+
|
24
|
+
def initialize(min_length: DEFAULT_MIN_LENGTH,
|
25
|
+
max_length: DEFAULT_MAX_LENGTH,
|
26
|
+
valid_chars: DEFAULT_VALID_CHARS)
|
27
|
+
@min_length = min_length
|
28
|
+
@max_length = max_length
|
29
|
+
@valid_chars = valid_chars
|
30
|
+
end
|
31
|
+
|
32
|
+
def validate!
|
33
|
+
return if @max_length >= @min_length
|
34
|
+
|
35
|
+
raise ValidationError("invalid length configuration")
|
36
|
+
end
|
37
|
+
|
38
|
+
def eval
|
39
|
+
chars_split = @valid_chars.split("")
|
40
|
+
string_length = rand(@min_length..@max_length)
|
41
|
+
string_length.times.map { chars_split.sample }.join
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "randrizer/types/type_builder"
|
4
|
+
|
5
|
+
module Randrizer
|
6
|
+
module Types
|
7
|
+
class StringSequence
|
8
|
+
include TypeBuilder
|
9
|
+
|
10
|
+
ALLOWED_TYPES = [
|
11
|
+
Types::Int,
|
12
|
+
Types::String,
|
13
|
+
Types::Const,
|
14
|
+
Types::Nullable,
|
15
|
+
Types::Optional,
|
16
|
+
Types::OneOf
|
17
|
+
].freeze
|
18
|
+
|
19
|
+
def initialize(sequence_def)
|
20
|
+
@sequence_def = sequence_def
|
21
|
+
end
|
22
|
+
|
23
|
+
def validate!
|
24
|
+
disallowed = @sequence_def.reject { |item| ALLOWED_TYPES.include?(item.class) }
|
25
|
+
|
26
|
+
raise ValidationError("types not allowed in a string sequence: #{disallowed}")
|
27
|
+
end
|
28
|
+
|
29
|
+
def eval
|
30
|
+
@sequence_def.map(&:eval).reject { |evaluated| evaluated == SKIP }.compact.join
|
31
|
+
end
|
32
|
+
|
33
|
+
def empty?
|
34
|
+
@sequence_def.empty?
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Randrizer
|
4
|
+
module Types
|
5
|
+
module TypeBuilder
|
6
|
+
class ValidationError < StandardError; end
|
7
|
+
|
8
|
+
def self.included(base)
|
9
|
+
base.extend(ClassMethods)
|
10
|
+
end
|
11
|
+
|
12
|
+
def validate!
|
13
|
+
raise NotImplementedError
|
14
|
+
end
|
15
|
+
|
16
|
+
module ClassMethods
|
17
|
+
def build(*args, **params)
|
18
|
+
new(*args, **params)
|
19
|
+
end
|
20
|
+
|
21
|
+
alias [] build
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Primitive types. They should not depend on each other.
|
4
|
+
require "randrizer/types/const"
|
5
|
+
require "randrizer/types/bool"
|
6
|
+
require "randrizer/types/int"
|
7
|
+
require "randrizer/types/float"
|
8
|
+
require "randrizer/types/string"
|
9
|
+
require "randrizer/types/one_of"
|
10
|
+
require "randrizer/types/skip"
|
11
|
+
require "randrizer/types/optional"
|
12
|
+
require "randrizer/types/nullable"
|
13
|
+
|
14
|
+
# Iterable types. They might inspect the internal types to special case behaviour and
|
15
|
+
# as such must be declared after the primitive types.
|
16
|
+
require "randrizer/types/list"
|
17
|
+
require "randrizer/types/dict"
|
18
|
+
require "randrizer/types/string_sequence"
|
data/lib/randrizer.rb
ADDED
data/randrizer.gemspec
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
lib = File.expand_path("lib", __dir__)
|
4
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
5
|
+
require "randrizer/version"
|
6
|
+
|
7
|
+
Gem::Specification.new do |spec|
|
8
|
+
spec.name = "randrizer"
|
9
|
+
spec.homepage = "https://github.com/ivgiuliani/randrizer"
|
10
|
+
spec.version = Randrizer::VERSION
|
11
|
+
spec.authors = ["Ivan Giuliani"]
|
12
|
+
spec.email = ["giuliani.v@gmail.com"]
|
13
|
+
|
14
|
+
spec.summary = "Generate random data for testing"
|
15
|
+
spec.description = "Random data generator with type composition"
|
16
|
+
spec.license = "MIT"
|
17
|
+
|
18
|
+
# Specify which files should be added to the gem when it is released.
|
19
|
+
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
20
|
+
spec.files = Dir.chdir(File.expand_path(__dir__)) do
|
21
|
+
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
22
|
+
end
|
23
|
+
spec.bindir = "exe"
|
24
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
25
|
+
spec.require_paths = ["lib"]
|
26
|
+
|
27
|
+
spec.add_development_dependency "bundler", "~> 2.0"
|
28
|
+
spec.add_development_dependency "pry-byebug", "~> 3.7.0"
|
29
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
30
|
+
spec.add_development_dependency "rspec", "~> 3.0"
|
31
|
+
spec.add_development_dependency "rubocop", "~> 0.75.0"
|
32
|
+
spec.add_development_dependency "rubocop-rspec", "~> 1.36.0"
|
33
|
+
end
|
metadata
ADDED
@@ -0,0 +1,157 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: randrizer
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Ivan Giuliani
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2020-02-26 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bundler
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '2.0'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '2.0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: pry-byebug
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 3.7.0
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: 3.7.0
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rake
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '10.0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '10.0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rspec
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '3.0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '3.0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: rubocop
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: 0.75.0
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: 0.75.0
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: rubocop-rspec
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - "~>"
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: 1.36.0
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - "~>"
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: 1.36.0
|
97
|
+
description: Random data generator with type composition
|
98
|
+
email:
|
99
|
+
- giuliani.v@gmail.com
|
100
|
+
executables:
|
101
|
+
- randrizer-json-schema
|
102
|
+
extensions: []
|
103
|
+
extra_rdoc_files: []
|
104
|
+
files:
|
105
|
+
- ".gitignore"
|
106
|
+
- ".rubocop.yml"
|
107
|
+
- ".ruby-version"
|
108
|
+
- Gemfile
|
109
|
+
- LICENCE.txt
|
110
|
+
- README.md
|
111
|
+
- exe/randrizer-json-schema
|
112
|
+
- lib/randrizer.rb
|
113
|
+
- lib/randrizer/cli.rb
|
114
|
+
- lib/randrizer/drivers/json_schema.rb
|
115
|
+
- lib/randrizer/drivers/json_schema/driver.rb
|
116
|
+
- lib/randrizer/drivers/json_schema/typegen.rb
|
117
|
+
- lib/randrizer/generator.rb
|
118
|
+
- lib/randrizer/types.rb
|
119
|
+
- lib/randrizer/types/bool.rb
|
120
|
+
- lib/randrizer/types/const.rb
|
121
|
+
- lib/randrizer/types/dict.rb
|
122
|
+
- lib/randrizer/types/float.rb
|
123
|
+
- lib/randrizer/types/int.rb
|
124
|
+
- lib/randrizer/types/list.rb
|
125
|
+
- lib/randrizer/types/nullable.rb
|
126
|
+
- lib/randrizer/types/one_of.rb
|
127
|
+
- lib/randrizer/types/optional.rb
|
128
|
+
- lib/randrizer/types/skip.rb
|
129
|
+
- lib/randrizer/types/string.rb
|
130
|
+
- lib/randrizer/types/string_sequence.rb
|
131
|
+
- lib/randrizer/types/type_builder.rb
|
132
|
+
- lib/randrizer/version.rb
|
133
|
+
- randrizer.gemspec
|
134
|
+
homepage: https://github.com/ivgiuliani/randrizer
|
135
|
+
licenses:
|
136
|
+
- MIT
|
137
|
+
metadata: {}
|
138
|
+
post_install_message:
|
139
|
+
rdoc_options: []
|
140
|
+
require_paths:
|
141
|
+
- lib
|
142
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
143
|
+
requirements:
|
144
|
+
- - ">="
|
145
|
+
- !ruby/object:Gem::Version
|
146
|
+
version: '0'
|
147
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
148
|
+
requirements:
|
149
|
+
- - ">="
|
150
|
+
- !ruby/object:Gem::Version
|
151
|
+
version: '0'
|
152
|
+
requirements: []
|
153
|
+
rubygems_version: 3.1.2
|
154
|
+
signing_key:
|
155
|
+
specification_version: 4
|
156
|
+
summary: Generate random data for testing
|
157
|
+
test_files: []
|