text-gen 0.11.2 → 0.12.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.
Files changed (46) hide show
  1. checksums.yaml +4 -4
  2. data/lib/text/gen/context.rb +119 -0
  3. data/lib/text/gen/filter/base.rb +16 -13
  4. data/lib/text/gen/filter/capitalize.rb +19 -0
  5. data/lib/text/gen/filter/censor.rb +52 -0
  6. data/lib/text/gen/filter/clear.rb +19 -0
  7. data/lib/text/gen/filter/distinct.rb +19 -0
  8. data/lib/text/gen/filter/downcase.rb +19 -0
  9. data/lib/text/gen/filter/exclude.rb +19 -0
  10. data/lib/text/gen/filter/locale.rb +44 -0
  11. data/lib/text/gen/filter/match.rb +19 -0
  12. data/lib/text/gen/filter/meta.rb +35 -0
  13. data/lib/text/gen/filter/pluralize.rb +96 -0
  14. data/lib/text/gen/filter/reject.rb +24 -0
  15. data/lib/text/gen/filter/remember.rb +18 -0
  16. data/lib/text/gen/filter/replace.rb +15 -0
  17. data/lib/text/gen/filter/select.rb +24 -0
  18. data/lib/text/gen/filter/swap.rb +50 -0
  19. data/lib/text/gen/filter/titleize.rb +29 -0
  20. data/lib/text/gen/filter/upcase.rb +19 -0
  21. data/lib/text/gen/filter.rb +10 -168
  22. data/lib/text/gen/investigator.rb +38 -0
  23. data/lib/text/gen/meta.rb +46 -0
  24. data/lib/text/gen/result.rb +7 -22
  25. data/lib/text/gen/result_accumulator.rb +6 -11
  26. data/lib/text/gen/runner.rb +113 -174
  27. data/lib/text/gen/store.rb +29 -1
  28. data/lib/text/gen/version.rb +1 -1
  29. data/lib/text/gen.rb +4 -0
  30. metadata +21 -17
  31. data/lib/text/gen/filter/item/locale.rb +0 -29
  32. data/lib/text/gen/filter/item/reject.rb +0 -28
  33. data/lib/text/gen/filter/item/select.rb +0 -28
  34. data/lib/text/gen/filter/item_filter.rb +0 -17
  35. data/lib/text/gen/filter/result/capitalize.rb +0 -17
  36. data/lib/text/gen/filter/result/censor.rb +0 -55
  37. data/lib/text/gen/filter/result/clear.rb +0 -19
  38. data/lib/text/gen/filter/result/downcase.rb +0 -17
  39. data/lib/text/gen/filter/result/exclude.rb +0 -22
  40. data/lib/text/gen/filter/result/match.rb +0 -22
  41. data/lib/text/gen/filter/result/meta.rb +0 -31
  42. data/lib/text/gen/filter/result/pluralize.rb +0 -91
  43. data/lib/text/gen/filter/result/swap.rb +0 -48
  44. data/lib/text/gen/filter/result/titleize.rb +0 -28
  45. data/lib/text/gen/filter/result/upcase.rb +0 -17
  46. data/lib/text/gen/filter/result_filter.rb +0 -35
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Text
4
+ module Gen
5
+ module Filter
6
+ class Titleize < Base
7
+ SKIP_WORDS = Set.new(%w[a an and as at but by for if in of on or the to v via vs])
8
+
9
+ def result(context, result)
10
+ return result if @depth && context.depth != @depth
11
+
12
+ text = result.text
13
+ text = titleize(text)
14
+ return result if text == result.text
15
+
16
+ Result.from(result, text:, type: component_key)
17
+ end
18
+
19
+ private
20
+
21
+ def titleize(str)
22
+ str.split(/\s+/).map.with_index do |word, idx|
23
+ idx.zero? || !SKIP_WORDS.include?(word) ? word.sub(/\S/, &:upcase) : word
24
+ end.join(" ")
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Text
4
+ module Gen
5
+ module Filter
6
+ class Upcase < Base
7
+ def result(context, result)
8
+ return result if @depth && context.depth != @depth
9
+
10
+ text = result.text
11
+ text = text.upcase
12
+ return result if text == result.text
13
+
14
+ Result.from(result, text:, type: component_key)
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -1,183 +1,25 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "filter/base"
4
- require_relative "filter/result_filter"
5
- require_relative "filter/item_filter"
6
-
7
- # Result filters
8
- require_relative "filter/result/capitalize"
9
- require_relative "filter/result/censor"
10
- require_relative "filter/result/clear"
11
- require_relative "filter/result/downcase"
12
- require_relative "filter/result/exclude"
13
- require_relative "filter/result/match"
14
- require_relative "filter/result/meta"
15
- require_relative "filter/result/pluralize"
16
- require_relative "filter/result/swap"
17
- require_relative "filter/result/titleize"
18
- require_relative "filter/result/upcase"
19
-
20
- # Item filters
21
- require_relative "filter/item/locale"
22
- require_relative "filter/item/select"
23
- require_relative "filter/item/reject"
4
+ Dir[File.join(__dir__, "filter", "*.rb")].each { |f| require f unless f.end_with?("/base.rb") }
24
5
 
