hirsute 0.1.0

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