pattern-ruby 0.1.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f811da9c8851dd1ac42ed24ab7db8b93a05bcefd61d9c76a66676e3c97cbdf15
4
- data.tar.gz: 3b282051dfbd99a38a5960d6ce46e7637b5ba63a1c2d9bbcae908ba58af7e558
3
+ metadata.gz: 3cf30e8485bb83eb27f583e82c6e54c6556ded68150104ee2a35f95032af655d
4
+ data.tar.gz: 27e26d1dd70b4f89457281e7234b95c766a883f71ce76bbbef4fe6d0c5838a1b
5
5
  SHA512:
6
- metadata.gz: 13de001ca6e9657cf5faa5ec36bf5ffc5522d957fd54b92c7b13a7d72e0748582f566208dc8b93307d78aa44aa5607ec0234a73443a36bb8ec75a8eb763544dc
7
- data.tar.gz: db8cd6c927e091c6bbdacc9a50e75e454041342fb2cfd9378ffc9687715435aafa9e01544d2be9b20a09f617c7522283e028dd3b6f4b5e90dde189b7cb865c11
6
+ metadata.gz: 26f94c3c765863235125bbedf9a1c6f460a2691744665001b2e91245f324f1976fd6c8a20bd0e2342651c2d8c7122d8ebdc2f4a46ca2d2e35a2ac07a9005f526
7
+ data.tar.gz: fb5b161865d3c210c3e5b8692bb7472fc0995e26f45553c20b039d5db8cbbfbe5cba45be99508b3eaf86db64a8474cbf1dc531b299fb32cd98741611d2f52c33
@@ -75,12 +75,24 @@ module PatternRuby
75
75
  return false unless intent
76
76
 
77
77
  # Find entity definitions that don't have a slot value yet
78
- unfilled = intent.entity_definitions.select { |name, _| !@slots.key?(name) || @slots[name].nil? }
78
+ unfilled = intent.entity_definitions.select { |k, _| !@slots.key?(k) || @slots[k].nil? }
79
79
  return false if unfilled.empty?
80
80
 
81
81
  # Try the input as a value for the first unfilled slot
82
- name, _defn = unfilled.first
83
- @slots[name] = input.strip
82
+ slot_name, defn = unfilled.first
83
+ value = input.strip
84
+
85
+ # Validate against entity type constraints if defined
86
+ if defn
87
+ if defn[:values] && !defn[:values].include?(value)
88
+ return false
89
+ end
90
+ if defn[:pattern] && !value.match?(defn[:pattern])
91
+ return false
92
+ end
93
+ end
94
+
95
+ @slots[slot_name] = value
84
96
  true
85
97
  end
86
98
  end
@@ -24,49 +24,88 @@ module PatternRuby
24
24
  @entity_registry = entity_registry
25
25
  end
26
26
 
27
- def compile(pattern_string)
27
+ def self.validate!(pattern_string)
28
+ new.validate!(pattern_string)
29
+ end
30
+
31
+ def validate!(pattern_string)
28
32
  raise ArgumentError, "pattern must be a String, got #{pattern_string.class}" unless pattern_string.is_a?(String)
29
33
  raise ArgumentError, "pattern cannot be nil or empty" if pattern_string.nil? || pattern_string.strip.empty?
30
34
  if pattern_string.length > MAX_PATTERN_LENGTH
31
35
  raise ArgumentError, "pattern exceeds maximum length of #{MAX_PATTERN_LENGTH} characters"
32
36
  end
33
37
 
