Module: Structured::ClassMethods
- Defined in:
- lib/structured.rb
Overview
Methods extended to a Structured class. A class would typically use the following methods within its class body:
-
#set_description to set a textual description of the object
-
#element to define expected elements of the input hash
-
#default_element to define processing of unknown element keys
The #explain method is also useful for printing out documentation for a Structured class.
Instance Method Summary collapse
-
#apply_val(obj, elt, val) ⇒ Object
Applies a value to an element for an object, after all processing for the value is done.
-
#build_from_hash(obj, hash) ⇒ Object
Given a hash, extracts all the elements from it and updates the object accordingly.
-
#convert_item(item, type, parent) ⇒ Object
Given an expected type and an item, checks that the item matches the expected type, and performs any necessary conversions.
-
#convert_structured(item, type, parent) ⇒ Object
Receive hash values that are to be converted to Structured objects.
-
#default_element(*args, **params) ⇒ Object
Accepts a default element for this class.
-
#describe_type(type) ⇒ Object
Provides a textual description of a type.
-
#description(len = nil) ⇒ Object
Returns the class’s description.
-
#each_element ⇒ Object
Iterates elements in a useful sorted order.
-
#element(name, *args, attr: true, **params) ⇒ Object
Declares that the class expects an element with the given name and type.
-
#element_data(type, optional: false, description: nil, preproc: nil, default: nil, check: nil) ⇒ Object
Processes the definition of an element.
-
#explain(io = STDOUT) ⇒ Object
Prints out documentation for this class.
-
#input_err(text) ⇒ Object
Raises an InputError.
-
#process_nil_val(val, data) ⇒ Object
Performs processing of an element value to deal with the possibility that the value is nil.
-
#process_value(obj, val, data) ⇒ Object
Given an element value and an #element_data hash of processing tools element, applies those processing tools.
-
#remove_element(name) ⇒ Object
Removes an element.
-
#reset_elements ⇒ Object
Sets up a class to manage elements.
-
#set_description(desc) ⇒ Object
Provides a description of this class, for use with the #explain method.
-
#template(indent: '') ⇒ Object
Produces a template YAML file for this Structured object.
- #template_type(type, indent, sp = ' ') ⇒ Object
-
#try_autoconvert(type, item) ⇒ Object
Several types can be automatically converted:.
- #try_read_array(filenames) ⇒ Object
-
#try_read_file(hash) ⇒ Object
If the hash contains a key :read_file, then try reading a file containing additional keys, and return a new hash merging the two.
- #try_run(block, obj, val, err_name) ⇒ Object
Instance Method Details
#apply_val(obj, elt, val) ⇒ Object
Applies a value to an element for an object, after all processing for the value is done.
573 574 575 576 577 578 579 |
# File 'lib/structured.rb', line 573 def apply_val(obj, elt, val) if obj.respond_to?("receive_#{elt}") obj.send("receive_#{elt}".to_sym, val) else obj.instance_variable_set("@#{elt}", val) end end |
#build_from_hash(obj, hash) ⇒ Object
Given a hash, extracts all the elements from it and updates the object accordingly. This method is called automatically upon initialization of the Structured class.
456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 |
# File 'lib/structured.rb', line 456 def build_from_hash(obj, hash) input_err("Initializer is not a Hash") unless hash.is_a?(Hash) hash = try_read_file(hash) @elements.each do |key, data| Structured.trace(key.to_s) do val = hash[key] || hash[key.to_s] cval = process_value(obj, val, data) apply_val(obj, key, cval) if cval end end # Process unknown elements unknown_keys = hash.keys.reject { |k| @elements.include?(k.to_sym) } return if unknown_keys.empty? unless @default_element input_err("Unexpected element(s): #{unknown_keys.join(', ')}") end unknown_keys.each do |key| Structured.trace(key.to_s) do val = hash[key] ckey = process_value(obj, key, @default_key) cval = process_value(obj, val, @default_element) next unless cval cval.receive_key(ckey) if cval.is_a?(Structured) obj.receive_any(ckey, cval) end end end |
#convert_item(item, type, parent) ⇒ Object
Given an expected type and an item, checks that the item matches the expected type, and performs any necessary conversions.
593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 |
# File 'lib/structured.rb', line 593 def convert_item(item, type, parent) case type # # In the when cases, the type is not just a class object # when :boolean return item if item.is_a?(TrueClass) || item.is_a?(FalseClass) input_err("#{item} is not boolean") when Array input_err("#{item} is not Array") unless item.is_a?(Array) Structured.trace(Array) do item = try_read_array(item[1..-1]) if item.first.to_s == 'read_file' return item.map.with_index { |i, idx| Structured.trace(idx) do convert_item(i, type.first, parent) end } end when Hash input_err("#{item} is not Hash") unless item.is_a?(Hash) Structured.trace(Hash) do return item.map { |k, v| Structured.trace(k.to_s) do conv_key = convert_item(k, type.first.first, parent) conv_item = convert_item(v, type.first.last, parent) conv_item.receive_key(conv_key) if conv_item.is_a?(Structured) [ conv_key, conv_item ] end }.to_h end else # # In these cases, the type is a class object. It can't be tested with # the === operator of a case/when. # # If the item can be automatically coverted to the expected type citem = try_autoconvert(type, item) # If the item is of the expected type, then return it return citem if citem.is_a?(type) # The only remaining hope for conversion is that type is Structured and # item is a hash return convert_structured(citem, type, parent) end end |
#convert_structured(item, type, parent) ⇒ Object
Receive hash values that are to be converted to Structured objects
673 674 675 676 677 678 679 680 681 682 683 684 685 686 |
# File 'lib/structured.rb', line 673 def convert_structured(item, type, parent) unless item.is_a?(Hash) if type.include?(Structured) input_err("#{item.inspect} not a Structured hash for #{type}") else input_err("#{item.inspect} not a #{type}") end end unless type.include?(Structured) || type.include?(StructuredPolymorphic) input_err("#{type} is not a Structured class") end return type.new(item, parent) end |
#default_element(*args, **params) ⇒ Object
Accepts a default element for this class. The arguments are the same as those for element_data except as noted below.
If this method is called, then for any keys found in an input hash that have no corresponding #element declaration in the Structured class, the method receive_any will be invoked. The value from the input hash will be processed based on any type declaration, ‘preproc`, and `check` given to default_element.
The default element keys can be processed based on the argument ‘key`, which should be a hash corresponding to the element_data arguments plus the key :type with the default key’s expected type. If ‘key` is not given, then the key must be and is automatically converted to a Symbol.
Caution: The ‘type` argument should almost always be a single class, and not a hash. This is because the default arguments are automatically treated like a hash, with the otherwise-undefined element names being the keys of the hash.
342 343 344 345 346 347 348 349 350 351 |
# File 'lib/structured.rb', line 342 def default_element(*args, **params) if (key_params = params.delete(:key)) @default_key = element_data( key_params.delete(:type) || Object, **key_params ) else @default_key = element_data(Symbol, preproc: proc { |s| s.to_sym }) end @default_element = element_data(*args, **params) end |
#describe_type(type) ⇒ Object
Provides a textual description of a type.
736 737 738 739 740 741 742 743 744 745 |
# File 'lib/structured.rb', line 736 def describe_type(type) case type when :boolean then 'Boolean' when Array then "Array of #{describe_type(type.first)}" when Hash desc1, desc2 = type.first.map { |x| describe_type(x) } "Hash of #{desc1} => #{desc2}" else return type.to_s end end |
#description(len = nil) ⇒ Object
Returns the class’s description. The given number can be used to limit the length of the description.
284 285 286 287 288 289 290 291 |
# File 'lib/structured.rb', line 284 def description(len = nil) desc = @class_description || '' if len && desc.length > len return desc[0, len] if len <= 5 return desc[0, len - 3] + '...' end return desc end |
#each_element ⇒ Object
Iterates elements in a useful sorted order.
436 437 438 439 440 441 442 443 444 445 446 |
# File 'lib/structured.rb', line 436 def each_element @elements.sort_by { |e, data| if data[:optional] == :omit [ 3, e.to_s ] else [ data[:optional] ? 2 : 1, e.to_s ] end }.each do |e, data| yield(e, data) end end |
#element(name, *args, attr: true, **params) ⇒ Object
Declares that the class expects an element with the given name and type. See element_data for an explanation of *args and **params.
the given element. Default is true.
301 302 303 304 305 306 307 308 309 310 311 312 |
# File 'lib/structured.rb', line 301 def element(name, *args, attr: true, **params) @elements[name.to_sym] = element_data(*args, **params) # # By default, when an element is received, a corresponding instance # variable is set. Classes using Structured can define +receive_[name]+ so # that the element declaration will perform other tasks. # # This creates the reader attribute only if there is no other method of # the same name. # attr_reader(name) if attr && !method_defined?(name) end |
#element_data(type, optional: false, description: nil, preproc: nil, default: nil, check: nil) ⇒ Object
Processes the definition of an element.
-
A class.
-
The value
:boolean
, indicating that a boolean is acceptable. -
An array containing a single element being a class, signifying that the expected type is an array of elements matching that class.
-
A hash containing a single Class1 => Class2 pair, signifying that the expected type is a hash of key-value pairs matching the indicated classes. If Class2 is a Structured class, then Class2 objects will have their Structured#receive_key method called, with the corresponding Class1 object as the argument.
convert it. The proc will be executed in the context of the receiving object.
is also used for optional elements that are not specified in an input hash.
value. This may be:
-
A Proc, in which case it should return true for valid values.
-
An Array of valid values (tested by ===}).
-
Any other object, in which case validity is determined by whether the check value === the element value.
392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 |
# File 'lib/structured.rb', line 392 def element_data( type, optional: false, description: nil, preproc: nil, default: nil, check: nil ) # Check the type argument case type when Class, :boolean when Array unless type.count == 1 && type.first.is_a?(Class) raise TypeError, "Invalid Array type declaration" end when Hash unless type.count == 1 && type.first.all? { |x| x.is_a?(Class) } raise TypeError, "Invalid Hash type declaration" end else raise TypeError, "Invalid type declaration #{type.inspect}" end if preproc raise TypeError, "preproc must be a Proc" unless preproc.is_a?(Proc) end case check when nil, Proc then check_obj = check # Pass through when Array then check_obj = proc { |o| check.any? { |c| c === o } } else check_obj = proc { |o| check === o } end return { :type => type, :optional => optional, :description => description, :preproc => preproc, :default => default, :check => check_obj, } end |
#explain(io = STDOUT) ⇒ Object
Prints out documentation for this class.
700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 |
# File 'lib/structured.rb', line 700 def explain(io = STDOUT) io.puts("Structured Class #{self}:") if @class_description io.puts("\n" + TextTools.line_break(@class_description, prefix: ' ')) end io.puts each_element do |elt, data| io.puts( " #{elt}: #{describe_type(data[:type])}" + \ "#{data[:optional] ? ' (optional)' : ''}" ) if data[:description] io.puts(TextTools.line_break(data[:description], prefix: ' ')) io.puts() end end if @default_element io.puts( " All other elements: #{describe_type(@default_key[:type])} => " \ "#{describe_type(@default_element[:type])}" ) if @default_element[:description] io.puts(TextTools.line_break( @default_element[:description], prefix: ' ' )) end io.puts() end end |
#input_err(text) ⇒ Object
Raises an InputError.
692 693 694 |
# File 'lib/structured.rb', line 692 def input_err(text) raise InputError, text end |
#process_nil_val(val, data) ⇒ Object
Performs processing of an element value to deal with the possibility that the value is nil. This method returns [ the new value, boolean of whether to stop processing ] according to the following rules:
-
If val is non-nil, then this method returns val itself, and processing should not stop.
-
If val is nil and this element is non-optional, then this method raises an error.
-
If val is nil and the element is optional, then the object’s default value is returned, and processing should stop.
-
If there is no default value for an optional element, then nil is returned, and processing should also stop.
563 564 565 566 567 568 569 |
# File 'lib/structured.rb', line 563 def process_nil_val(val, data) return [ val, false ] unless val.nil? unless data[:optional] input_err("Required element is missing (or was deleted by a preproc)") end return [ data[:default], true ] end |
#process_value(obj, val, data) ⇒ Object
Given an element value and an #element_data hash of processing tools element, applies those processing tools. Namely, apply any preproc, check the type and perform other checks, and perform any conversions. The return value should be usable as the received value for the corresponding element.
If this method returns nil, then there is no element to process. This method may also raise an InputError.
533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 |
# File 'lib/structured.rb', line 533 def process_value(obj, val, data) val, ret = process_nil_val(val, data) return val if ret if data[:preproc] val = try_run(data[:preproc], obj, val, "preproc") val, ret = process_nil_val(val, data) return val if ret end cval = convert_item(val, data[:type], obj) if data[:check] && !try_run(data[:check], obj, cval, "check") input_err "Value #{cval} failed check" end return cval end |
#remove_element(name) ⇒ Object
Removes an element. Note that the attribute definition if any and the receive_[name]
method are left intact.
318 319 320 |
# File 'lib/structured.rb', line 318 def remove_element(name) @elements.delete(name.to_sym) end |
#reset_elements ⇒ Object
Sets up a class to manage elements. This method is called when Structured is included in the class.
As an implementation note: Information about a Structured class is stored in instance variables of the class’s object.
266 267 268 269 270 271 |
# File 'lib/structured.rb', line 266 def reset_elements @elements = {} @default_element = nil @default_key = nil @class_description = nil end |
#set_description(desc) ⇒ Object
Provides a description of this class, for use with the #explain method.
276 277 278 |
# File 'lib/structured.rb', line 276 def set_description(desc) @class_description = desc end |
#template(indent: '') ⇒ Object
Produces a template YAML file for this Structured object.
749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 |
# File 'lib/structured.rb', line 749 def template(indent: '') res = "#{indent}# #{name}\n" if @class_description res << TextTools.line_break(@class_description, prefix: "#{indent}# ") res << "\n" end in_opt = false max_len = @elements.keys.map { |e| e.to_s.length }.max each_element do |elt, data| next if data[:optional] == :omit if data[:optional] && !in_opt res << "#{indent}#\n#{indent}# Optional\n" in_opt = true end res << "#{indent}#{elt}:" spacing = ' ' * (max_len - elt.to_s.length + 1) if data[:default] res << spacing << data[:default].inspect << "\n" else res << template_type(data[:type], indent, spacing) end end return res end |
#template_type(type, indent, sp = ' ') ⇒ Object
781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 |
# File 'lib/structured.rb', line 781 def template_type(type, indent, sp = ' ') res = '' case type when :boolean res << " true/false\n" when Class if type == String res << "#{sp}\"\"\n" elsif type.include?(Structured) res << "\n" << type.template(indent: indent + ' ') else res << "#{sp}# #{type}\n" end when Array if type.first == String res << "#{sp}[ \"\", ... ]\n" else res << "\n#{indent} -" << template_type(type.first, indent + ' ') end when Hash if type.first.first == String res << "\n#{indent} \"\":" else res << "\n#{indent} [#{type.first.first}]:" end res << template_type(type.first.last, indent + ' ') end return res end |
#try_autoconvert(type, item) ⇒ Object
Several types can be automatically converted:
-
Symbol into String
-
String into Regexp
650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 |
# File 'lib/structured.rb', line 650 def try_autoconvert(type, item) if type == String && item.is_a?(Symbol) return item.to_s end if type == Symbol && item.is_a?(String) return item.to_sym end # Special case in which strings will be converted to Regexps if type == Regexp && item.is_a?(String) begin return Regexp.new(item) rescue RegexpError input_err("#{item} is not a valid regular expression") end end return item end |
#try_read_array(filenames) ⇒ Object
507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 |
# File 'lib/structured.rb', line 507 def try_read_array(filenames) new_item = [] begin filenames.each do |file| begin res = YAML.load_file(file) raise InputError unless res.is_a?(Array) new_item.concat(res) rescue input_err("Failed to read array from #{file}: #$!") end end end return new_item end |
#try_read_file(hash) ⇒ Object
If the hash contains a key :read_file, then try reading a file containing additional keys, and return a new hash merging the two. This will not work recursively; the input file may not further contain a :read_file key.
If the given hash and the :read_file hash contain duplicate keys, the given hash overrides the file values.
494 495 496 497 498 499 500 501 502 503 504 505 |
# File 'lib/structured.rb', line 494 def try_read_file(hash) file = hash['read_file'] || hash[:read_file] return hash unless file begin res = YAML.load_file(file).merge(hash) res.delete('read_file') res.delete(:read_file) return res rescue input_err("Failed to read Structured YAML input from #{file}: #$!") end end |
#try_run(block, obj, val, err_name) ⇒ Object
581 582 583 584 585 586 587 |
# File 'lib/structured.rb', line 581 def try_run(block, obj, val, err_name) begin val = obj.instance_exec(val, &block) rescue StandardError => e input_err("#{err_name} failed: #{e.to_s}") end end |