25
6
  module Text
26
7
  module Gen
27
8
  module Filter
28
- RESULT_FILTER_CLASSES = {
29
- "capitalize" => Filter::Result::Capitalize,
30
- "censor" => Filter::Result::Censor,
31
- "clear" => Filter::Result::Clear,
32
- "downcase" => Filter::Result::Downcase,
33
- "exclude" => Filter::Result::Exclude,
34
- "match" => Filter::Result::Match,
35
- "meta" => Filter::Result::Meta,
36
- "pluralize" => Filter::Result::Pluralize,
37
- "swap" => Filter::Result::Swap,
38
- "titleize" => Filter::Result::Titleize,
39
- "upcase" => Filter::Result::Upcase
40
- }.freeze
41
-
42
- ITEM_FILTER_CLASSES = {
43
- "locale" => Filter::Item::Locale,
44
- "select" => Filter::Item::Select,
45
- "reject" => Filter::Item::Reject
46
- }.freeze
47
-
48
9
  class << self
49
- def functions(result, filters, lookup: nil)
50
- return result if filters.nil? || filters.empty?
51
-
52
- filters.each do |filter|
53
- filter_class = RESULT_FILTER_CLASSES[filter["type"]]
54
- if filter_class
55
- filter_instance = filter_class.new(filter, lookup: lookup)
56
- result = filter_instance.apply(result)
57
- end
58
- end
59
-
60
- result
61
- end
62
-
63
- def filter_locale(items, filters)
64
- locale_filters = filters_by_type(filters, "locale")
65
- return items if locale_filters.empty?
66
-
67
- filter_instance = ITEM_FILTER_CLASSES["locale"].new(locale_filters.first)
68
- filter_instance.apply(items)
69
- end
70
-
71
- def filter_select(items, filters)
72
- select_filters = filters_by_type(filters, "select")
73
- return items if select_filters.empty?
74
-
75
- # OR logic: any filter matches
76
- items.select do |item|
77
- select_filters.any? do |filter|
78
- filter_instance = ITEM_FILTER_CLASSES["select"].new(filter)
79
- filter_instance.apply([item]).any?
80
- end
81
- end
82
- end
83
-
84
- def filter_reject(items, filters)
85
- reject_filters = filters_by_type(filters, "reject")
86
- return items if reject_filters.empty?
87
-
88
- # AND logic: all filters must pass
89
- reject_filters.each do |filter|
90
- filter_instance = ITEM_FILTER_CLASSES["reject"].new(filter)
91
- items = filter_instance.apply(items)
92
- end
93
- items
94
- end
95
-
96
- def result_select(result, filters)
97
- match_filters = filters_by_type(filters, "match")
98
- return result if match_filters.empty?
99
-
100
- # OR logic: any filter matches
101
- match_filters.any? do |filter|
102
- filter_instance = RESULT_FILTER_CLASSES["match"].new(filter)
103
- filter_instance.apply(result)
104
- end ? result : nil
105
- end
106
-
107
- def result_reject(result, filters)
108
- exclude_filters = filters_by_type(filters, "exclude")
109
- return result if exclude_filters.empty?
110
-
111
- # AND logic: all filters must pass
112
- exclude_filters.all? do |filter|
113
- filter_instance = RESULT_FILTER_CLASSES["exclude"].new(filter)
114
- filter_instance.apply(result)
115
- end ? result : nil
116
- end
117
-
118
- # Create a builder that always returns a constant value
119
- def constant_builder(key, str)
120
- {
121
- "key" => key,
122
- "items" => [constant_item(str)],
123
- "meta" => {},
124
- "filters" => []
125
- }
126
- end
127
-
128
- def constant_item(str, item = nil)
129
- segments = Segment::Parser.parse(str)
130
- citem = { "segments" => segments }
131
- if item
132
- citem["value"] = item["value"] if item["value"]
133
- citem["multiplier"] = item["multiplier"] if item["multiplier"]
134
- citem["filters"] = item["filters"] if item["filters"]
135
- citem["meta"] = item["meta"] if item["meta"]
136
- end
137
- citem
138
- end
139
-
140
- def filter_by_type(filters, type)
141
- filters&.find { |f| f["type"] == type }
142
- end
143
-
144
- def filters_by_type(filters, type)
145
- return [] if filters.nil? || filters.empty?
146
-
147
- filters.select { |f| f["type"] == type }
148
- end
149
-
150
- # Returns a hash of all available filters with their parameters
151
- # Filters that require explicit parameters will have an array of parameter names
152
- # Filters that don't require parameters will have an empty array
153
10
  def available_filters