38
+ # Check for unbalanced brackets
39
+ check_balanced(pattern_string, "[", "]", "square brackets")
40
+ check_balanced(pattern_string, "(", ")", "parentheses")
41
+ check_balanced(pattern_string, "{", "}", "curly braces")
42
+
43
+ # Check for empty entity names
44
+ if pattern_string.match?(/\{\s*\}/)
45
+ raise ArgumentError, "empty entity name {} in pattern"
46
+ end
47
+ if pattern_string.match?(/\{\s*:/)
48
+ raise ArgumentError, "entity name cannot start with ':' in pattern"
49
+ end
50
+
51
+ true
52
+ end
53
+
54
+ def compile(pattern_string)
55
+ validate!(pattern_string)
56
+
34
57
  tokens = tokenize(pattern_string)
35
58
  entity_names = []
36
59
  literal_count = 0
37
60
  regex_parts = []
61
+ optional_flags = []
38
62
 
39
63
  tokens.each do |token|
40
64
  case token
41
65
  when EntityToken
42
66
  entity_names << token.name.to_sym
43
67
  regex_parts << build_entity_regex(token)
68
+ optional_flags << false
44
69
  when OptionalToken
45
70
  inner = compile_inner(token.content)
46
71
  entity_names.concat(inner[:entity_names])
47
- regex_parts << "(?:\\s+#{inner[:regex]})?"
72
+ regex_parts << inner[:regex]
73
+ optional_flags << true
48
74
  when AlternationToken
49
75
  alts = token.alternatives.map { |a| Regexp.escape(a) }
50
76
  regex_parts << "(?:#{alts.join('|')})"
51
77
  literal_count += 1
78
+ optional_flags << false
52
79
  when WildcardToken
53
80
  regex_parts << "(.+)"
81
+ optional_flags << false
54
82
  when LiteralToken
55
83
  regex_parts << Regexp.escape(token.text)
56
84
  literal_count += 1
85
+ optional_flags << false
57
86
  end
58
87
  end
59
88
 
60
89
  token_count = tokens.size
61
90
  entity_count = entity_names.size
62
91
 
63
- # Join with \s+ but make whitespace before optional groups flexible
92
+ # Join parts with \s+ separators, wrapping optional parts in (?:...)?
64
93
  regex_str = +""
94
+ need_sep = false
65
95
  regex_parts.each_with_index do |part, i|
66
- if i > 0 && !part.start_with?("(?:\\s+") # optional groups already include leading \s+
67
- regex_str << "\\s+"
96
+ if optional_flags[i]
97
+ if i == 0
98
+ regex_str << "(?:#{part}\\s+)?"
99
+ need_sep = false
100
+ else
101
+ regex_str << "(?:\\s+#{part})?"
102
+ need_sep = true
103
+ end
104
+ else
105
+ regex_str << "\\s+" if need_sep
106
+ regex_str << part
107
+ need_sep = true
68
108
  end
69
- regex_str << part
70
109
  end
71
110
 
72
111
  regex = Regexp.new("\\A\\s*#{regex_str}\\s*\\z", Regexp::IGNORECASE)
@@ -97,11 +136,14 @@ module PatternRuby
97
136
  scanner.skip(/\s+/)
98
137
  break if scanner.eos?
99
138
 
100
- if scanner.scan(/\{([^}]+)\}/)
139
+ if scanner.scan(/\{([^}]*)\}/)
101
140
  # Entity: {name} or {name:constraint}
102
141
  content = scanner[1]
142
+ raise ArgumentError, "empty entity name {} in pattern" if content.strip.empty?
103
143
  name, constraint = content.split(":", 2)
104
- tokens << EntityToken.new(name.strip, constraint&.strip)
144
+ name = name.strip
145
+ raise ArgumentError, "empty entity name in pattern" if name.empty?
146
+ tokens << EntityToken.new(name, constraint&.strip)
105
147
  elsif scanner.scan(/\[/)
106
148
  # Optional: [content]
107
149
  depth = 1
@@ -159,6 +201,16 @@ module PatternRuby
159
201
  end
160
202
  end
161
203
 
204
+ def check_balanced(str, open_char, close_char, name)
205
+ depth = 0
206
+ str.each_char do |c|
207
+ depth += 1 if c == open_char
208
+ depth -= 1 if c == close_char
209
+ raise ArgumentError, "unbalanced #{name} in pattern: #{str.inspect}" if depth < 0
210
+ end
211
+ raise ArgumentError, "unbalanced #{name} in pattern: #{str.inspect}" if depth != 0
212
+ end
213
+
162
214
  def compile_inner(content)
163
215
  inner_tokens = tokenize(content)
164
216
  entity_names = []
@@ -2,6 +2,7 @@ module PatternRuby
2
2
  class Pipeline
3
3
  def initialize(&block)
4
4
  @steps = []
5
+ @error_handler = nil
5
6
  instance_eval(&block) if block
6
7
  end
7
8
 
@@ -9,10 +10,21 @@ module PatternRuby
9
10
  @steps << { name: name, handler: block }
10
11
  end
11
12
 
13
+ def on_error(&block)
14
+ @error_handler = block
15
+ end
16
+
12
17
  def process(input)
13
18
  result = input
14
19
  @steps.each do |s|
15
20
  result = s[:handler].call(result)
21
+ rescue => e
22
+ if @error_handler
23
+ result = @error_handler.call(e, s[:name], result)
24
+ break if result.nil?
25
+ else
26
+ raise
27
+ end
16
28
  end
17
29
  result
18
30
  end
@@ -1,3 +1,3 @@
1
1
  module PatternRuby
2
- VERSION = "0.1.1"
2
+ VERSION = "0.2.0"
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: pattern-ruby
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Johannes Dwi Cahyo