Class: LcpRuby::Generators::Entity::FieldTokenParser

Inherits:
Object
  • Object
show all
Defined in:
lib/generators/lcp_ruby/entity/field_token_parser.rb

Constant Summary collapse

TOKEN_RE =
/\A
  (?<name>[a-z_][a-z0-9_]*)
  :
  (?<type>[a-z_]+)
  (?:\{(?<enum_values>[^}]*\|[^}]*)\})?
  (?<required>!)?
  (?<flags>(?::[a-z_]+)*)
  (?<blocks>(?:\{[^}]*\})*)
\z/x
NUMERIC_OPTION_KEYS =

Numeric option keys whose value the parser coerces to Integer at parse time so downstream emitters don’t have to. Only ‘:limit` (v3 string limit) is wired into the emitter today; `:precision` and `:scale` are deferred to v4 per Decision 26 (the brace-modifier grammar has no two-arg form for `decimalN,M`).

%i[limit].freeze

Class Method Summary collapse

Class Method Details

.block_flag_order_hint(raw) ⇒ Object



91
92
93
94
95
96
97
# File 'lib/generators/lcp_ruby/entity/field_token_parser.rb', line 91

def self.block_flag_order_hint(raw)
  return nil unless (m = BLOCK_FLAG_ORDER_RE.match(raw))
  block = "{#{m[:block]}}"
  flag  = ":#{m[:flag]}"
  "expected ':flags' before '{blocks}' — got '#{block}' followed by '#{flag}'. " \
    "Reorder to '#{flag}#{block}' or split into separate tokens."
end

.parse(tokens) ⇒ Object



27
28
29
# File 'lib/generators/lcp_ruby/entity/field_token_parser.rb', line 27

def self.parse(tokens)
  tokens.map { |t| parse_token(t) }
end

.parse_error_for(raw) ⇒ Object



76
77
78
79
80
81
# File 'lib/generators/lcp_ruby/entity/field_token_parser.rb', line 76

def self.parse_error_for(raw)
  base = "Cannot parse field token '#{raw}'. " \
         "Expected name:type[{values}][!][:flags][{blocks}]."
  hint = block_flag_order_hint(raw)
  hint ? "#{base}\n  #{hint}" : base
end

.parse_token(raw) ⇒ Object



31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
# File 'lib/generators/lcp_ruby/entity/field_token_parser.rb', line 31

def self.parse_token(raw)
  m = TOKEN_RE.match(raw) or raise Thor::Error, parse_error_for(raw)

  name = m[:name].to_sym
  type = m[:type].to_sym
  modifiers = {}
  options = {}

  enum_values =
    if m[:enum_values]
      raise Thor::Error,
        "Pipe-separated value list is only valid for enum fields (got '#{raw}')." unless type == :enum
      m[:enum_values].split("|").map(&:strip)
    elsif type == :enum
      raise Thor::Error,
        "Enum field '#{name}' requires inline values: #{name}:enum{val1|val2|val3}"
    end

  modifiers[:required] = true if m[:required]

  m[:flags].to_s.scan(/:([a-z_]+)/).each { |(flag)| modifiers[flag.to_sym] = true }

  m[:blocks].to_s.scan(/\{([^}]*)\}/).each do |(body)|
    body.split(",").map(&:strip).each do |segment|
      raise Thor::Error, "Empty modifier in '#{raw}'." if segment.empty?

      if segment.include?(":")
        key, value = segment.split(":", 2).map(&:strip)
        key_sym = key.to_sym
        options[key_sym] = NUMERIC_OPTION_KEYS.include?(key_sym) ? value.to_i : value
      else
        modifiers[segment.to_sym] = true
      end
    end
  end

  FieldDescriptor.new(
    name: name,
    type: type,
    enum_values: enum_values,
    modifiers: modifiers,
    options: options
  )
end