lib/rasn1/model.rb in rasn1-0.11.0 vs lib/rasn1/model.rb in rasn1-0.12.0

- old
+ new

@@ -55,20 +55,46 @@ # or like this: # record2 = Record2.new(rented: true, a_record: { id: 65537, room: 43 }) # # == Delegation # {Model} may delegate some methods to its root element. Thus, if root element - # is, for example, a {TypeInts::Choice}, model may delegate +#chosen+ and +#chosen_value+. + # is, for example, a {Types::Choice}, model may delegate +#chosen+ and +#chosen_value+. # # All methods defined by root may be delegated by model, unless model also defines # this method. # @author Sylvain Daubert - class Model + # @author adfoster-r7 ModelValidationError, track source location for dynamic class methods + class Model # rubocop:disable Metrics/ClassLength # @private - Elem = Struct.new(:name, :proc_or_class, :content) + Elem = Struct.new(:name, :proc_or_class, :content) do + def initialize(name, proc_or_class, content) + if content.is_a?(Array) + duplicate_names = find_all_duplicate_names(content.map(&:name) + [name]) + raise ModelValidationError, "Duplicate name #{duplicate_names.first} found" if duplicate_names.any? + end + super + end + + private + + # @param [Array<String>] names + # @return [Array<String>] The duplicate names found in the array + def find_all_duplicate_names(names) + names.group_by { |name| name } + .select { |_name, values| values.length > 1 } + .keys + end + end + # @private + WrapElem = Struct.new(:element, :options) do + def name + "#{element.name}_wrapper" + end + end + module Accel # @return [Hash] attr_reader :options # Use another model in this model @@ -77,10 +103,19 @@ # @return [Elem] def model(name, model_klass) @root = Elem.new(name, model_klass, nil) end + # Use a {Wrapper} around a {Types::Base} or a {Model} object + # @param [Types::Base,Model] element + # @param [Hash] options + # @return [WrapElem] + # @since 0.12 + def wrapper(element, options={}) + @root = WrapElem.new(element, options) + end + # Update options of root element. # May be used when subclassing. # class Model1 < RASN1::Model # sequence :seq, implicit: 0, # content: [bool(:bool), integer(:int)] @@ -90,12 +125,17 @@ # class Model2 < Model1 # root_options implicit: 1 # end # @param [Hash] options # @return [void] + # @since 0.12.0 may change name through +:name+ def root_options(options) @options = options + return unless options.key?(:name) + + @root = @root.dup + @root.name = options[:name] end # On inheritance, create +@root+ class variable # @param [Class] klass # @return [void] @@ -103,37 +143,42 @@ super root = @root klass.class_eval { @root = root } end + # @since 0.11.0 + # @since 0.12.0 track source location on error (adfoster-r7) def define_type_accel_base(accel_name, klass) - singleton_class.class_eval( - "def #{accel_name}(name, options={})\n" \ - " options[:name] = name\n" \ - " proc = proc do |opts|\n" \ - " #{klass}.new(options.merge(opts))\n" \ - " end\n" \ - " @root = Elem.new(name, proc, options[:content])\n" \ - 'end' - ) + singleton_class.class_eval <<-EVAL, __FILE__, __LINE__ + 1 + def #{accel_name}(name, options={}) # def sequence(name, type, options) + options[:name] = name + proc = proc do |opts| + #{klass}.new(options.merge(opts)) # Sequence.new(options.merge(opts)) + end + @root = Elem.new(name, proc, options[:content]) + end + EVAL end + # @since 0.11.0 + # @since 0.12.0 track source location on error (adfoster-r7) def define_type_accel_of(accel_name, klass) - singleton_class.class_eval( - "def #{accel_name}_of(name, type, options={})\n" \ - " options[:name] = name\n" \ - " proc = proc do |opts|\n" \ - " #{klass}.new(type, options.merge(opts))\n" \ - " end\n" \ - " @root = Elem.new(name, proc, nil)\n" \ - 'end' - ) + singleton_class.class_eval <<-EVAL, __FILE__, __LINE__ + 1 + def #{accel_name}_of(name, type, options={}) # def sequence_of(name, type, options) + options[:name] = name + proc = proc do |opts| + #{klass}.new(type, options.merge(opts)) # SequenceOf.new(type, options.merge(opts)) + end + @root = Elem.new(name, proc, nil) + end + EVAL end # Define an accelarator to access a type in a model definition # @param [String] accel_name # @param [Class] klass + # @since 0.11.0 def define_type_accel(accel_name, klass) if klass < Types::SequenceOf define_type_accel_of(accel_name, klass) else define_type_accel_base(accel_name, klass) @@ -182,96 +227,112 @@ end end extend Accel - # @method sequence(name, options) + # @!method sequence(name, options) + # @!scope class # @param [Symbol,String] name name of object in model # @param [Hash] options # @return [Elem] # @see Types::Sequence#initialize - # @method set(name, options) + # @!method set(name, options) + # @!scope class # @param [Symbol,String] name name of object in model # @param [Hash] options # @return [Elem] # @see Types::Set#initialize - # @method choice(name, options) + # @!method choice(name, options) + # @!scope class # @param [Symbol,String] name name of object in model # @param [Hash] options # @return [Elem] # @see Types::Choice#initialize %w[sequence set choice].each do |type| self.define_type_accel_base(type, Types.const_get(type.capitalize)) end - # @method sequence_of(name, type, options) + # @!method sequence_of(name, type, options) + # @!scope class # @param [Symbol,String] name name of object in model # @param [Model, Types::Base] type type for SEQUENCE OF # @param [Hash] options # @return [Elem] # @see Types::SequenceOf#initialize - # @method set_of(name, type, options) + # @!method set_of(name, type, options) + # @!scope class # @param [Symbol,String] name name of object in model # @param [Model, Types::Base] type type for SET OF # @param [Hash] options # @return [Elem] # @see Types::SetOf#initialize %w[sequence set].each do |type| define_type_accel_of(type, Types.const_get("#{type.capitalize}Of")) end - # @method boolean(name, options) + # @!method boolean(name, options) + # @!scope class # @param [Symbol,String] name name of object in model # @param [Hash] options # @return [Elem] # @see Types::Boolean#initialize - # @method integer(name, options) + # @!method integer(name, options) + # @!scope class # @param [Symbol,String] name name of object in model # @param [Hash] options # @return [Elem] # @see Types::Integer#initialize - # @method bit_string(name, options) + # @!method bit_string(name, options) + # @!scope class # @param [Symbol,String] name name of object in model # @param [Hash] options # @return [Elem] # @see Types::BitString#initialize - # @method octet_string(name, options) + # @!method octet_string(name, options) + # @!scope class # @param [Symbol,String] name name of object in model # @param [Hash] options # @return [Elem] # @see Types::OctetString#initialize - # @method null(name, options) + # @!method null(name, options) + # @!scope class # @param [Symbol,String] name name of object in model # @param [Hash] options # @return [Elem] # @see Types::Null#initialize - # @method enumerated(name, options) + # @!method enumerated(name, options) + # @!scope class # @param [Symbol,String] name name of object in model # @param [Hash] options # @return [Elem] # @see Types::Enumerated#initialize - # @method utf8_string(name, options) + # @!method utf8_string(name, options) + # @!scope class # @param [Symbol,String] name name of object in model # @param [Hash] options # @return [Elem] # @see Types::Utf8String#initialize - # @method numeric_string(name, options) + # @!method numeric_string(name, options) + # @!scope class # @param [Symbol,String] name name of object in model # @param [Hash] options # @return [Elem] # @see Types::NumericString#initialize - # @method printable_string(name, options) + # @!method printable_string(name, options) + # @!scope class # @param [Symbol,String] name name of object in model # @param [Hash] options # @return [Elem] # @see Types::PrintableString#initialize - # @method visible_string(name, options) + # @!method visible_string(name, options) + # @!scope class # @param [Symbol,String] name name of object in model # @param [Hash] options # @return [Elem] # @see Types::VisibleString#initialize - # @method ia5_string(name, options) + # @!method ia5_string(name, options) + # @!scope class # @param [Symbol,String] name name of object in model # @param [Hash] options # @return [Elem] # @see Types::IA5String#initialize Types.primitives.each do |prim| @@ -283,22 +344,22 @@ # Create a new instance of a {Model} # @param [Hash] args def initialize(args={}) root = generate_root - set_elements(root) + generate_elements(root) initialize_elements(self, args) end # Give access to element +name+ in model # @param [String,Symbol] name # @return [Types::Base] def [](name) @elements[name] end - # Set value of element +name+. Element should be a {Base}. + # Set value of element +name+. Element should be a {Types::Base}. # @param [String,Symbol] name # @param [Object] value # @return [Object] value def []=(name, value) raise Error, 'cannot set value for a Model' if @elements[name].is_a? Model @@ -458,28 +519,47 @@ @elements = {} @elements[@root_name] = get_type(class_element.proc_or_class, self.class.options || {}) class_element end - def set_elements(element) # rubocop:disable Naming/AccessorMethodName + def generate_elements(element) + if element.is_a?(WrapElem) + generate_wrapper(element) + return + end return unless element.content.is_a? Array @elements[name].value = element.content.map do |another_element| - subel = get_type(another_element.proc_or_class) - @elements[another_element.name] = subel - set_elements(another_element) if composed?(subel) && another_element.content.is_a?(Array) + add_subelement(another_element) + end + end + + def generate_wrapper(wrap_elem) + inner_elem = wrap_elem.element + subel = add_subelement(inner_elem) + Wrapper.new(subel, wrap_elem.options) + end + + def add_subelement(subelement) + case subelement + when Elem + subel = get_type(subelement.proc_or_class) + @elements[subelement.name] = subel + generate_elements(subelement) if composed?(subel) && subelement.content.is_a?(Array) subel + when WrapElem + generate_wrapper(subelement) end end def initialize_elements(obj, args) args.each do |name, value| next unless obj[name] case value when Hash - raise ArgumentError, "element #{name}: may only pass a Hash for Model elements" unless obj[name].is_a? Model + raise ArgumentError, "element #{name}: may only pass a Hash for Model elements" unless obj[name].is_a?(Model) initialize_elements obj[name], value when Array initialize_element_from_array(obj[name], value) else @@ -497,18 +577,25 @@ else value.each { |el| composed << el } end end - def private_to_h(element=nil) + def private_to_h(element=nil) # rubocop:disable Metrics/CyclomaticComplexity my_element = element || root my_element = my_element.root if my_element.is_a?(Model) value = case my_element when Types::SequenceOf sequence_of_to_h(my_element) when Types::Sequence sequence_to_h(my_element) + # @author adfoster-r7 + when Types::Choice + raise ChoiceError.new(my_element) if my_element.chosen.nil? + + private_to_h(my_element.value[my_element.chosen]) + when Wrapper + wrapper_to_h(my_element) else my_element.value end if element.nil? { @root_name => value } @@ -527,13 +614,31 @@ def sequence_to_h(seq) ary = seq.value.map do |el| next if el.optional? && el.value.nil? - name = el.is_a?(Model) ? @elements.key(el) : el.name - [name, private_to_h(el)] + case el + when Model + hsh = el.to_h + hsh = hsh[hsh.keys.first] + [@elements.key(el), hsh] + when Wrapper + [@elements.key(el.element), wrapper_to_h(el)] + else + [el.name, private_to_h(el)] + end end ary.compact! ary.to_h + end + + def wrapper_to_h(wrap) + case wrap.element + when Model + hsh = wrap.element.to_h + hsh[hsh.keys.first] + else + private_to_h(wrap.element) + end end end end