overhaul struct initializers, options hash only and a lot of options now supported
[lwes-ruby.git] / lib / lwes / struct.rb
blobcf0985c8d3ece7f7e2678f5bc90b1c25ff7dab4a
1 module LWES
2   class Struct
4     # creates a new Struct-based class, takes the following
5     # options hash:
6     #
7     #   :file     - pathname to the ESF file, this is always required
8     #   :class    - Ruby base class name, if the ESF file only has one
9     #               event defined (besides MetaEventInfo), then specifying
10     #               it is optional, otherwise it is required when multiple
11     #               events are defined in the same ESF :file given above
12     #   :parent   - parent class or module, the default is 'Object' putting
13     #               the new class in the global namespace.
14     #   :event    - event name if it differs from the Ruby base class name
15     #               given (or inferred) above.  For DRY-ness, you are
16     #               recommended to keep your event names and Ruby class
17     #               names in sync and not need this option.
18     #   :optional - Array of field names that are optional, the special
19     #               "MetaEventInfo" name makes all elements defined in
20     #               the MetaEventInfo section of the ESF is optional.
21     #               This may also be a regular expression.
22     #   :skip     - Array of field names to skip from the Event defininition
23     #               entirely, these could include fields that are only
24     #               implemented by the Listener.  This may also be a
25     #               regular expression.
26     #   :defaults - hash of default key -> value pairs to set at
27     #               creation time
28     #
29     def self.new(options, &block)
30       file = options[:file] or raise ArgumentError, "esf file missing"
31       test ?r, file or raise ArgumentError, "file #{file.inspect} not readable"
32       dump = TypeDB.new(file).to_hash
33       klass = options[:class] || begin
34         # make it easier to deal with single event files
35         events = (dump.keys -  [ :MetaEventInfo ])
36         events.size > 1 and
37             raise RuntimeError,
38                   "multiple event defs available: #{events.inspect}\n" \
39                   "pick one with :class"
40         events.first
41       end
43       name = options[:name] || klass
44       parent = options[:parent] || Object
45       event_def = dump[name.to_sym] or
46         raise RuntimeError, "#{name.inspect} not defined in #{file}"
48       # merge MetaEventInfo fields in
49       meta_event_info = dump[:MetaEventInfo]
50       alpha = proc { |a,b| a.first.to_s <=> b.first.to_s }
51       event_def = event_def.sort(&alpha)
52       if meta_event_info
53         seen = event_def.map { |(field, _)| field }
54         meta_event_info.sort(&alpha).each do |field_type|
55           seen.include?(field_type.first) or event_def << field_type
56         end
57       end
59       Array(options[:skip]).each do |x|
60         if Regexp === x
61           event_def.delete_if { |(f,_)| x =~ f.to_s }
62         else
63           if x.to_sym == :MetaEventInfo
64             meta_event_info.nil? and
65               raise RuntimeError, "MetaEventInfo not defined in #{file}"
66             meta_event_info.each do |(field,_)|
67               event_def.delete_if { |(f,_)| field == f }
68             end
69           else
70             event_def.delete_if { |(f,_)| f == x.to_sym }
71           end
72         end
73       end
75       tmp = ::Struct.new(*(event_def.map { |(field,_)| field }))
76       tmp = parent.const_set(klass, tmp)
77       type_db = tmp.const_set :TYPE_DB, {}
78       event_def.each { |(field,type)| type_db[field] = type }
79       tmp.const_set :TYPE_LIST, event_def
81       optional = tmp.const_set :OPTIONAL, {}
82       Array(options[:optional]).each do |x|
83         if Regexp === x
84           event_def.each { |(f,_)| optional[f] = true if x =~ f.to_s }
85         else
86           if x.to_sym == :MetaEventInfo
87             meta_event_info.nil? and
88               raise RuntimeError, "MetaEventInfo not defined in #{file}"
89             meta_event_info.each{ |(field,_)| optional[field] = true }
90           else
91             optional[x.to_sym] = true
92           end
93         end
94       end
95       defaults = options[:defaults] || {}
96       defaults = tmp.const_set :DEFAULTS, defaults.dup
98       # define a parent-level method, so instead of Event.new
99       # you could do
100       if false
101         (parent == Object ?
102           Kernel : parent.instance_eval { class << self; self; end }
103         ).__send__(:define_method, klass) do |*args|
104           if Hash === (init = args.first)
105             rv = tmp.new()
106             defaults.merge(init).each_pair { |k,v| rv[k] = v }
107             rv
108           else
109             rv = tmp.new(*args)
110             defaults.each_pair { |k,v| rv[k] ||= v }
111             rv
112           end
113         end
114       else
115         tmp.instance_eval { class << self; self; end }.instance_eval do
116           alias_method :_new, :new
117           define_method(:new) do |*args|
118             if Hash === (init = args.first)
119               rv = tmp._new()
120               defaults.merge(init).each_pair { |k,v| rv[k] = v }
121               rv
122             else
123               rv = tmp._new(*args)
124               defaults.each_pair { |k,v| rv[k] ||= v }
125               rv
126             end
127           end
128         end
129       end
131       tmp.class_eval(&block) if block_given?
132       tmp
133     end
134   end