astroturf 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +21 -0
- data/Gemfile +5 -0
- data/LICENSE.txt +21 -0
- data/README.md +188 -0
- data/Rakefile +12 -0
- data/astroturf.gemspec +27 -0
- data/lib/astroturf/dataset.rb +57 -0
- data/lib/astroturf/definition.rb +98 -0
- data/lib/astroturf/errors.rb +8 -0
- data/lib/astroturf/generator.rb +193 -0
- data/lib/astroturf/inflector.rb +109 -0
- data/lib/astroturf/labeler.rb +51 -0
- data/lib/astroturf/matrix.rb +59 -0
- data/lib/astroturf/primary_key/integer_sequence.rb +25 -0
- data/lib/astroturf/primary_key/uuid.rb +42 -0
- data/lib/astroturf/primary_key.rb +63 -0
- data/lib/astroturf/version.rb +5 -0
- data/lib/astroturf.rb +39 -0
- metadata +88 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 79d39987b44a757f2689d900dd7256baef7b9d95a3206bf33384b84d57c84b9a
|
|
4
|
+
data.tar.gz: '004561878c96134cdfe94faf12bfb503985aa941e608e353b6a055f8d39073dc'
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 49b5b8e9dba709aaef0bb233a04a0be9d97feac081a59a139a193b915a7b300316dfd39d5440469a12d568ac3052093f7eb9ff5930729c262f8fa9592d072dc4
|
|
7
|
+
data.tar.gz: 1d660ba73e13a93ea2c385815a7ea8283093aac4329bd5321b5b4b77d779caa5ebe523b50a72b94f23ae1b6d951ab4762f6d7ef8ba5de6d9cd10eb2647ec4965
|
data/.gitignore
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
*.gem
|
|
2
|
+
*.rbc
|
|
3
|
+
/.config
|
|
4
|
+
/coverage/
|
|
5
|
+
/InstalledFiles
|
|
6
|
+
/pkg/
|
|
7
|
+
/spec/reports/
|
|
8
|
+
/spec/examples.txt
|
|
9
|
+
/test/tmp/
|
|
10
|
+
/test/version_tmp/
|
|
11
|
+
/tmp/
|
|
12
|
+
|
|
13
|
+
# Bundler
|
|
14
|
+
/.bundle/
|
|
15
|
+
/vendor/bundle
|
|
16
|
+
/lib/bundler/man/
|
|
17
|
+
|
|
18
|
+
# Environment / IDE
|
|
19
|
+
.DS_Store
|
|
20
|
+
.vscode/
|
|
21
|
+
.idea/
|
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Jeff Lunt <jefflunt@gmail.com>
|
|
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,188 @@
|
|
|
1
|
+
# Astroturf
|
|
2
|
+
|
|
3
|
+
Exhaustive synthetic data generator for Rails and Ruby applications.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Why Astroturf?
|
|
8
|
+
|
|
9
|
+
Most test suites test only the happy path.
|
|
10
|
+
|
|
11
|
+
Hand-written fixtures rot. Factory builders are slow and sample only a tiny fraction of valid states. When you undertake a large architectural overhaul or an automated AI-driven refactoring pass, hidden edge cases surface in production: an unhandled enum state, a `nil` middle name, an unexpected boolean combination, or a broken foreign key relationship.
|
|
12
|
+
|
|
13
|
+
**Astroturf aims for total coverage of your value and combination space.**
|
|
14
|
+
|
|
15
|
+
You define the dimensions in a YAML file. Astroturf computes the Cartesian product across every attribute, wires relational foreign keys, generates deterministic labels, and produces in-memory datasets in milliseconds.
|
|
16
|
+
|
|
17
|
+
Use these datasets to:
|
|
18
|
+
- Verify behavioral equivalence before and after sweeping refactors.
|
|
19
|
+
- Generate deterministic Rails fixtures (`test/fixtures/*.yml`).
|
|
20
|
+
- Seed development databases with every permutation of your domain model.
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## Features
|
|
25
|
+
|
|
26
|
+
- **Full Cartesian Matrix Generation**: Computes every combination across arbitrary attribute dimensions.
|
|
27
|
+
- **Relational Graph Wiring**: Supports nested `has_many` and `has_one` hierarchies with automatic foreign key resolution.
|
|
28
|
+
- **Two Association Modes**:
|
|
29
|
+
- `mode: combinations` (default): Generates the full Cartesian explosion of child records for every parent record.
|
|
30
|
+
- `mode: count`: Cycles or samples a fixed count of children per parent.
|
|
31
|
+
- **Semantic Fixture Labels**: Automatically names records from their matrix values (e.g., `user_admin_active_true`, `user_admin_post_draft`).
|
|
32
|
+
- **Pluggable Primary Keys**:
|
|
33
|
+
- Sequential integers (`1, 2, 3...`) with configurable starting points and steps.
|
|
34
|
+
- Deterministic RFC 4122 UUIDv5 strings based on record labels.
|
|
35
|
+
- Custom callable procs.
|
|
36
|
+
- **Attribute Interpolation**: Dynamically references `%{label}`, `%{seq}`, `%{id}`, parent attributes (`%{parent.label}`, `%{parent.id}`), or current matrix values in string templates.
|
|
37
|
+
- **Zero External Dependencies**: Pure Ruby. Generates fast in-memory hashes without requiring an active database connection.
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## The Matrix Specification: `astroturf.yml`
|
|
42
|
+
|
|
43
|
+
```yaml
|
|
44
|
+
users:
|
|
45
|
+
primary_key: integer # Options: integer (default), uuid, or hash options
|
|
46
|
+
matrix:
|
|
47
|
+
role: [admin, member, guest]
|
|
48
|
+
status: [active, suspended]
|
|
49
|
+
email_verified: [true, false]
|
|
50
|
+
defaults:
|
|
51
|
+
name: "User %{label}"
|
|
52
|
+
email: "%{label}@example.com"
|
|
53
|
+
created_at: 2026-01-01 00:00:00 UTC
|
|
54
|
+
|
|
55
|
+
has_many:
|
|
56
|
+
# Mode 1: Full Cartesian Explosion (Default)
|
|
57
|
+
# 12 users * 4 post combinations = 48 posts total
|
|
58
|
+
posts:
|
|
59
|
+
mode: combinations
|
|
60
|
+
matrix:
|
|
61
|
+
visibility: [public, private]
|
|
62
|
+
published: [true, false]
|
|
63
|
+
defaults:
|
|
64
|
+
title: "Post for %{parent.label}"
|
|
65
|
+
|
|
66
|
+
# Mode 2: Fixed Cardinality
|
|
67
|
+
# Each user gets exactly 2 tokens
|
|
68
|
+
tokens:
|
|
69
|
+
mode: count
|
|
70
|
+
count: 2
|
|
71
|
+
defaults:
|
|
72
|
+
token: "tok_%{parent.id}_%{seq}"
|
|
73
|
+
|
|
74
|
+
has_one:
|
|
75
|
+
profile:
|
|
76
|
+
defaults:
|
|
77
|
+
bio: "Bio for %{parent.label}"
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
## Usage
|
|
83
|
+
|
|
84
|
+
### 1. Generate In-Memory Hashes
|
|
85
|
+
|
|
86
|
+
```ruby
|
|
87
|
+
require "astroturf"
|
|
88
|
+
|
|
89
|
+
dataset = Astroturf.load_file("astroturf.yml")
|
|
90
|
+
# or: dataset = Astroturf.load(yaml_string_or_hash)
|
|
91
|
+
|
|
92
|
+
# Access all generated tables and records
|
|
93
|
+
dataset.tables.keys
|
|
94
|
+
# => ["users", "posts", "tokens", "profiles"]
|
|
95
|
+
|
|
96
|
+
# Inspect a single table
|
|
97
|
+
users = dataset["users"]
|
|
98
|
+
# => {
|
|
99
|
+
# "user_admin_active_true" => {
|
|
100
|
+
# "id" => 1,
|
|
101
|
+
# "role" => "admin",
|
|
102
|
+
# "status" => "active",
|
|
103
|
+
# "email_verified" => true,
|
|
104
|
+
# "name" => "User user_admin_active_true",
|
|
105
|
+
# "email" => "user_admin_active_true@example.com",
|
|
106
|
+
# "created_at" => 2026-01-01 00:00:00 UTC
|
|
107
|
+
# },
|
|
108
|
+
# ...
|
|
109
|
+
# }
|
|
110
|
+
|
|
111
|
+
# Total record count
|
|
112
|
+
dataset.total_records
|
|
113
|
+
# => 74
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### 2. Export Rails Fixtures
|
|
117
|
+
|
|
118
|
+
Generate standard Rails fixture YAML strings or write them directly to your test suite:
|
|
119
|
+
|
|
120
|
+
```ruby
|
|
121
|
+
# Export as a YAML string for a single table
|
|
122
|
+
yaml_output = dataset.to_fixtures("users")
|
|
123
|
+
|
|
124
|
+
# Write .yml fixture files directly to disk
|
|
125
|
+
dataset.write_fixtures!("test/fixtures")
|
|
126
|
+
# Creates:
|
|
127
|
+
# test/fixtures/users.yml
|
|
128
|
+
# test/fixtures/posts.yml
|
|
129
|
+
# test/fixtures/tokens.yml
|
|
130
|
+
# test/fixtures/profiles.yml
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
---
|
|
134
|
+
|
|
135
|
+
## Primary Key Strategies
|
|
136
|
+
|
|
137
|
+
### Integer Sequences (Default)
|
|
138
|
+
|
|
139
|
+
```yaml
|
|
140
|
+
users:
|
|
141
|
+
primary_key: integer
|
|
142
|
+
# Or with custom offset and step:
|
|
143
|
+
# primary_key:
|
|
144
|
+
# type: integer
|
|
145
|
+
# start_at: 1000
|
|
146
|
+
# step: 5
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
### Deterministic UUIDs
|
|
150
|
+
|
|
151
|
+
Astroturf produces RFC 4122 UUIDv5 values derived from the record's label. Generating the same dataset always produces identical UUIDs.
|
|
152
|
+
|
|
153
|
+
```yaml
|
|
154
|
+
accounts:
|
|
155
|
+
primary_key: uuid
|
|
156
|
+
matrix:
|
|
157
|
+
plan: [free, pro]
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
---
|
|
161
|
+
|
|
162
|
+
## Running Tests
|
|
163
|
+
|
|
164
|
+
Run the test suite using Rake:
|
|
165
|
+
|
|
166
|
+
```bash
|
|
167
|
+
bundle exec rake test
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
Or run tests directly with Ruby:
|
|
171
|
+
|
|
172
|
+
```bash
|
|
173
|
+
ruby -Ilib -Itest test/astroturf_test.rb
|
|
174
|
+
ruby -Ilib -Itest test/matrix_test.rb
|
|
175
|
+
ruby -Ilib -Itest test/primary_key_test.rb
|
|
176
|
+
ruby -Ilib -Itest test/labeler_test.rb
|
|
177
|
+
ruby -Ilib -Itest test/definition_test.rb
|
|
178
|
+
ruby -Ilib -Itest test/generator_test.rb
|
|
179
|
+
ruby -Ilib -Itest test/dataset_test.rb
|
|
180
|
+
ruby -Ilib -Itest test/inflector_test.rb
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
---
|
|
184
|
+
|
|
185
|
+
## License
|
|
186
|
+
|
|
187
|
+
This project is available as open source under the terms of the [MIT License](LICENSE.txt).
|
|
188
|
+
Authored by Jeff Lunt (<jefflunt@gmail.com>).
|
data/Rakefile
ADDED
data/astroturf.gemspec
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "lib/astroturf/version"
|
|
4
|
+
|
|
5
|
+
Gem::Specification.new do |spec|
|
|
6
|
+
spec.name = "astroturf"
|
|
7
|
+
spec.version = Astroturf::VERSION
|
|
8
|
+
spec.authors = ["Jeff Lunt"]
|
|
9
|
+
spec.email = ["jefflunt@gmail.com"]
|
|
10
|
+
spec.summary = "Exhaustive synthetic data generator for Rails and Ruby applications"
|
|
11
|
+
spec.description = "Astroturf generates combinatorial synthetic data graphs in memory from declarative YAML matrix specifications."
|
|
12
|
+
spec.homepage = "https://github.com/elabs/astroturf"
|
|
13
|
+
spec.license = "MIT"
|
|
14
|
+
spec.required_ruby_version = ">= 2.6.0"
|
|
15
|
+
|
|
16
|
+
spec.metadata["source_code_uri"] = spec.homepage
|
|
17
|
+
|
|
18
|
+
spec.files = Dir.chdir(File.expand_path(__dir__)) do
|
|
19
|
+
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{\A(?:test|spec|features)/}) }
|
|
20
|
+
end
|
|
21
|
+
spec.bindir = "exe"
|
|
22
|
+
spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
|
|
23
|
+
spec.require_paths = ["lib"]
|
|
24
|
+
|
|
25
|
+
spec.add_development_dependency "minitest", "~> 5.0"
|
|
26
|
+
spec.add_development_dependency "rake", "~> 12.0"
|
|
27
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
require "yaml"
|
|
5
|
+
|
|
6
|
+
module Astroturf
|
|
7
|
+
class Dataset
|
|
8
|
+
attr_reader :tables
|
|
9
|
+
|
|
10
|
+
def initialize(tables = {})
|
|
11
|
+
@tables = tables
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def to_h
|
|
15
|
+
@tables
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def [](table_name)
|
|
19
|
+
@tables[table_name.to_s] || {}
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def table_names
|
|
23
|
+
@tables.keys
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def records(table_name)
|
|
27
|
+
self[table_name].values
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def total_records
|
|
31
|
+
@tables.values.map(&:size).sum
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def to_fixtures(table_name = nil)
|
|
35
|
+
if table_name
|
|
36
|
+
data = self[table_name]
|
|
37
|
+
data.empty? ? "{}\n" : YAML.dump(data)
|
|
38
|
+
else
|
|
39
|
+
@tables.each_with_object({}) do |(tbl, data), hash|
|
|
40
|
+
hash[tbl] = data.empty? ? "{}\n" : YAML.dump(data)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def write_fixtures!(dir_path)
|
|
46
|
+
FileUtils.mkdir_p(dir_path)
|
|
47
|
+
|
|
48
|
+
written_files = []
|
|
49
|
+
@tables.each do |tbl, data|
|
|
50
|
+
file_path = File.join(dir_path, "#{tbl}.yml")
|
|
51
|
+
File.write(file_path, data.empty? ? "{}\n" : YAML.dump(data))
|
|
52
|
+
written_files << file_path
|
|
53
|
+
end
|
|
54
|
+
written_files
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "matrix"
|
|
4
|
+
require_relative "primary_key"
|
|
5
|
+
require_relative "inflector"
|
|
6
|
+
require_relative "errors"
|
|
7
|
+
|
|
8
|
+
module Astroturf
|
|
9
|
+
class Definition
|
|
10
|
+
attr_reader :name, :matrix, :defaults, :pk_spec, :associations, :options
|
|
11
|
+
|
|
12
|
+
def self.from_hash(hash)
|
|
13
|
+
raise ConfigurationError, "Astroturf definition must be a Hash" unless hash.is_a?(Hash)
|
|
14
|
+
|
|
15
|
+
hash.map do |entity_name, config|
|
|
16
|
+
new(entity_name, config || {})
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def initialize(name, config = {})
|
|
21
|
+
@name = name.to_s
|
|
22
|
+
@options = config || {}
|
|
23
|
+
@matrix = Matrix.new(config[:matrix] || config["matrix"] || {})
|
|
24
|
+
@defaults = config[:defaults] || config["defaults"] || {}
|
|
25
|
+
@pk_spec = config[:primary_key] || config["primary_key"] || :integer
|
|
26
|
+
@associations = parse_associations(config)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def singular_name
|
|
30
|
+
Inflector.singularize(@name)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def table_name
|
|
34
|
+
@options[:table_name] || @options["table_name"] || Inflector.tableize(@name)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def foreign_key_name
|
|
38
|
+
@options[:foreign_key] || @options["foreign_key"] || "#{singular_name}_id"
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
def parse_associations(config)
|
|
44
|
+
associations = []
|
|
45
|
+
|
|
46
|
+
# Parse has_many
|
|
47
|
+
raw_has_many = config[:has_many] || config["has_many"] || {}
|
|
48
|
+
raw_has_many.each do |assoc_name, assoc_config|
|
|
49
|
+
associations << build_association(:has_many, assoc_name, assoc_config)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Parse has_one
|
|
53
|
+
raw_has_one = config[:has_one] || config["has_one"] || {}
|
|
54
|
+
raw_has_one.each do |assoc_name, assoc_config|
|
|
55
|
+
associations << build_association(:has_one, assoc_name, assoc_config)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
associations
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def build_association(type, assoc_name, assoc_config)
|
|
62
|
+
assoc_config ||= {}
|
|
63
|
+
mode = (assoc_config[:mode] || assoc_config["mode"] || :combinations).to_sym
|
|
64
|
+
count = assoc_config[:count] || assoc_config["count"]
|
|
65
|
+
foreign_key = assoc_config[:foreign_key] || assoc_config["foreign_key"]
|
|
66
|
+
|
|
67
|
+
Association.new(
|
|
68
|
+
type: type,
|
|
69
|
+
name: assoc_name.to_s,
|
|
70
|
+
definition: Definition.new(assoc_name, assoc_config),
|
|
71
|
+
mode: mode,
|
|
72
|
+
count: count ? count.to_i : nil,
|
|
73
|
+
foreign_key: foreign_key
|
|
74
|
+
)
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
class Association
|
|
79
|
+
attr_reader :type, :name, :definition, :mode, :count, :foreign_key
|
|
80
|
+
|
|
81
|
+
def initialize(type:, name:, definition:, mode: :combinations, count: nil, foreign_key: nil)
|
|
82
|
+
@type = type
|
|
83
|
+
@name = name
|
|
84
|
+
@definition = definition
|
|
85
|
+
@mode = mode
|
|
86
|
+
@count = count
|
|
87
|
+
@foreign_key = foreign_key
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def has_many?
|
|
91
|
+
@type == :has_many
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def has_one?
|
|
95
|
+
@type == :has_one
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "dataset"
|
|
4
|
+
require_relative "labeler"
|
|
5
|
+
require_relative "primary_key"
|
|
6
|
+
|
|
7
|
+
module Astroturf
|
|
8
|
+
class Generator
|
|
9
|
+
def self.generate(definitions)
|
|
10
|
+
new(definitions).generate
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def initialize(definitions)
|
|
14
|
+
@definitions = Array(definitions)
|
|
15
|
+
@tables = Hash.new { |h, k| h[k] = {} }
|
|
16
|
+
@pk_generators = {}
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def generate
|
|
20
|
+
@definitions.each do |definition|
|
|
21
|
+
generate_root(definition)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
Dataset.new(@tables)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def pk_generator_for(definition)
|
|
30
|
+
@pk_generators[definition.table_name] ||= PrimaryKey.build(definition.pk_spec)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def generate_root(definition)
|
|
34
|
+
combos = definition.matrix.combinations
|
|
35
|
+
pk_gen = pk_generator_for(definition)
|
|
36
|
+
|
|
37
|
+
combos.each_with_index do |combo, index|
|
|
38
|
+
label = Labeler.generate(definition.singular_name, combo)
|
|
39
|
+
pk = pk_gen.next_id(label, combo)
|
|
40
|
+
|
|
41
|
+
attributes = build_attributes(
|
|
42
|
+
definition: definition,
|
|
43
|
+
combo: combo,
|
|
44
|
+
label: label,
|
|
45
|
+
pk: pk,
|
|
46
|
+
sequence: index + 1
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
@tables[definition.table_name][label] = attributes
|
|
50
|
+
|
|
51
|
+
generate_associations(definition, attributes, label, pk)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def generate_associations(definition, parent_attrs, parent_label, parent_pk)
|
|
56
|
+
definition.associations.each do |assoc|
|
|
57
|
+
assoc_def = assoc.definition
|
|
58
|
+
pk_gen = pk_generator_for(assoc_def)
|
|
59
|
+
fk_name = assoc.foreign_key || definition.foreign_key_name
|
|
60
|
+
child_name = assoc_def.singular_name
|
|
61
|
+
|
|
62
|
+
if assoc.has_one?
|
|
63
|
+
combo = assoc_def.matrix.combinations.first || {}
|
|
64
|
+
label = Labeler.generate(child_name, combo, parent_label: parent_label)
|
|
65
|
+
pk = pk_gen.next_id(label, combo)
|
|
66
|
+
|
|
67
|
+
child_attrs = build_attributes(
|
|
68
|
+
definition: assoc_def,
|
|
69
|
+
combo: combo,
|
|
70
|
+
label: label,
|
|
71
|
+
pk: pk,
|
|
72
|
+
parent_attrs: parent_attrs,
|
|
73
|
+
parent_label: parent_label,
|
|
74
|
+
parent_pk: parent_pk,
|
|
75
|
+
foreign_key_name: fk_name,
|
|
76
|
+
sequence: 1
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
@tables[assoc_def.table_name][label] = child_attrs
|
|
80
|
+
generate_associations(assoc_def, child_attrs, label, pk)
|
|
81
|
+
|
|
82
|
+
elsif assoc.has_many?
|
|
83
|
+
combos = assoc_def.matrix.combinations
|
|
84
|
+
|
|
85
|
+
if assoc.mode == :count && assoc.count
|
|
86
|
+
combos_to_generate = if combos.empty?
|
|
87
|
+
Array.new(assoc.count) { {} }
|
|
88
|
+
else
|
|
89
|
+
combos.cycle.take(assoc.count)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
combos_to_generate.each_with_index do |combo, idx|
|
|
93
|
+
seq = idx + 1
|
|
94
|
+
label = Labeler.generate(child_name, combo, parent_label: parent_label, sequence: seq)
|
|
95
|
+
pk = pk_gen.next_id(label, combo)
|
|
96
|
+
|
|
97
|
+
child_attrs = build_attributes(
|
|
98
|
+
definition: assoc_def,
|
|
99
|
+
combo: combo,
|
|
100
|
+
label: label,
|
|
101
|
+
pk: pk,
|
|
102
|
+
parent_attrs: parent_attrs,
|
|
103
|
+
parent_label: parent_label,
|
|
104
|
+
parent_pk: parent_pk,
|
|
105
|
+
foreign_key_name: fk_name,
|
|
106
|
+
sequence: seq
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
@tables[assoc_def.table_name][label] = child_attrs
|
|
110
|
+
generate_associations(assoc_def, child_attrs, label, pk)
|
|
111
|
+
end
|
|
112
|
+
else
|
|
113
|
+
# :combinations mode (Default)
|
|
114
|
+
combos.each_with_index do |combo, idx|
|
|
115
|
+
label = Labeler.generate(child_name, combo, parent_label: parent_label)
|
|
116
|
+
pk = pk_gen.next_id(label, combo)
|
|
117
|
+
|
|
118
|
+
child_attrs = build_attributes(
|
|
119
|
+
definition: assoc_def,
|
|
120
|
+
combo: combo,
|
|
121
|
+
label: label,
|
|
122
|
+
pk: pk,
|
|
123
|
+
parent_attrs: parent_attrs,
|
|
124
|
+
parent_label: parent_label,
|
|
125
|
+
parent_pk: parent_pk,
|
|
126
|
+
foreign_key_name: fk_name,
|
|
127
|
+
sequence: idx + 1
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
@tables[assoc_def.table_name][label] = child_attrs
|
|
131
|
+
generate_associations(assoc_def, child_attrs, label, pk)
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def build_attributes(definition:, combo:, label:, pk:, sequence:, parent_attrs: nil, parent_label: nil, parent_pk: nil, foreign_key_name: nil)
|
|
139
|
+
context = {
|
|
140
|
+
"id" => pk,
|
|
141
|
+
"label" => label,
|
|
142
|
+
"seq" => sequence
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if parent_label
|
|
146
|
+
context["parent.label"] = parent_label
|
|
147
|
+
context["parent.id"] = parent_pk
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# Add combo attributes into context
|
|
151
|
+
combo.each do |k, v|
|
|
152
|
+
context[k.to_s] = v
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
attributes = { "id" => pk }
|
|
156
|
+
attributes[foreign_key_name] = parent_pk if foreign_key_name && parent_pk
|
|
157
|
+
|
|
158
|
+
# Merge defaults with interpolation
|
|
159
|
+
definition.defaults.each do |key, val|
|
|
160
|
+
attributes[key.to_s] = resolve_value(val, context)
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# Matrix values override defaults
|
|
164
|
+
combo.each do |key, val|
|
|
165
|
+
attributes[key.to_s] = val
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
attributes
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def resolve_value(val, context)
|
|
172
|
+
case val
|
|
173
|
+
when String
|
|
174
|
+
interpolate(val, context)
|
|
175
|
+
when Proc
|
|
176
|
+
val.arity == 0 ? val.call : val.call(context)
|
|
177
|
+
else
|
|
178
|
+
val
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def interpolate(template, context)
|
|
183
|
+
template.gsub(/%\{([^}]+)\}/) do
|
|
184
|
+
key = Regexp.last_match(1)
|
|
185
|
+
if context.key?(key)
|
|
186
|
+
context[key].to_s
|
|
187
|
+
else
|
|
188
|
+
"%{#{key}}"
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
end
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Astroturf
|
|
4
|
+
module Inflector
|
|
5
|
+
PLURAL_TO_SINGULAR = [
|
|
6
|
+
[/(quiz)zes$/i, '\1'],
|
|
7
|
+
[/(matr)ices$/i, '\1ix'],
|
|
8
|
+
[/(vert|ind)ices$/i, '\1ex'],
|
|
9
|
+
[/^(ox)en$/i, '\1'],
|
|
10
|
+
[/(alias|status)es$/i, '\1'],
|
|
11
|
+
[/(octop|vir)i$/i, '\1us'],
|
|
12
|
+
[/(cris|ax|test)es$/i, '\1is'],
|
|
13
|
+
[/(shoe)s$/i, '\1'],
|
|
14
|
+
[/(o)es$/i, '\1'],
|
|
15
|
+
[/(bus)es$/i, '\1'],
|
|
16
|
+
[/([m|l])ice$/i, '\1ouse'],
|
|
17
|
+
[/(x|ch|ss|sh)es$/i, '\1'],
|
|
18
|
+
[/(m)ovies$/i, '\1ovie'],
|
|
19
|
+
[/(s)eries$/i, '\1eries'],
|
|
20
|
+
[/([^aeiouy]|qu)ies$/i, '\1y'],
|
|
21
|
+
[/([lr])ves$/i, '\1f'],
|
|
22
|
+
[/(tive)s$/i, '\1'],
|
|
23
|
+
[/(hive)s$/i, '\1'],
|
|
24
|
+
[/(li|wi|kni)ves$/i, '\1fe'],
|
|
25
|
+
[/(shea|loa|lea|thie)ves$/i, '\1f'],
|
|
26
|
+
[/(^analy)ses$/i, '\1sis'],
|
|
27
|
+
[/((a)naly|(b)a|(d)iagno|(p)arenthe|(p)rogno|(s)ynop|(t)he)ses$/i, '\1\2sis'],
|
|
28
|
+
[/([ti])a$/i, '\1um'],
|
|
29
|
+
[/(n)ews$/i, '\1ews'],
|
|
30
|
+
[/(h|j|k|l|m|n|p|q|r|s|t|v|w|x|z)zes$/i, '\1'],
|
|
31
|
+
[/s$/i, ""]
|
|
32
|
+
].freeze
|
|
33
|
+
|
|
34
|
+
SINGULAR_TO_PLURAL = [
|
|
35
|
+
[/(quiz)$/i, '\1zes'],
|
|
36
|
+
[/^(ox)$/i, '\1en'],
|
|
37
|
+
[/([m|l])ouse$/i, '\1ice'],
|
|
38
|
+
[/(matr|vert|ind)ix|ex$/i, '\1ices'],
|
|
39
|
+
[/(x|ch|ss|sh)$/i, '\1es'],
|
|
40
|
+
[/([^aeiouy]|qu)y$/i, '\1ies'],
|
|
41
|
+
[/(hive)$/i, '\1s'],
|
|
42
|
+
[/(?:([^f])fe|([lr])f)$/i, '\1\2ves'],
|
|
43
|
+
[/sis$/i, "ses"],
|
|
44
|
+
[/([ti])um$/i, '\1a'],
|
|
45
|
+
[/(buffal|tomat)o$/i, '\1oes'],
|
|
46
|
+
[/(bu)s$/i, '\1ses'],
|
|
47
|
+
[/(alias|status)$/i, '\1es'],
|
|
48
|
+
[/(octop|vir)us$/i, '\1i'],
|
|
49
|
+
[/(ax|test)is$/i, '\1es'],
|
|
50
|
+
[/s$/i, "s"],
|
|
51
|
+
[/$/i, "s"]
|
|
52
|
+
].freeze
|
|
53
|
+
|
|
54
|
+
IRREGULAR = {
|
|
55
|
+
"person" => "people",
|
|
56
|
+
"man" => "men",
|
|
57
|
+
"child" => "children",
|
|
58
|
+
"sex" => "sexes",
|
|
59
|
+
"move" => "moves",
|
|
60
|
+
"zombie" => "zombies"
|
|
61
|
+
}.freeze
|
|
62
|
+
|
|
63
|
+
def self.singularize(word)
|
|
64
|
+
str = word.to_s
|
|
65
|
+
return str if str.empty?
|
|
66
|
+
|
|
67
|
+
IRREGULAR.each do |singular, plural|
|
|
68
|
+
return singular.dup if str.downcase == plural
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
PLURAL_TO_SINGULAR.each do |rule, replacement|
|
|
72
|
+
if str =~ rule
|
|
73
|
+
return str.sub(rule, replacement)
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
str
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def self.pluralize(word)
|
|
81
|
+
str = word.to_s
|
|
82
|
+
return str if str.empty?
|
|
83
|
+
|
|
84
|
+
IRREGULAR.each do |singular, plural|
|
|
85
|
+
return plural.dup if str.downcase == singular
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
SINGULAR_TO_PLURAL.each do |rule, replacement|
|
|
89
|
+
if str =~ rule
|
|
90
|
+
return str.sub(rule, replacement)
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
"#{str}s"
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def self.tableize(word)
|
|
98
|
+
pluralize(underscore(word))
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def self.underscore(word)
|
|
102
|
+
word.to_s
|
|
103
|
+
.gsub(/([A-Z\d]+)([A-Z][a-z])/, '\1_\2')
|
|
104
|
+
.gsub(/([a-z\d])([A-Z])/, '\1_\2')
|
|
105
|
+
.tr("-", "_")
|
|
106
|
+
.downcase
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Astroturf
|
|
4
|
+
class Labeler
|
|
5
|
+
def self.generate(entity_name, coordinates = {}, parent_label: nil, sequence: nil)
|
|
6
|
+
new.generate(entity_name, coordinates, parent_label: parent_label, sequence: sequence)
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def generate(entity_name, coordinates = {}, parent_label: nil, sequence: nil)
|
|
10
|
+
parts = []
|
|
11
|
+
parts << sanitize(parent_label) if parent_label && !parent_label.empty?
|
|
12
|
+
parts << sanitize(entity_name)
|
|
13
|
+
|
|
14
|
+
if coordinates && !coordinates.empty?
|
|
15
|
+
coordinates.each do |_key, val|
|
|
16
|
+
parts << sanitize(format_value(val))
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
parts << sequence.to_s if sequence
|
|
21
|
+
|
|
22
|
+
parts.reject(&:empty?).join("_")
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def sanitize(value)
|
|
26
|
+
str = value.to_s.strip
|
|
27
|
+
# Replace spaces, hyphens, and non-alphanumeric chars with underscore
|
|
28
|
+
str = str.gsub(/[^a-zA-Z0-9]+/, "_")
|
|
29
|
+
# Collapse multiple underscores
|
|
30
|
+
str = str.gsub(/_+/, "_")
|
|
31
|
+
# Strip leading/trailing underscores
|
|
32
|
+
str = str.gsub(/\A_+|_+\z/, "")
|
|
33
|
+
str.downcase
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def format_value(val)
|
|
39
|
+
case val
|
|
40
|
+
when nil
|
|
41
|
+
"nil"
|
|
42
|
+
when true
|
|
43
|
+
"true"
|
|
44
|
+
when false
|
|
45
|
+
"false"
|
|
46
|
+
else
|
|
47
|
+
val.to_s
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "errors"
|
|
4
|
+
|
|
5
|
+
module Astroturf
|
|
6
|
+
class Matrix
|
|
7
|
+
attr_reader :dimensions
|
|
8
|
+
|
|
9
|
+
def self.combinations(dimensions)
|
|
10
|
+
new(dimensions).combinations
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def initialize(dimensions = {})
|
|
14
|
+
@dimensions = normalize_dimensions(dimensions || {})
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def combinations
|
|
18
|
+
return [{}] if @dimensions.empty?
|
|
19
|
+
|
|
20
|
+
keys = @dimensions.keys
|
|
21
|
+
value_arrays = @dimensions.values
|
|
22
|
+
|
|
23
|
+
return [] if value_arrays.any?(&:empty?)
|
|
24
|
+
|
|
25
|
+
value_arrays.first.product(*value_arrays[1..-1]).map do |tuple|
|
|
26
|
+
keys.zip(tuple).to_h
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def size
|
|
31
|
+
return 1 if @dimensions.empty?
|
|
32
|
+
return 0 if @dimensions.values.any?(&:empty?)
|
|
33
|
+
|
|
34
|
+
@dimensions.values.map(&:size).inject(1, :*)
|
|
35
|
+
end
|
|
36
|
+
alias_method :count, :size
|
|
37
|
+
|
|
38
|
+
def empty?
|
|
39
|
+
size.zero?
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
def normalize_dimensions(raw_dimensions)
|
|
45
|
+
raise InvalidMatrixError, "Matrix dimensions must be a Hash" unless raw_dimensions.is_a?(Hash)
|
|
46
|
+
|
|
47
|
+
raw_dimensions.each_with_object({}) do |(key, values), hash|
|
|
48
|
+
hash[key.to_s] = case values
|
|
49
|
+
when Range
|
|
50
|
+
values.to_a
|
|
51
|
+
when Array
|
|
52
|
+
values
|
|
53
|
+
else
|
|
54
|
+
[values]
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Astroturf
|
|
4
|
+
module PrimaryKey
|
|
5
|
+
class IntegerSequence
|
|
6
|
+
attr_reader :current, :start_at, :step
|
|
7
|
+
|
|
8
|
+
def initialize(start_at: 1, step: 1)
|
|
9
|
+
@start_at = start_at
|
|
10
|
+
@step = step
|
|
11
|
+
@current = start_at
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def next_id(_label = nil, _context = {})
|
|
15
|
+
val = @current
|
|
16
|
+
@current += @step
|
|
17
|
+
val
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def reset!
|
|
21
|
+
@current = @start_at
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "digest"
|
|
4
|
+
|
|
5
|
+
module Astroturf
|
|
6
|
+
module PrimaryKey
|
|
7
|
+
class Uuid
|
|
8
|
+
DEFAULT_NAMESPACE = "astroturf"
|
|
9
|
+
|
|
10
|
+
attr_reader :namespace
|
|
11
|
+
|
|
12
|
+
def initialize(namespace: DEFAULT_NAMESPACE)
|
|
13
|
+
@namespace = namespace
|
|
14
|
+
@counter = 0
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def next_id(label = nil, _context = {})
|
|
18
|
+
@counter += 1
|
|
19
|
+
name = label ? "#{label}" : "record_#{@counter}"
|
|
20
|
+
generate_uuid_v5(@namespace, name)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def reset!
|
|
24
|
+
@counter = 0
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
# Generates a standard RFC 4122 compliant UUIDv5 using SHA-1
|
|
30
|
+
def generate_uuid_v5(ns, name)
|
|
31
|
+
hash = Digest::SHA1.hexdigest("#{ns}:#{name}")
|
|
32
|
+
time_low = hash[0...8]
|
|
33
|
+
time_mid = hash[8...12]
|
|
34
|
+
time_hi = ((hash[12...16].to_i(16) & 0x0fff) | 0x5000).to_s(16).rjust(4, "0")
|
|
35
|
+
clock_seq = ((hash[16...20].to_i(16) & 0x3fff) | 0x8000).to_s(16).rjust(4, "0")
|
|
36
|
+
node = hash[20...32]
|
|
37
|
+
|
|
38
|
+
"#{time_low}-#{time_mid}-#{time_hi}-#{clock_seq}-#{node}"
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "primary_key/integer_sequence"
|
|
4
|
+
require_relative "primary_key/uuid"
|
|
5
|
+
require_relative "errors"
|
|
6
|
+
|
|
7
|
+
module Astroturf
|
|
8
|
+
module PrimaryKey
|
|
9
|
+
def self.build(spec = :integer)
|
|
10
|
+
case spec
|
|
11
|
+
when :integer, "integer", nil
|
|
12
|
+
IntegerSequence.new
|
|
13
|
+
when :uuid, "uuid"
|
|
14
|
+
Uuid.new
|
|
15
|
+
when Hash
|
|
16
|
+
build_from_hash(spec)
|
|
17
|
+
else
|
|
18
|
+
if spec.respond_to?(:next_id)
|
|
19
|
+
spec
|
|
20
|
+
elsif spec.respond_to?(:call)
|
|
21
|
+
CallableAdapter.new(spec)
|
|
22
|
+
else
|
|
23
|
+
raise UnknownPrimaryKeyStrategyError, "Unsupported primary key strategy: #{spec.inspect}"
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def self.build_from_hash(hash)
|
|
29
|
+
strategy = (hash[:type] || hash["type"] || :integer).to_s
|
|
30
|
+
case strategy
|
|
31
|
+
when "integer"
|
|
32
|
+
start_at = hash[:start_at] || hash["start_at"] || 1
|
|
33
|
+
step = hash[:step] || hash["step"] || 1
|
|
34
|
+
IntegerSequence.new(start_at: start_at.to_i, step: step.to_i)
|
|
35
|
+
when "uuid"
|
|
36
|
+
namespace = hash[:namespace] || hash["namespace"] || Uuid::DEFAULT_NAMESPACE
|
|
37
|
+
Uuid.new(namespace: namespace.to_s)
|
|
38
|
+
else
|
|
39
|
+
raise UnknownPrimaryKeyStrategyError, "Unsupported primary key strategy: #{strategy.inspect}"
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
class CallableAdapter
|
|
44
|
+
def initialize(callable)
|
|
45
|
+
@callable = callable
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def next_id(label = nil, context = {})
|
|
49
|
+
if @callable.arity == 0
|
|
50
|
+
@callable.call
|
|
51
|
+
elsif @callable.arity == 1
|
|
52
|
+
@callable.call(label)
|
|
53
|
+
else
|
|
54
|
+
@callable.call(label, context)
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def reset!
|
|
59
|
+
@callable.reset! if @callable.respond_to?(:reset!)
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
data/lib/astroturf.rb
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "yaml"
|
|
4
|
+
require "date"
|
|
5
|
+
require "time"
|
|
6
|
+
require_relative "astroturf/version"
|
|
7
|
+
require_relative "astroturf/errors"
|
|
8
|
+
require_relative "astroturf/matrix"
|
|
9
|
+
require_relative "astroturf/primary_key"
|
|
10
|
+
require_relative "astroturf/labeler"
|
|
11
|
+
require_relative "astroturf/definition"
|
|
12
|
+
require_relative "astroturf/dataset"
|
|
13
|
+
require_relative "astroturf/generator"
|
|
14
|
+
|
|
15
|
+
module Astroturf
|
|
16
|
+
def self.load_file(path)
|
|
17
|
+
content = File.read(path)
|
|
18
|
+
load(content)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def self.load(yaml_or_hash)
|
|
22
|
+
hash = case yaml_or_hash
|
|
23
|
+
when String
|
|
24
|
+
YAML.safe_load(yaml_or_hash, permitted_classes: [Date, Time, Symbol], aliases: true)
|
|
25
|
+
when Hash
|
|
26
|
+
yaml_or_hash
|
|
27
|
+
else
|
|
28
|
+
raise ConfigurationError, "Expected YAML string or Hash, got #{yaml_or_hash.class}"
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
definitions = Definition.from_hash(hash)
|
|
32
|
+
Generator.generate(definitions)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
class << self
|
|
36
|
+
alias_method :generate, :load
|
|
37
|
+
alias_method :build, :load
|
|
38
|
+
end
|
|
39
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: astroturf
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Jeff Lunt
|
|
8
|
+
bindir: exe
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: minitest
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '5.0'
|
|
19
|
+
type: :development
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '5.0'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: rake
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - "~>"
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '12.0'
|
|
33
|
+
type: :development
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - "~>"
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '12.0'
|
|
40
|
+
description: Astroturf generates combinatorial synthetic data graphs in memory from
|
|
41
|
+
declarative YAML matrix specifications.
|
|
42
|
+
email:
|
|
43
|
+
- jefflunt@gmail.com
|
|
44
|
+
executables: []
|
|
45
|
+
extensions: []
|
|
46
|
+
extra_rdoc_files: []
|
|
47
|
+
files:
|
|
48
|
+
- ".gitignore"
|
|
49
|
+
- Gemfile
|
|
50
|
+
- LICENSE.txt
|
|
51
|
+
- README.md
|
|
52
|
+
- Rakefile
|
|
53
|
+
- astroturf.gemspec
|
|
54
|
+
- lib/astroturf.rb
|
|
55
|
+
- lib/astroturf/dataset.rb
|
|
56
|
+
- lib/astroturf/definition.rb
|
|
57
|
+
- lib/astroturf/errors.rb
|
|
58
|
+
- lib/astroturf/generator.rb
|
|
59
|
+
- lib/astroturf/inflector.rb
|
|
60
|
+
- lib/astroturf/labeler.rb
|
|
61
|
+
- lib/astroturf/matrix.rb
|
|
62
|
+
- lib/astroturf/primary_key.rb
|
|
63
|
+
- lib/astroturf/primary_key/integer_sequence.rb
|
|
64
|
+
- lib/astroturf/primary_key/uuid.rb
|
|
65
|
+
- lib/astroturf/version.rb
|
|
66
|
+
homepage: https://github.com/elabs/astroturf
|
|
67
|
+
licenses:
|
|
68
|
+
- MIT
|
|
69
|
+
metadata:
|
|
70
|
+
source_code_uri: https://github.com/elabs/astroturf
|
|
71
|
+
rdoc_options: []
|
|
72
|
+
require_paths:
|
|
73
|
+
- lib
|
|
74
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
75
|
+
requirements:
|
|
76
|
+
- - ">="
|
|
77
|
+
- !ruby/object:Gem::Version
|
|
78
|
+
version: 2.6.0
|
|
79
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
80
|
+
requirements:
|
|
81
|
+
- - ">="
|
|
82
|
+
- !ruby/object:Gem::Version
|
|
83
|
+
version: '0'
|
|
84
|
+
requirements: []
|
|
85
|
+
rubygems_version: 4.0.16
|
|
86
|
+
specification_version: 4
|
|
87
|
+
summary: Exhaustive synthetic data generator for Rails and Ruby applications
|
|
88
|
+
test_files: []
|