154
- result_filters = RESULT_FILTER_CLASSES.keys.each_with_object({}) do |name, hash|
155
- hash[name] = filter_parameters(name)
156
- end
157
-
158
- item_filters = ITEM_FILTER_CLASSES.keys.each_with_object({}) do |name, hash|
159
- hash[name] = filter_parameters(name)
160
- end
161
-
162
- result_filters.merge(item_filters)
11
+ Base.subclasses
163
12
  end
164
13
 
165
- private
166
-
167
- # Define which filters require explicit parameters
168
- FILTERS_WITH_PARAMETERS = {
169
- "locale" => ["key"],
170
- "swap" => ["key", "value"],
171
- "meta" => ["key", "value"],
172
- "censor" => ["key", "value"],
173
- "match" => ["key", "value"],
174
- "exclude" => ["key", "value"],
175
- "select" => ["key", "value"],
176
- "reject" => ["key", "value"]
177
- }.freeze
14
+ def build(hsh_or_key, depth)
15
+ hsh = hsh_or_key.is_a?(Hash) ? hsh_or_key : key_to_hsh(hsh_or_key)
16
+ filter_class = available_filters.find { |f| f.filter_name == hsh["type"] }
17
+ filter_class.new(hsh, depth) if filter_class
18
+ end
178
19
 
179
- def filter_parameters(filter_name)
180
- FILTERS_WITH_PARAMETERS.fetch(filter_name, [])
20
+ def key_to_hsh(key)
21
+ type, key, value = key.split(":", 3)
22
+ { "type" => type, "text" => key, "value" => value }
181
23
  end
182
24
  end
183
25
  end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Text
4
+ module Gen
5
+ class Investigator
6
+ attr_reader :key, :lookup, :max_recursion
7
+
8
+ def initialize(key:, lookup:, max_recursion: 10)
9
+ @store = Store.new(lookup)
10
+ @key = key.to_s.downcase
11
+ @max_recursion = max_recursion
12
+ end
13
+
14
+ def investigate
15
+ visit_and_recurse(key, 0).sort.uniq
16
+ end
17
+
18
+ def visit_and_recurse(key, depth)
19
+ depth += 1
20
+ raise MaxRecursionError if depth > max_recursion
21
+
22
+ builder = store.fetch(key)
23
+ keys = [key]
24
+ builder["items"].each do |item|
25
+ item["segments"].each do |segment|
26
+ if segment["type"] == "reference"
27
+ keys << segment["text"]
28
+ keys += visit_and_recurse(segment["text"], depth)
29
+ end
30
+ end
31
+ end
32
+ keys
33
+ rescue StandardError
34
+ [key]
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Text
4
+ module Gen
5
+ class Meta
6
+ class << self
7
+ def merge_meta(lhs, rhs)
8
+ return {} unless lhs && rhs
9
+ return lhs.dup if rhs.empty?
10
+ return rhs.dup if lhs.empty?
11
+
12
+ hsh = {}
13
+ lhs.each { |k, v| append_kv(hsh, k, v) }
14
+ rhs.each { |k, v| append_kv(hsh, k, v) }
15
+ hsh
16
+ end
17
+
18
+ def append_kv(hsh, key, val)
19
+ key = key.to_s.downcase
20
+ arr = val.is_a?(Array) ? val : Array(val)
21
+ hsh[key] = hsh.key?(key) ? (hsh[key] + arr).uniq : arr.dup
22
+ hsh
23
+ end
24
+
25
+ def clear_kv(hsh, key, val)
26
+ return {} if key == "*" && val == "*"
27
+
28
+ new_hsh = {}
29
+ if key == "*"
30
+ hsh.each_key { |k| new_hsh[k] = hsh[k].dup.reject { |v| v == val } }
31
+ elsif val == "*"
32
+ hsh.each_key { |k| new_hsh[k] = hsh[k].dup unless k == key }
33
+ else
34
+ hsh.each_key do |k|
35
+ if hsh.key?(k)
36
+ new_hsh[k] = k == key ? hsh[k].select { |v| v != val } : hsh[k].dup
37
+ end
38
+ end
39
+ end
40
+
41
+ new_hsh
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -30,35 +30,19 @@ module Text
30
30
  end
