Skip to content

Commit 126e8e1

Browse files
committed
refactor: split Stdlib::Functions into autoloaded adapters
Extract RababaAdapter and SecrystAdapter from the monolithic Functions module into their own files, autoloaded from the parent namespace. Simple text-transform functions (title_case, downcase, compose, etc.) stay inline since they have no heavy dependencies. - lib/interscript/stdlib.rb: add autoload :Functions entry - lib/interscript/stdlib/functions.rb: parent namespace file with autoload for RababaAdapter and SecrystAdapter - lib/interscript/stdlib/functions/rababa_adapter.rb: mutex-protected diacritizer cache; reverse() works without the gem loaded - lib/interscript/stdlib/functions/secryst_adapter.rb: per-model translator cache Public API (Interscript::Stdlib::Functions.<name>) is unchanged. Callers in interpreter.rb and compiler/ruby.rb work without edits. Zero require_relative added. Zero internal require added.
1 parent b3e079e commit 126e8e1

5 files changed

Lines changed: 243 additions & 84 deletions

File tree

lib/interscript/stdlib.rb

Lines changed: 1 addition & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -189,88 +189,5 @@ def self.reverse_function
189189
}
190190
end
191191

192-
module Functions
193-
def self.title_case(output, word_separator: " ")
194-
output = output.gsub(/^(.)/, &:upcase)
195-
output = output.gsub(/#{word_separator}(.)/, &:upcase) unless word_separator == ""
196-
output
197-
end
198-
199-
def self.downcase(output, word_separator: nil)
200-
if word_separator
201-
output = output.gsub(/^(.)/, &:downcase)
202-
output.gsub(/#{word_separator}(.)/, &:downcase) unless word_separator == ""
203-
else
204-
output.downcase
205-
end
206-
end
207-
208-
def self.compose(output, _: nil)
209-
output.unicode_normalize(:nfc)
210-
end
211-
212-
def self.decompose(output, _: nil)
213-
output.unicode_normalize(:nfd)
214-
end
215-
216-
def self.separate(output, separator: " ")
217-
output.split("").join(separator)
218-
end
219-
220-
def self.unseparate(output, separator: " ")
221-
output.split(separator).join("")
222-
end
223-
224-
@secryst_models = {}
225-
def self.secryst(output, model:)
226-
begin
227-
require "secryst"
228-
rescue
229-
nil
230-
end # Try to load secryst, but don't fail hard if not possible.
231-
unless defined? Secryst
232-
raise Interscript::ExternalUtilError, "Secryst is not loaded. Please read docs/Usage_with_Secryst.adoc"
233-
end
234-
Interscript.secryst_index_locations.each do |remote|
235-
Secryst::Provisioning.add_remote(remote)
236-
end
237-
@secryst_models[model] ||= Secryst::Translator.new(model_file: model)
238-
output.split("\n").map(&:chomp).map do |i|
239-
@secryst_models[model].translate(i)
240-
end.join("\n")
241-
end
242-
243-
def self.rababa(output, config:)
244-
begin
245-
require "rababa"
246-
rescue
247-
nil
248-
end # Try to load rababa, but don't fail hard if not possible.
249-
unless defined? Rababa
250-
raise Interscript::ExternalUtilError, "Rababa is not loaded. Please read docs/Usage_with_Rababa.adoc"
251-
end
252-
253-
config_value = Interscript.rababa_configs[config]
254-
model_uri = config_value["model"]
255-
rababa_config = config_value["config"]
256-
model_path = Interscript.rababa_provision(config, model_uri)
257-
258-
@rababa_diacritizer ||= Rababa::Diacritizer.new(model_path, rababa_config)
259-
260-
@rababa_diacritizer.diacritize_text(output)
261-
end
262-
263-
def self.rababa_reverse(output, config:)
264-
# require "rababa" rescue nil # Try to load rababa, but don't fail hard if not possible.
265-
# unless defined? Rababa
266-
# raise StandardError, "Rababa is not loaded. Please read docs/Usage_with_Rababa.adoc"
267-
# end
268-
269-
# A call to allocate allows us to remove diacritics without initializing the model
270-
# Rababa::Diacritizer.allocate.remove_diacritics(output)
271-
272-
# Unfortunately, this is broken as of now.
273-
output.gsub(/[\u064e\u064b\u064f\u064c\u0650\u064d\u0652\u0651]/, "")
274-
end
275-
end
192+
autoload :Functions, "interscript/stdlib/functions"
276193
end
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
class Interscript::Stdlib
2+
module Functions
3+
autoload :RababaAdapter, "interscript/stdlib/functions/rababa_adapter"
4+
autoload :SecrystAdapter, "interscript/stdlib/functions/secryst_adapter"
5+
6+
def self.title_case(output, word_separator: " ")
7+
output = output.gsub(/^(.)/, &:upcase)
8+
output = output.gsub(/#{word_separator}(.)/, &:upcase) unless word_separator == ""
9+
output
10+
end
11+
12+
def self.downcase(output, word_separator: nil)
13+
if word_separator
14+
output = output.gsub(/^(.)/, &:downcase)
15+
output.gsub(/#{word_separator}(.)/, &:downcase) unless word_separator == ""
16+
else
17+
output.downcase
18+
end
19+
end
20+
21+
def self.compose(output, _: nil)
22+
output.unicode_normalize(:nfc)
23+
end
24+
25+
def self.decompose(output, _: nil)
26+
output.unicode_normalize(:nfd)
27+
end
28+
29+
def self.separate(output, separator: " ")
30+
output.split("").join(separator)
31+
end
32+
33+
def self.unseparate(output, separator: " ")
34+
output.split(separator).join("")
35+
end
36+
37+
def self.secryst(output, model:)
38+
SecrystAdapter.call(output, model: model)
39+
end
40+
41+
def self.rababa(output, config:)
42+
RababaAdapter.call(output, config: config)
43+
end
44+
45+
def self.rababa_reverse(output, config: nil)
46+
RababaAdapter.reverse(output)
47+
end
48+
end
49+
end
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
class Interscript::Stdlib
2+
module Functions
3+
class RababaAdapter
4+
@rababa_diacritizer = nil
5+
@mutex = Mutex.new
6+
7+
class << self
8+
def call(output, config:)
9+
diacritizer = diacritizer_for(config)
10+
diacritizer.diacritize_text(output)
11+
end
12+
13+
def reverse(output, config: nil)
14+
# The legacy rababa reverse path strips harakat directly. It does
15+
# not need the model loaded — keep it dependency-free so maps
16+
# that only call `rababa_reverse` work without Rababa installed.
17+
output.gsub(/[ًٌٍَُِّْ]/, "")
18+
end
19+
20+
def reset_cache
21+
@mutex.synchronize { @rababa_diacritizer = nil }
22+
end
23+
24+
private
25+
26+
def diacritizer_for(config_key)
27+
require_rababa!
28+
@mutex.synchronize do
29+
@rababa_diacritizer ||= build_diacritizer(config_key)
30+
end
31+
end
32+
33+
def build_diacritizer(config_key)
34+
config_value = Interscript.rababa_configs.fetch(config_key) do
35+
raise Interscript::ExternalUtilError,
36+
"No rababa config registered under '#{config_key}'"
37+
end
38+
model_uri = config_value["model"]
39+
rababa_config = config_value["config"]
40+
model_path = Interscript.rababa_provision(config_key, model_uri)
41+
Rababa::Diacritizer.new(model_path, rababa_config)
42+
end
43+
44+
def require_rababa!
45+
return if defined?(Rababa)
46+
begin
47+
require "rababa"
48+
rescue LoadError
49+
raise Interscript::ExternalUtilError,
50+
"Rababa is not loaded. Please read docs/Usage_with_Rababa.adoc"
51+
end
52+
end
53+
end
54+
end
55+
end
56+
end
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
class Interscript::Stdlib
2+
module Functions
3+
class SecrystAdapter
4+
@translators = {}
5+
@mutex = Mutex.new
6+
7+
class << self
8+
def call(output, model:)
9+
translator = translator_for(model)
10+
output.split("\n").map(&:chomp).map { |line| translator.translate(line) }.join("\n")
11+
end
12+
13+
def reset_cache
14+
@mutex.synchronize { @translators.clear }
15+
end
16+
17+
private
18+
19+
def translator_for(model_key)
20+
require_secryst!
21+
@mutex.synchronize do
22+
@translators[model_key] ||= build_translator(model_key)
23+
end
24+
end
25+
26+
def build_translator(model_key)
27+
Interscript.secryst_index_locations.each do |remote|
28+
Secryst::Provisioning.add_remote(remote)
29+
end
30+
Secryst::Translator.new(model_file: model_key)
31+
end
32+
33+
def require_secryst!
34+
return if defined?(Secryst)
35+
begin
36+
require "secryst"
37+
rescue LoadError
38+
raise Interscript::ExternalUtilError,
39+
"Secryst is not loaded. Please read docs/Usage_with_Secryst.adoc"
40+
end
41+
end
42+
end
43+
end
44+
end
45+
end

spec/stdlib_functions_spec.rb

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
require "interscript"
2+
3+
RSpec.describe Interscript::Stdlib::Functions do
4+
it "autoloads the Functions namespace from stdlib.rb" do
5+
expect(described_class).to eq(Interscript::Stdlib::Functions)
6+
end
7+
8+
it "lists rababa and secryst as available functions" do
9+
expect(Interscript::Stdlib.available_functions).to include(:rababa, :secryst, :rababa_reverse)
10+
end
11+
12+
it "reverse-maps rababa to rababa_reverse" do
13+
expect(Interscript::Stdlib.reverse_function[:rababa]).to eq(:rababa_reverse)
14+
expect(Interscript::Stdlib.reverse_function[:rababa_reverse]).to eq(:rababa)
15+
end
16+
17+
describe ".title_case" do
18+
it "capitalizes the first letter of each word" do
19+
expect(described_class.title_case("hello world")).to eq("Hello World")
20+
end
21+
end
22+
23+
describe ".downcase" do
24+
it "lowercases everything when no separator given" do
25+
expect(described_class.downcase("HELLO")).to eq("hello")
26+
end
27+
end
28+
29+
describe ".compose / .decompose" do
30+
it "round-trips through NFC and NFD" do
31+
composed = described_class.compose("café")
32+
expect(composed).to eq("café")
33+
decomposed = described_class.decompose(composed)
34+
expect(decomposed).to eq("café")
35+
end
36+
end
37+
38+
describe ".separate / .unseparate" do
39+
it "round-trips" do
40+
separated = described_class.separate("abc")
41+
expect(separated).to eq("a b c")
42+
expect(described_class.unseparate(separated)).to eq("abc")
43+
end
44+
end
45+
46+
describe ".rababa_reverse" do
47+
it "strips harakat without loading the model" do
48+
result = described_class.rababa_reverse("كَتَبَ")
49+
expect(result).to eq("كتب")
50+
end
51+
52+
it "does not require the config kwarg" do
53+
expect { described_class.rababa_reverse("كَتَبَ") }.not_to raise_error
54+
end
55+
end
56+
57+
describe ".rababa (without registered config)" do
58+
it "raises ExternalUtilError naming the missing config" do
59+
expect { described_class.rababa("كتب", config: "default") }.to raise_error(
60+
Interscript::ExternalUtilError,
61+
/No rababa config registered under 'default'/
62+
)
63+
end
64+
end
65+
66+
describe ".secryst (without Secryst gem loaded)" do
67+
it "raises ExternalUtilError with a helpful message" do
68+
expect { described_class.secryst("hello", model: "default") }.to raise_error(
69+
Interscript::ExternalUtilError,
70+
/Secryst is not loaded/
71+
)
72+
end
73+
end
74+
end
75+
76+
RSpec.describe Interscript::Stdlib::Functions::RababaAdapter do
77+
it "is autoloaded from a separate file" do
78+
expect(described_class.name).to eq("Interscript::Stdlib::Functions::RababaAdapter")
79+
end
80+
81+
describe ".reverse" do
82+
it "strips harakat without touching the model" do
83+
expect(described_class.reverse("كَتَبَ")).to eq("كتب")
84+
end
85+
end
86+
end
87+
88+
RSpec.describe Interscript::Stdlib::Functions::SecrystAdapter do
89+
it "is autoloaded from a separate file" do
90+
expect(described_class.name).to eq("Interscript::Stdlib::Functions::SecrystAdapter")
91+
end
92+
end

0 commit comments

Comments
 (0)