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.
@@ -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
@@ -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