hirsute 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|