31
31
 
32
32
  def merge_all_meta(results)
33
- return unless results
34
-
35
- results.each do |r|
36
- r.meta.each { |k, v| merge_kv(k, v) }
37
- end
33
+ (results || []).each { |r| @meta = Text::Gen::Meta.merge_meta(meta, r.meta) }
38
34
  end
39
35
 
40
36
  def merge_meta(hsh)
41
- return unless hsh
42
-
43
- hsh.each { |k, v| merge_kv(k, v) }
37
+ @meta = Text::Gen::Meta.merge_meta(meta, hsh)
44
38
  end
45
39
 
46
40
  def merge_kv(key, val)
47
- key = key.to_s.downcase
48
- arr = val.is_a?(Array) ? val : Array(val)
49
- meta[key] = meta.key?(key) ? (meta[key] + arr).uniq : arr.dup
41
+ Text::Gen::Meta.append_kv(meta, key, val)
50
42
  end
51
43
 
52
44
  def clear_meta(key, val)
53
- if key == "*" && val == "*"
54
- @meta = {}
55
- elsif key == "*"
56
- @meta.each_value { |v| v.delete(val) }
57
- elsif val == "*"
58
- @meta.delete(key)
59
- else
60
- @meta[key]&.delete(val)
61
- end
45
+ @meta = Text::Gen::Meta.clear_kv(meta, key, val)
62
46
  end
63
47
 
64
48
  def to_s
@@ -66,12 +50,13 @@ module Text
66
50
  end
67
51
 
68
52
  class << self
69
- def from(text:, type:, result:, value: nil, multiplier: nil)
53
+ def from(result, type:, text: nil, value: nil, multiplier: nil, meta: nil)
54
+ text ||= result.text
70
55
  value ||= result.value
71
56
  multiplier ||= result.multiplier
72
57
  new(text:, type:, value:, multiplier:).tap do |s|
73
58
  s.components.append(result)
74
- s.merge_meta(result.meta)
59
+ s.merge_meta(meta || result.meta)
75
60
  end
76
61
  end
77
62
 
@@ -5,16 +5,15 @@ module Text
5
5
  # ResultAccumulator handles accumulating results with attempt tracking
6
6
  class ResultAccumulator
7
7
  class << self
8
- def accumulate(unique:, count:, max_attempts:, &)
9
- new(unique: unique, count: count, max_attempts: max_attempts).accumulate(&)
8
+ def accumulate(count:, max_attempts:, &)
9
+ new(count: count, max_attempts: max_attempts).accumulate(&)
10
10
  end
11
11
  end
12
12
 
13
- def initialize(unique:, count:, max_attempts:)
14
- @unique = unique
13
+ def initialize(count:, max_attempts:)
15
14
  @count = [count, 1].max
16
15
  @max_attempts = [max_attempts, 1].max
17
- @results = unique ? {} : []
16
+ @results = []
18
17
  @attempts = max_attempts * count
19
18
  end
20
19
 
@@ -27,17 +26,13 @@ module Text
27
26
  raise MaxAttemptsError if @attempts.zero?
28
27
  end
29
28
 
30
- @results.map { |_, v| v }
29
+ @results
31
30
  end
32
31
 
33
32
  private
34
33
 
35
34
  def add_result(result)
36
- if @unique
37
- @results[result.text] = result
38
- else
39
- @results << [result.text, result]
40
- end
35
+ @results << result
41
36
  end
42
37
  end
43
38
  end