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 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
@@ -0,0 +1,12 @@
1
+ .DS_Store
2
+
3
+ .data/
4
+ .bundle/
5
+ log/
6
+ tmp/
7
+ .idea/
8
+
9
+ .rspec_status
10
+
11
+ .local_files
12
+ Gemfile.lock
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
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ source 'https://rubygems.org'
4
+
5
+ gemspec
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,9 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "bundler/setup"
5
+ require "randrizer"
6
+ require "randrizer/cli"
7
+
8
+ Randrizer::Cli.run
9
+ exit 0
@@ -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,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "randrizer/drivers/json_schema/driver"
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "randrizer/types"
4
+
5
+ module Randrizer
6
+ class Generator
7
+ def self.generate(type)
8
+ type.validate!
9
+ type.eval
10
+ end
11
+ end
12
+ 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,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Randrizer
4
+ module Types
5
+ class Skip
6
+ def ==(other)
7
+ other.class == Skip
8
+ end
9
+
10
+ def eval
11
+ self
12
+ end
13
+ end
14
+
15
+ SKIP = Skip.new
16
+ end
17
+ 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"
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Randrizer
4
+ VERSION = "0.0.1"
5
+ end
data/lib/randrizer.rb ADDED
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "randrizer/version"
4
+ require "randrizer/types"
5
+ require "randrizer/drivers/json_schema"
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: []