cleanups
[lwes-ruby.git] / lib / lwes / struct.rb
blob05d5447e3abdcfe8853f6f2d16f9c581de7c6738
1 module LWES
2   class Struct
4     # creates a new Struct-based class, takes the following
5     # options hash:
6     #
7     #   :db       - pre-created LWES::TypeDB object
8     #               this is required unless :file is given
9     #   :file     - pathname to the ESF file,
10     #               this is required unless :db is given
11     #   :class    - Ruby base class name, if the ESF file only has one
12     #               event defined (besides MetaEventInfo), then specifying
13     #               it is optional, otherwise it is required when multiple
14     #               events are defined in the same ESF :file given above
15     #   :parent   - parent class or module, the default is 'Object' putting
16     #               the new class in the global namespace.
17     #   :name     - event name if it differs from the Ruby base class name
18     #               given (or inferred) above.  For DRY-ness, you are
19     #               recommended to keep your event names and Ruby class
20     #               names in sync and not need this option.
21     #   :skip     - Array of field names to skip from the Event defininition
22     #               entirely, these could include fields that are only
23     #               implemented by the Listener.  This may also be a
24     #               regular expression.
25     #   :defaults - hash of default key -> value pairs to set at
26     #               creation time
27     #
28     def self.new(options, &block)
29       db = options[:db]
30       db ||= begin
31         file = options[:file] or
32           raise ArgumentError, "TypeDB :db or ESF :file missing"
33         File.readable?(file) or
34           raise ArgumentError, "file #{file.inspect} not readable"
35         TypeDB.new(file)
36       end
37       dump = db.to_hash
38       klass = options[:class] || begin
39         # make it easier to deal with single event files
40         events = (dump.keys -  [ :MetaEventInfo ])
41         events.size > 1 and
42             raise RuntimeError,
43                   "multiple event defs available: #{events.inspect}\n" \
44                   "pick one with :class"
45         events.first
46       end
48       name = options[:name] || klass
49       parent = options[:parent] || Object
50       event_def = dump[name.to_sym] or
51         raise RuntimeError, "#{name.inspect} not defined in #{file}"
53       # merge MetaEventInfo fields in
54       meta_event_info = dump[:MetaEventInfo]
55       alpha = proc { |a,b| a.first.to_s <=> b.first.to_s }
56       event_def = event_def.sort(&alpha)
57       if meta_event_info
58         seen = event_def.map { |(field, _)| field }
59         meta_event_info.sort(&alpha).each do |field_type|
60           seen.include?(field_type.first) or event_def << field_type
61         end
62       end
64       Array(options[:skip]).each do |x|
65         if Regexp === x
66           event_def.delete_if { |(f,_)| x =~ f.to_s }
67         else
68           if x.to_sym == :MetaEventInfo
69             meta_event_info.nil? and
70               raise RuntimeError, "MetaEventInfo not defined in #{file}"
71             meta_event_info.each do |(field,_)|
72               event_def.delete_if { |(f,_)| field == f }
73             end
74           else
75             event_def.delete_if { |(f,_)| f == x.to_sym }
76           end
77         end
78       end
80       tmp = ::Struct.new(*(event_def.map { |(field,_)| field }))
81       components = klass.to_s.split(/::/)
82       components.each_with_index do |component, i|
83         if i == (components.size - 1)
84           tmp = parent.const_set(component, tmp)
85         else
86           parent = begin
87             parent.const_get(component)
88           rescue NameError
89             eval "module #{parent.name}::#{component}; end"
90             parent.const_get(component)
91           end
92         end
93       end
94       tmp.const_set :TYPE_DB, db
95       tmp.const_set :NAME, name.to_s
96       ed = tmp.const_set :EVENT_DEF, {}
97       event_def.each { |(field,type)| ed[field] = type }
99       # freeze since emitter.c can segfault if this ever changes
100       type_list = event_def.map do |(field,type)|
101         [ field, field.to_s.freeze, type ].freeze
102       end.freeze
103       tmp.const_set :TYPE_LIST, type_list
105       aref_map = tmp.const_set :AREF_MAP, {}
106       type_list.each_with_index do |(field_sym,field_str,_),idx|
107         aref_map[field_sym] = aref_map[field_str] = idx
108       end
110       tmp.const_set :HAVE_ENCODING,
111                     type_list.include?([ :enc, 'enc', LWES::INT_16 ])
113       defaults = options[:defaults] || {}
114       defaults = tmp.const_set :DEFAULTS, defaults.dup
115       tmp.class_eval(&block) if block_given?
117       # define a parent-level method, eval is faster than define_method
118       eval <<EOS
119 class ::#{tmp.name}
120   class << self
121     alias _new new
122     undef_method :new
123     def new(*args)
124       if Hash === (init = args.first)
125         rv = _new()
126         DEFAULTS.merge(init).each_pair { |k,v| rv[k] = v }
127         rv
128       else
129         rv = _new(*args)
130         DEFAULTS.each_pair { |k,v| rv[k] ||= v }
131         rv
132       end
133     end
134   end
138     # avoid linear scans for large structs, not sure if 50 is a good enough
139     # threshold but it won't help for anything <= 10 since Ruby (or at least
140     # MRI) already optimizes those cases
141     if event_def.size > 50
142       eval <<EOS
143 class ::#{tmp.name}
144   alias __aref []
145   alias __aset []=
146   def [](key)
147     __aref(key.kind_of?(Fixnum) ? key : AREF_MAP[key])
148   end
150   def []=(key, value)
151     __aset(key.kind_of?(Fixnum) ? key : AREF_MAP[key], value)
152   end
155       fast_methods = []
156       event_def.each_with_index do |(fld,_), idx|
157         next if idx <= 9
158         if idx != aref_map[fld]
159           raise LoadError, "event_def corrupt: #{event_def}"
160         end
161         fast_methods << "undef_method :#{fld}, :#{fld}=\n"
162         fast_methods << "\ndef #{fld}; __aref #{idx}; end\n"
163         fast_methods << "\ndef #{fld}=(val); __aset #{idx}, val ; end\n"
164       end
166       eval("class ::#{tmp.name}; #{fast_methods.join("\n")}\n end")
167     end
169     tmp
170     end
171   end