|
| 1 | +# frozen_string_literal: true |
| 2 | + |
| 3 | +module Interscript |
| 4 | + module Isc |
| 5 | + # Normalizes the raw parslet tree into the documented Parser.parse |
| 6 | + # shape: scalar leaves (string/identifier nodes) become plain |
| 7 | + # strings, metadata field pairs become keyed hashes, and arrow-form |
| 8 | + # test pairs merge into single {input:, expected:} entries. Deeper |
| 9 | + # item trees (rule from/to, constraints, kwargs) keep their parslet |
| 10 | + # shape — DocumentBuilder materializes those through Transform. |
| 11 | + class Normalizer |
| 12 | + def self.normalize(tree) |
| 13 | + new(tree).normalize |
| 14 | + end |
| 15 | + |
| 16 | + def initialize(tree) |
| 17 | + @tree = tree |
| 18 | + end |
| 19 | + |
| 20 | + def normalize |
| 21 | + system = @tree[:system] |
| 22 | + return @tree unless system |
| 23 | + |
| 24 | + { |
| 25 | + system: { |
| 26 | + system_code: scalar(system[:system_code]), |
| 27 | + body: Array(system[:body]).map { |block| normalize_block(block) } |
| 28 | + } |
| 29 | + } |
| 30 | + end |
| 31 | + |
| 32 | + private |
| 33 | + |
| 34 | + # {string: [{char: "B"@8}, ...]} and {identifier: "main"@31} |
| 35 | + # become plain strings. |
| 36 | + def scalar(node) |
| 37 | + return node if node.is_a?(::String) || node.nil? |
| 38 | + if node.key?(:string) |
| 39 | + # Parts are {char:} hashes in the general grammar; some |
| 40 | + # constructions (e.g. the rababa directive) capture bare |
| 41 | + # slices instead. |
| 42 | + Array(node[:string]).map { |part| part.is_a?(Hash) ? part[:char].to_s : part.to_s }.join |
| 43 | + elsif node.key?(:identifier) |
| 44 | + node[:identifier].to_s |
| 45 | + elsif node.key?(:raw) |
| 46 | + node[:raw].to_s |
| 47 | + else |
| 48 | + node |
| 49 | + end |
| 50 | + end |
| 51 | + |
| 52 | + def normalize_block(block) |
| 53 | + return block unless block.is_a?(Hash) |
| 54 | + if block.key?(:metadata) |
| 55 | + {metadata: Array(block[:metadata]).map { |field| normalize_meta_field(field) }} |
| 56 | + elsif block.key?(:aliases) |
| 57 | + { |
| 58 | + aliases: Array(block[:aliases]).map do |alias_def| |
| 59 | + {name: scalar(alias_def[:name]), value: alias_def[:value]}.compact |
| 60 | + end |
| 61 | + } |
| 62 | + elsif block.key?(:tests) |
| 63 | + {tests: merge_tests(Array(block[:tests]))} |
| 64 | + elsif block.key?(:stage) |
| 65 | + {stage_name: scalar(block[:stage_name]), stage: block[:stage]} |
| 66 | + elsif block.key?(:target) |
| 67 | + {target: scalar(block[:target]), alias: scalar(block[:alias])}.compact |
| 68 | + else |
| 69 | + block |
| 70 | + end |
| 71 | + end |
| 72 | + |
| 73 | + # {field_name: {identifier: "authority"}, field_value: {string: [...]}} |
| 74 | + # becomes {authority: "BGN-PCGN"}. |
| 75 | + def normalize_meta_field(field) |
| 76 | + return field unless field.is_a?(Hash) && field.key?(:field_name) |
| 77 | + |
| 78 | + name = scalar(field[:field_name]).to_sym |
| 79 | + value = field.key?(:field_block) ? field[:field_block] : scalar(field[:field_value]) |
| 80 | + {name => value} |
| 81 | + end |
| 82 | + |
| 83 | + # The arrow form usually parses as one {input:, expected:} hash |
| 84 | + # per test; tolerate adjacent single-key entries as well. |
| 85 | + def merge_tests(entries) |
| 86 | + merged = [] |
| 87 | + pending_input = nil |
| 88 | + entries.each do |entry| |
| 89 | + next unless entry.is_a?(Hash) |
| 90 | + if entry.key?(:input) && entry.key?(:expected) |
| 91 | + merged << {input: scalar(entry[:input]), expected: scalar(entry[:expected])} |
| 92 | + pending_input = nil |
| 93 | + elsif entry.key?(:input) |
| 94 | + pending_input = scalar(entry[:input]) |
| 95 | + elsif entry.key?(:expected) |
| 96 | + merged << {input: pending_input, expected: scalar(entry[:expected])} |
| 97 | + pending_input = nil |
| 98 | + else |
| 99 | + merged << entry |
| 100 | + end |
| 101 | + end |
| 102 | + merged << {input: pending_input, expected: ""} if pending_input |
| 103 | + merged |
| 104 | + end |
| 105 | + end |
| 106 | + end |
| 107 | +end |
0 commit comments