hirsute 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/MIT_LICENSE +7 -0
- data/README.md +105 -0
- data/bin/hirsute +14 -0
- data/lib/hirsute.rb +110 -0
- data/lib/hirsute_collection.rb +67 -0
- data/lib/hirsute_constraint.rb +17 -0
- data/lib/hirsute_fixed.rb +17 -0
- data/lib/hirsute_generator.rb +140 -0
- data/lib/hirsute_make_generators.rb +125 -0
- data/lib/hirsute_output.rb +142 -0
- data/lib/hirsute_template.rb +159 -0
- data/lib/hirsute_test.rb +25 -0
- data/lib/hirsute_utils.rb +108 -0
- data/lib/histoparse.rb +47 -0
- data/manual.md +184 -0
- data/samples/readme.hrs +44 -0
- data/samples/wine_cellar.hrs +145 -0
- data/tests/first_names.txt +8 -0
- data/tests/hirsute_test.rb +362 -0
- data/tests/histoparse_test.rb +32 -0
- metadata +72 -0
@@ -0,0 +1,125 @@
|
|
1
|
+
# the various commands that make Hirsute generators. This mostly just keeps these methods isolated
|
2
|
+
|
3
|
+
require('lib/hirsute_generator.rb')
|
4
|
+
require('lib/hirsute_utils.rb')
|
5
|
+
require('lib/histoparse.rb')
|
6
|
+
|
7
|
+
module Hirsute
|
8
|
+
DEFAULT = :hirsute_default
|
9
|
+
module GeneratorMakers
|
10
|
+
include Hirsute::Support
|
11
|
+
include Hirsute::HistoParse
|
12
|
+
|
13
|
+
public
|
14
|
+
# A generator that increments values from a starting point. Good for IDs
|
15
|
+
def counter(startingPoint=1,&block)
|
16
|
+
gen_make_generator(block) {@current = startingPoint;def _generate(onObj); cur_current = @current; @current = @current + 1; cur_current; end;}
|
17
|
+
end
|
18
|
+
|
19
|
+
# A generator that combines all the generators passed in.
|
20
|
+
def combination(*args,&block)
|
21
|
+
CompoundGenerator.new(args.map {|item| generator_from_value(item)},block)
|
22
|
+
end
|
23
|
+
|
24
|
+
# given a list of generators, pick between 1 and n of them and return a CompoundGenerator based off the subset
|
25
|
+
def subset(*args,&block)
|
26
|
+
gen_make_generator(block) {
|
27
|
+
@generators = args.map {|item| generator_from_value(item)}
|
28
|
+
@block = block
|
29
|
+
def _generate(onObj)
|
30
|
+
count = rand(@generators.length)
|
31
|
+
subset = @generators[0..count]
|
32
|
+
|
33
|
+
CompoundGenerator.new(subset,@block)
|
34
|
+
end
|
35
|
+
}
|
36
|
+
end
|
37
|
+
|
38
|
+
# pick one of the itmes in the list randomly
|
39
|
+
def one_of (list,histogram = nil,&block)
|
40
|
+
if !histogram
|
41
|
+
gen_make_generator(block) {@options = list; def _generate(onObj); @options.choice; end;}
|
42
|
+
else
|
43
|
+
histogram_buckets = histogram
|
44
|
+
histogram_buckets = parse_histogram(histogram).histogram_buckets if histogram.kind_of? String
|
45
|
+
|
46
|
+
gen_make_generator(block) {
|
47
|
+
@options = list
|
48
|
+
@histogram = histogram_buckets
|
49
|
+
def _generate(onObj)
|
50
|
+
random_item_with_histogram(@options,@histogram)
|
51
|
+
end
|
52
|
+
}
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
# reads from file, using each line as the result of the generation
|
57
|
+
# algorithm defines the style of reading lines. The default is :markov
|
58
|
+
# which picks a random number, reads n lines in, returns that, and then
|
59
|
+
# picks another random number and reads n more lines in
|
60
|
+
def read_from_file(file_name,algorithm=:markov,&block)
|
61
|
+
ReadFromFileGenerator.new(file_name,algorithm,block)
|
62
|
+
end
|
63
|
+
|
64
|
+
# pull items in sequence from an array. once it reaches the end, reset
|
65
|
+
def read_from_sequence(array,&block)
|
66
|
+
raise "List must have at least one item" if array.length == 0
|
67
|
+
gen_make_generator(block) {
|
68
|
+
@items = array
|
69
|
+
@index = 0
|
70
|
+
|
71
|
+
def _generate(onObj)
|
72
|
+
item = @items[@index]
|
73
|
+
if item
|
74
|
+
@index = @index + 1
|
75
|
+
item
|
76
|
+
else
|
77
|
+
@index = 0
|
78
|
+
generate(onObj)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
}
|
82
|
+
end
|
83
|
+
|
84
|
+
# fork to different generators depending on the value of a particular field within the object
|
85
|
+
# a value of DEFAULT can be used as a catch-all. If DEFAULT is not defined, a field value that isn't defined will cause
|
86
|
+
# nil to be returned
|
87
|
+
def depending_on(field,options,&block)
|
88
|
+
gen = DependentGenerator.new(field,block)
|
89
|
+
gen.instance_eval {
|
90
|
+
@options = options
|
91
|
+
@field = field
|
92
|
+
|
93
|
+
# note that this generator won't be activated until the field we want is set
|
94
|
+
def _generate(onObj)
|
95
|
+
field_value = onObj.get(@field)
|
96
|
+
if @options[field_value]
|
97
|
+
@options[field_value]
|
98
|
+
elsif @options[DEFAULT]
|
99
|
+
@options[DEFAULT]
|
100
|
+
else
|
101
|
+
nil
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
}
|
106
|
+
gen
|
107
|
+
end
|
108
|
+
|
109
|
+
# Force a generator to not run until the given fields are set
|
110
|
+
def requires_field(field,generator,&block)
|
111
|
+
# this leverages existing depending_on behavior, just passing a set of options with one path
|
112
|
+
options = {DEFAULT => generator}
|
113
|
+
depending_on(field,options,&block)
|
114
|
+
end
|
115
|
+
|
116
|
+
private
|
117
|
+
# generic method for making a generator based off of a block. useful for simple cases.
|
118
|
+
def gen_make_generator(finishProc=nil,&block)
|
119
|
+
gen = Generator.new(finishProc)
|
120
|
+
gen.instance_eval(&block)
|
121
|
+
gen
|
122
|
+
end
|
123
|
+
|
124
|
+
end
|
125
|
+
end
|
@@ -0,0 +1,142 @@
|
|
1
|
+
# Defines output modules that can translate Hirsute objects into load formats for various systems
|
2
|
+
require 'lib/hirsute_utils.rb'
|
3
|
+
|
4
|
+
module Hirsute
|
5
|
+
|
6
|
+
include Support
|
7
|
+
|
8
|
+
# base class for working with objects
|
9
|
+
class Outputter
|
10
|
+
|
11
|
+
attr_accessor :fields
|
12
|
+
|
13
|
+
def initialize(collection,options=Hash.new)
|
14
|
+
@collection = collection
|
15
|
+
@obj_class = Hirsute::Support.class_for_name(@collection.object_name)
|
16
|
+
@fields = @obj_class.field_order
|
17
|
+
@options = options
|
18
|
+
end
|
19
|
+
|
20
|
+
def get_storage_option(option,default=nil)
|
21
|
+
return default if !@options
|
22
|
+
|
23
|
+
retVal = @options[option]
|
24
|
+
if retVal != nil
|
25
|
+
retVal
|
26
|
+
else
|
27
|
+
default
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
# allows the outputter to do any preliminary work
|
32
|
+
def start
|
33
|
+
_start
|
34
|
+
end
|
35
|
+
|
36
|
+
def _start;end;
|
37
|
+
|
38
|
+
# cleanup work
|
39
|
+
def finish
|
40
|
+
_finish
|
41
|
+
end
|
42
|
+
|
43
|
+
def _finish;end;
|
44
|
+
|
45
|
+
def get_file(object_name)
|
46
|
+
object_name + ".load"
|
47
|
+
end
|
48
|
+
|
49
|
+
# external method telling
|
50
|
+
def output
|
51
|
+
#derive file name from class of object
|
52
|
+
|
53
|
+
begin
|
54
|
+
@file = File.open(get_file(@collection.object_name),'w')
|
55
|
+
start
|
56
|
+
|
57
|
+
@collection.each {|item| _outputItem(item)}
|
58
|
+
|
59
|
+
finish
|
60
|
+
rescue Exception => e
|
61
|
+
puts "Error #{e}"
|
62
|
+
ensure
|
63
|
+
@file.close if !@file.nil?
|
64
|
+
end
|
65
|
+
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
|
70
|
+
class MySQLOutputter < Outputter
|
71
|
+
|
72
|
+
MySQLOutputter::DEFAULT_MAX_PACKET = 1048576
|
73
|
+
|
74
|
+
def _start
|
75
|
+
@cur_statement = insertStringBase
|
76
|
+
end
|
77
|
+
|
78
|
+
def _outputItem(item)
|
79
|
+
# add VALUES(...) to existing string only if the existing string plus the values line is smaller than mox_allowed_packet
|
80
|
+
|
81
|
+
value_string = " VALUES (" + @fields.map{|column| object_value_to_sql_literal(item.get(column))}.join(",") + "),"
|
82
|
+
|
83
|
+
if @cur_statement.length + value_string.length > get_storage_option(:max_allowed_packet,MySQLOutputter::DEFAULT_MAX_PACKET)
|
84
|
+
# the current statement plus the addition would be too large. so output current statement, reset, start again
|
85
|
+
output_current
|
86
|
+
@cur_statement = insertStringBase
|
87
|
+
end
|
88
|
+
|
89
|
+
@cur_statement << value_string
|
90
|
+
end
|
91
|
+
|
92
|
+
def _finish
|
93
|
+
output_current # flush the last entry
|
94
|
+
end
|
95
|
+
|
96
|
+
private
|
97
|
+
|
98
|
+
def output_current
|
99
|
+
@file.puts(@cur_statement[0...-1] << ";\n") # trim the tailing comma that comes from the last value_string
|
100
|
+
end
|
101
|
+
|
102
|
+
def insertStringBase
|
103
|
+
"INSERT INTO #{@obj_class.storage_name} (" +
|
104
|
+
@fields.map{|column| column.to_s}.join(",") +
|
105
|
+
") "
|
106
|
+
end
|
107
|
+
|
108
|
+
# convenience method for getting a SQL representation of a ruby object
|
109
|
+
def object_value_to_sql_literal(value)
|
110
|
+
if value.nil?
|
111
|
+
"NULL"
|
112
|
+
elsif value.kind_of? Numeric
|
113
|
+
value.to_s
|
114
|
+
else
|
115
|
+
"'#{value}'"
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
end
|
120
|
+
|
121
|
+
class CSVOutputter < Outputter
|
122
|
+
def separator
|
123
|
+
get_storage_option(:separator,",")
|
124
|
+
end
|
125
|
+
|
126
|
+
def get_file(name)
|
127
|
+
name + ".csv"
|
128
|
+
end
|
129
|
+
|
130
|
+
def _start
|
131
|
+
#output header
|
132
|
+
header = @fields.map{|field| "\"#{field}\""}.join(separator)
|
133
|
+
@file.puts header
|
134
|
+
end
|
135
|
+
|
136
|
+
def _outputItem(item)
|
137
|
+
line = @fields.map {|field| "\"#{item.send(field)}\""}.join(separator)
|
138
|
+
@file.puts line
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
end
|
@@ -0,0 +1,159 @@
|
|
1
|
+
# Defines the Template class that forms the foundation of Hirsute object definitions
|
2
|
+
|
3
|
+
require('lib/hirsute_generator.rb')
|
4
|
+
require('lib/hirsute_make_generators.rb')
|
5
|
+
require('lib/hirsute_fixed.rb')
|
6
|
+
require('lib/hirsute_collection.rb')
|
7
|
+
require('lib/hirsute_utils.rb')
|
8
|
+
|
9
|
+
module Hirsute
|
10
|
+
class Template
|
11
|
+
include GeneratorMakers
|
12
|
+
include Support
|
13
|
+
|
14
|
+
public
|
15
|
+
def initialize(templateName)
|
16
|
+
@templateName = templateName
|
17
|
+
end
|
18
|
+
|
19
|
+
attr_reader :templateName
|
20
|
+
|
21
|
+
# has takes a hash of field name -> field generator definitions and stores them
|
22
|
+
# for later use
|
23
|
+
# remembering that the syntax is
|
24
|
+
# has
|
25
|
+
# "id" => counter(1)
|
26
|
+
# this means that counter must be a method in this class
|
27
|
+
def has(fieldDefs)
|
28
|
+
@fieldDefs = Hash.new
|
29
|
+
|
30
|
+
# do this in a loop to have special handling for different types
|
31
|
+
fieldDefs.each_pair {|key,value| @fieldDefs[key] = generator_from_value(value)}
|
32
|
+
|
33
|
+
# define accessors for each of the fields defined in the template
|
34
|
+
hashToFields(fieldDefs)
|
35
|
+
|
36
|
+
# add a fields field to the class _instance_ (note that in has, no instances of the object itself yet exist)
|
37
|
+
class_for_name(@templateName).instance_eval {
|
38
|
+
@fields = fieldDefs.keys
|
39
|
+
def fields; @fields; end;
|
40
|
+
|
41
|
+
def field_order;@fieldOrder.nil? ? @fields : @fieldOrder;end;
|
42
|
+
}
|
43
|
+
end
|
44
|
+
|
45
|
+
# specify an order for the fields to be output. defaults to @fields
|
46
|
+
def in_this_order(field_order)
|
47
|
+
class_for_name(@templateName).instance_eval {
|
48
|
+
@fieldOrder = field_order
|
49
|
+
}
|
50
|
+
end
|
51
|
+
|
52
|
+
# Define a set of Constraint objects that act as data integrity enforcers. For instance, if a field needs to be unique.
|
53
|
+
# Hash is defined as field_name, requirement type
|
54
|
+
# While these are defined as part of this class, they're actually copied over to the collection class, since that's who needs
|
55
|
+
# to enforce the constraint
|
56
|
+
def requires(requirements)
|
57
|
+
class_for_name(@templateName).instance_eval {
|
58
|
+
@requirements = requirements
|
59
|
+
def requirements; @requirements; end;
|
60
|
+
}
|
61
|
+
end
|
62
|
+
|
63
|
+
# Allows a template to have transient objects that are not going to be persisted to the data store
|
64
|
+
# In that case, they get added as fields within the template, but get stored separately so that they're
|
65
|
+
# not included in make
|
66
|
+
def transients(transients)
|
67
|
+
@transients = transients
|
68
|
+
hashToFields(transients)
|
69
|
+
|
70
|
+
class_for_name(@templateName).instance_eval {
|
71
|
+
@transients = transients.keys
|
72
|
+
def transients;@transients;end;
|
73
|
+
}
|
74
|
+
end
|
75
|
+
|
76
|
+
# is_stored_in defines some meaningful name for where a generated object should be stored
|
77
|
+
# in the final output (e.g., the name of a database table)
|
78
|
+
def is_stored_in(storageName)
|
79
|
+
class_for_name(@templateName).instance_eval {
|
80
|
+
@storage_name = storageName
|
81
|
+
class << self
|
82
|
+
attr_reader :storage_name
|
83
|
+
end
|
84
|
+
}
|
85
|
+
end
|
86
|
+
|
87
|
+
# makes an object based on this template definition. the
|
88
|
+
def make(addToSingleCollection=true)
|
89
|
+
fieldsAlreadySet = Hash.new(false)
|
90
|
+
dependentGenerators = Hash.new
|
91
|
+
|
92
|
+
allFields = Array.new
|
93
|
+
obj = class_for_name(@templateName).new
|
94
|
+
# populate all the fields; traverse both collections of fields at once
|
95
|
+
[@fieldDefs,@transients].each do |field_map|
|
96
|
+
next if field_map.nil?
|
97
|
+
|
98
|
+
field_map.each_pair do |fieldName,generator|
|
99
|
+
# if it's a dependent generator, check to see if the fields it's dependent on have been set
|
100
|
+
if generator.kind_of? Hirsute::DependentGenerator
|
101
|
+
if !dependent_fields_are_set?(fieldsAlreadySet,generator)
|
102
|
+
dependentGenerators[fieldName] = generator
|
103
|
+
next
|
104
|
+
end
|
105
|
+
end
|
106
|
+
obj.set(fieldName,generator.generate(obj))
|
107
|
+
fieldsAlreadySet[fieldName] = true;
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
# now handle any dependent generators left hanging and try to spot endless loops
|
112
|
+
cur_dependent_gens_length = dependentGenerators.size
|
113
|
+
while(cur_dependent_gens_length > 0)
|
114
|
+
dependentGenerators.keys.each do |field|
|
115
|
+
generator = dependentGenerators[field]
|
116
|
+
next if generator.nil?
|
117
|
+
if dependent_fields_are_set?(fieldsAlreadySet,generator)
|
118
|
+
|
119
|
+
# all dependencies are in place
|
120
|
+
obj.set(field,generator.generate(obj))
|
121
|
+
fieldsAlreadySet[field] = true
|
122
|
+
dependentGenerators.delete(field)
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
# if the size of the hash hasn't changed, we have a problem: another pass through the loop won't change things, so it'll go forever
|
127
|
+
raise "Dependency loop spotted in #{@templateName}. Check generators to make sure there are no circular dependencies." if dependentGenerators.size == cur_dependent_gens_length
|
128
|
+
cur_dependent_gens_length = dependentGenerators.size
|
129
|
+
end
|
130
|
+
|
131
|
+
# if there is exactly one collection declared for this type, add this object to it
|
132
|
+
colls = Hirsute::Collection.collections_holding_object(@templateName)
|
133
|
+
colls[0] << obj if addToSingleCollection && colls && colls.length == 1
|
134
|
+
obj
|
135
|
+
end
|
136
|
+
|
137
|
+
# makes n objects based on template and returns them as an array
|
138
|
+
def *(count)
|
139
|
+
ret_val = Collection.new(@templateName)
|
140
|
+
(1..count).each {|idx| ret_val << make(false)}
|
141
|
+
ret_val
|
142
|
+
end
|
143
|
+
|
144
|
+
private
|
145
|
+
|
146
|
+
# given a hash of values, add attr_accessors for each key
|
147
|
+
# define accessors for each of the fields defined in the template
|
148
|
+
def hashToFields(hash)
|
149
|
+
class_for_name(@templateName).class_eval {
|
150
|
+
hash.keys.each {|item| attr_accessor item.to_sym}
|
151
|
+
}
|
152
|
+
end
|
153
|
+
|
154
|
+
def dependent_fields_are_set?(fields_set,dependent_generator)
|
155
|
+
unset_fields = dependent_generator.dependency_fields.select {|fieldName| !fields_set[fieldName]}
|
156
|
+
unset_fields.length == 0
|
157
|
+
end
|
158
|
+
end
|
159
|
+
end
|
data/lib/hirsute_test.rb
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
## some simple exercises of the hirsute system
|
2
|
+
|
3
|
+
storage :mysql
|
4
|
+
|
5
|
+
a('user') {
|
6
|
+
has :user_id => counter(12),
|
7
|
+
:comment => "some random text",
|
8
|
+
:email => combination("hirsute",counter(100),"@test.com");
|
9
|
+
is_stored_in "simcity_user";
|
10
|
+
}
|
11
|
+
|
12
|
+
users = user * 5
|
13
|
+
|
14
|
+
foreach user {|cur_user|
|
15
|
+
puts cur_user.user_id
|
16
|
+
}
|
17
|
+
|
18
|
+
finish users
|
19
|
+
|
20
|
+
# pseudo-code
|
21
|
+
# users.each do |user|
|
22
|
+
# num_regions = random_number_from_histogram([.1,.2,.5,.1,.1]) # probably the middle one
|
23
|
+
# regions.filter 10 {|region| region.creator_id.is_nil>}
|
24
|
+
# regions.each {|region| region.creator_id = user.user_id}
|
25
|
+
# end
|