diff --git a/Rakefile b/Rakefile index ef60206b..0f8e4ad6 100644 --- a/Rakefile +++ b/Rakefile @@ -17,6 +17,9 @@ task :compile, [:compiler, :target] do |t, args| when "python" require "interscript/compiler/python" [Interscript::Compiler::Python, "py"] + when "json_ir" + require "interscript/compiler/json_ir" + [Interscript::Compiler::JsonIR, "json"] end FileUtils.mkdir_p(args[:target]) diff --git a/lib/interscript.rb b/lib/interscript.rb index cada6920..8fb82ade 100644 --- a/lib/interscript.rb +++ b/lib/interscript.rb @@ -1,7 +1,17 @@ -require "interscript/version" require "yaml" module Interscript + # Autoload all internal library code. No require_relative or internal + # require calls — everything loads lazily via autoload (OCP: adding a + # new child module = adding one autoload line here). + autoload :VERSION, "interscript/version" + autoload :Stdlib, "interscript/stdlib" + autoload :Compiler, "interscript/compiler" + autoload :Interpreter, "interscript/interpreter" + autoload :DSL, "interscript/dsl" + autoload :Node, "interscript/node" + autoload :Detector, "interscript/detector" + # An error caused by a lack of some map class MapNotFoundError < StandardError; end # An error caused by a missing dependency @@ -185,13 +195,3 @@ def exclude_maps(maps, compiler:, platform: true) end end end - -require "interscript/stdlib" - -require "interscript/compiler" -require "interscript/interpreter" - -require "interscript/dsl" -require "interscript/node" - -require "interscript/detector" diff --git a/lib/interscript/compiler.rb b/lib/interscript/compiler.rb index 73960af7..a664d205 100644 --- a/lib/interscript/compiler.rb +++ b/lib/interscript/compiler.rb @@ -1,5 +1,11 @@ # An Interscript compiler interface class Interscript::Compiler + # Autoload all compiler variants (OCP: new compiler = one autoload). + autoload :Javascript, "interscript/compiler/javascript" + autoload :Python, "interscript/compiler/python" + autoload :Ruby, "interscript/compiler/ruby" + autoload :JsonIR, "interscript/compiler/json_ir" + attr_accessor :code def self.call(map, **kwargs) diff --git a/lib/interscript/compiler/json_ir.rb b/lib/interscript/compiler/json_ir.rb new file mode 100644 index 00000000..ab8b8bd2 --- /dev/null +++ b/lib/interscript/compiler/json_ir.rb @@ -0,0 +1,243 @@ +# frozen_string_literal: true + +require "json" + +# JSON IR compiler — emits a data-only intermediate representation consumable +# by non-Ruby runtimes (e.g. interscript-ts). Unlike Compiler::Javascript, +# which emits imperative JS that calls into a runtime, this emits a pure-data +# JSON document describing the AST. +# +# The shape is versioned via SCHEMA_VERSION. Runtimes should refuse to load +# IR with an unknown schema. +class Interscript::Compiler::JsonIR < Interscript::Compiler + SCHEMA_VERSION = 1 + + CompiledResult = Struct.new(:code) do + # JsonIR output is already JSON. The runtime treats .code as opaque + # text; consumers parse via JSON.parse. + def to_json(*) + code + end + end + + def compile(map, debug: false) + @map = map + @c = JSON.pretty_generate(serialise_document(map)) + self + end + + def code + @c + end + + private + + def serialise_document(doc) + # Merge aliases from the document AND from dependency documents so + # that consumers (interscript-ts) can resolve every alias reference + # without re-implementing the Ruby dep_aliases indirection. + all_aliases = {} + + # Walk dependencies and merge their alias definitions. + # posix defines :upper, :lower; unicode defines :combining marks; etc. + doc.dependencies.each do |dep| + next unless dep.document + dep.document.aliases.each do |aname, defn| + all_aliases[aname.to_s] ||= serialise_item(defn.data) + end + end + + # Also walk dep_aliases (for run-rule dependency resolution paths). + doc.dep_aliases.each_value do |dep| + next unless dep.document + dep.document.aliases.each do |aname, defn| + all_aliases[aname.to_s] ||= serialise_item(defn.data) + end + end + + # Also merge ALL library aliases unconditionally. Libraries (posix, + # unicode, var-Cyrl, var-kor) define character classes that maps + # reference via alias() without listing the library as an explicit + # dependency in the dependency list. + Interscript.maps(libraries: true).each do |lib| + begin + libdoc = Interscript.parse(lib) + libdoc.aliases.each do |aname, defn| + all_aliases[aname.to_s] ||= serialise_item(defn.data) + end + rescue + # skip unparseable libraries + end + end + + # Document's own aliases override everything. + doc.aliases.each do |name, defn| + all_aliases[name.to_s] = serialise_item(defn.data) + end + + { + schemaVersion: SCHEMA_VERSION, + systemCode: doc.name.to_s, + dependencies: doc.dependencies.map(&:full_name), + metadata: serialise_metadata(doc.metadata), + stages: serialise_stages(doc.stages), + aliases: all_aliases, + functions: {} + } + end + + def serialise_metadata(metadata) + return {} unless metadata + out = {} + metadata.data.each do |k, v| + out[k.to_s] = case v + when Symbol then v.to_s + else v + end + end + out + end + + def serialise_stages(stages) + # Document#stages returns Hash{name => Stage} for the document's own stages, + # but may also include imported stages. Iterate values and skip anything + # that isn't an Interscript::Node::Stage (e.g. imported Hash entries). + stages.values.map { |s| serialise_stage(s) if s.is_a?(Interscript::Node::Stage) }.compact + end + + def serialise_stage(stage) + { + kind: "stage", + name: stage.name.to_s, + rules: stage.children.map { |r| serialise_rule(r) } + } + end + + def serialise_rule(rule) + case rule + when Interscript::Node::Rule::Sub + serialise_sub_rule(rule) + when Interscript::Node::Rule::Run + serialise_run_rule(rule) + when Interscript::Node::Rule::Funcall + serialise_funcall_rule(rule) + when Interscript::Node::Group::Parallel + # Parallel rule groups contain sub-rules that are applied simultaneously. + # In IR, we emit them as a marker rule so the runtime can decide. + { + kind: "parallel", + rules: rule.children.map { |r| serialise_rule(r) } + } + when Interscript::Node::Group::Sequential + { + kind: "sequential", + rules: rule.children.map { |r| serialise_rule(r) } + } + else + raise Interscript::MapLogicError, "Cannot serialise rule of type #{rule.class}" + end + end + + def serialise_sub_rule(rule) + out = {kind: "sub"} + out[:from] = serialise_item(rule.from) if rule.from + out[:to] = serialise_to(rule.to) + out[:before] = serialise_item(rule.before) if rule.before + out[:after] = serialise_item(rule.after) if rule.after + out[:notBefore] = serialise_item(rule.not_before) if rule.not_before + out[:notAfter] = serialise_item(rule.not_after) if rule.not_after + out[:priority] = rule.priority if rule.priority + out + end + + def serialise_run_rule(rule) + stage = rule.stage + doc_name = stage.map + if doc_name && @map.respond_to?(:dep_aliases) && @map.dep_aliases[doc_name.to_sym] + resolved = @map.dep_aliases[doc_name.to_sym].document + doc_name = resolved.name.to_s if resolved && resolved.respond_to?(:name) + end + { + kind: "run", + stage: stage.name.to_s, + docName: doc_name&.to_s + } + end + + def serialise_funcall_rule(rule) + { + kind: "funcall", + name: rule.name.to_s, + kwargs: symbolise_keys(rule.kwargs) + } + end + + def serialise_to(to) + case to + when Symbol + {kind: "funcall_inline", name: to.to_s} + else + serialise_item(to) + end + end + + def serialise_item(item) + case item + when Interscript::Node::Item::String + {kind: "string", value: item.data} + when Interscript::Node::Item::CaptureGroup + {kind: "capture_group", data: serialise_item(item.data)} + when Interscript::Node::Item::CaptureRef + {kind: "capture_ref", id: item.id} + when Interscript::Node::Item::Alias + out = {kind: "alias", name: item.name.to_s} + out[:map] = item.map if item.map + out + when Interscript::Node::Item::Any + # Any with a String payload becomes a character class in Ruby's + # regex compilation. Range payloads become `[first-last]`. Both + # must survive IR serialisation as char-class form, NOT expanded + # into alternatives — Ruby's String#succ expansion of "a".."ᖵ" + # produces nonsense like "zzz" and misses most of the BMP. + case item.value + when ::Range + {kind: "any_char_class", range: [item.value.first, item.value.last]} + when ::String + {kind: "any_char_class", chars: item.value.split("")} + else + data = item.data || [] + {kind: "any", of: data.map { |i| serialise_item(i) }} + end + when Interscript::Node::Item::Group + {kind: "group", items: item.children.map { |i| serialise_item(i) }} + when Interscript::Node::Item::Repeat + {kind: "repeat", item: serialise_item(item.data), min: 0, max: nil} + when Interscript::Node::Item::Stage + {kind: "stage_ref", name: item.name.to_s} + when nil + nil + else + raise Interscript::MapLogicError, "Cannot serialise item of type #{item.class}" + end + end + + def capture_index(_item) + # CaptureGroup index is determined by position in pattern compilation. + # Runtimes should treat as a placeholder; the actual index is computed + # during pattern compilation based on capture-group ordering. + 0 + end + + def serialise_aliases(aliases) + out = {} + aliases.each do |name, defn| + out[name.to_s] = serialise_item(defn.data) + end + out + end + + def symbolise_keys(hash) + return {} if hash.nil? + hash.transform_keys(&:to_s) + end +end diff --git a/lib/interscript/dsl.rb b/lib/interscript/dsl.rb index c9a7e6c3..f6e0f2c5 100644 --- a/lib/interscript/dsl.rb +++ b/lib/interscript/dsl.rb @@ -95,12 +95,14 @@ def self.parse(map_name, reverse: true) end end -require "interscript/dsl/symbol_mm" -require "interscript/dsl/items" - -require "interscript/dsl/document" -require "interscript/dsl/group" -require "interscript/dsl/stage" -require "interscript/dsl/metadata" -require "interscript/dsl/tests" -require "interscript/dsl/aliases" +module Interscript::DSL + # Autoload all DSL modules (OCP: new DSL section = one autoload). + autoload :SymbolMM, "interscript/dsl/symbol_mm" + autoload :Items, "interscript/dsl/items" + autoload :Document, "interscript/dsl/document" + autoload :Group, "interscript/dsl/group" + autoload :Stage, "interscript/dsl/stage" + autoload :Metadata, "interscript/dsl/metadata" + autoload :Tests, "interscript/dsl/tests" + autoload :Aliases, "interscript/dsl/aliases" +end diff --git a/lib/interscript/dsl/group.rb b/lib/interscript/dsl/group.rb index 3c5da955..c49a1f85 100644 --- a/lib/interscript/dsl/group.rb +++ b/lib/interscript/dsl/group.rb @@ -50,4 +50,6 @@ def parallel(**kwargs, &block) end end -require "interscript/dsl/group/parallel" +class Interscript::DSL::Group + autoload :Parallel, "interscript/dsl/group/parallel" +end diff --git a/lib/interscript/node.rb b/lib/interscript/node.rb index bb7f6995..faa9915d 100644 --- a/lib/interscript/node.rb +++ b/lib/interscript/node.rb @@ -1,4 +1,16 @@ class Interscript::Node + # Autoload all node types. Adding a new node type = one autoload + # line here. No require_relative (OCP). + autoload :Group, "interscript/node/group" + autoload :Document, "interscript/node/document" + autoload :MetaData, "interscript/node/metadata" + autoload :AliasDef, "interscript/node/alias_def" + autoload :Dependency, "interscript/node/dependency" + autoload :Tests, "interscript/node/tests" + autoload :Stage, "interscript/node/stage" + autoload :Rule, "interscript/node/rule" + autoload :Item, "interscript/node/item" + def initialize raise NotImplementedError, "You can't construct a Node directly" end @@ -12,15 +24,3 @@ def to_hash question: "is something missing?"} end end - -require "interscript/node/group" -require "interscript/node/document" - -require "interscript/node/metadata" -require "interscript/node/alias_def" -require "interscript/node/dependency" -require "interscript/node/tests" - -require "interscript/node/stage" -require "interscript/node/rule" -require "interscript/node/item" diff --git a/lib/interscript/node/group.rb b/lib/interscript/node/group.rb index f0a26937..82e16953 100644 --- a/lib/interscript/node/group.rb +++ b/lib/interscript/node/group.rb @@ -41,5 +41,7 @@ def inspect end end -require "interscript/node/group/parallel" -require "interscript/node/group/sequential" +class Interscript::Node::Group + autoload :Parallel, "interscript/node/group/parallel" + autoload :Sequential, "interscript/node/group/sequential" +end diff --git a/lib/interscript/node/item.rb b/lib/interscript/node/item.rb index 7584620d..ff09a1f6 100644 --- a/lib/interscript/node/item.rb +++ b/lib/interscript/node/item.rb @@ -47,10 +47,18 @@ def self.try_convert(i) end end -require "interscript/node/item/alias" -require "interscript/node/item/string" -require "interscript/node/item/group" -require "interscript/node/item/any" -require "interscript/node/item/stage" -require "interscript/node/item/capture" -require "interscript/node/item/repeat" +class Interscript::Node::Item + # Autoload all item types (OCP: new item type = one autoload). + autoload :Alias, "interscript/node/item/alias" + autoload :String, "interscript/node/item/string" + autoload :Group, "interscript/node/item/group" + autoload :Any, "interscript/node/item/any" + autoload :Stage, "interscript/node/item/stage" + autoload :CaptureGroup, "interscript/node/item/capture" + autoload :Repeat, "interscript/node/item/repeat" + # Maybe, MaybeSome, Some are subclasses of Repeat defined in + # the same file. Autoload points to repeat.rb which defines all of them. + autoload :Maybe, "interscript/node/item/repeat" + autoload :MaybeSome, "interscript/node/item/repeat" + autoload :Some, "interscript/node/item/repeat" +end diff --git a/lib/interscript/node/rule.rb b/lib/interscript/node/rule.rb index 03dd9115..539507e4 100644 --- a/lib/interscript/node/rule.rb +++ b/lib/interscript/node/rule.rb @@ -1,9 +1,10 @@ class Interscript::Node::Rule < Interscript::Node + # Autoload all rule types (OCP: new rule type = one autoload). + autoload :Sub, "interscript/node/rule/sub" + autoload :Run, "interscript/node/rule/run" + autoload :Funcall, "interscript/node/rule/funcall" + def ==(other) super && reverse_run == other.reverse_run end end - -require "interscript/node/rule/sub" -require "interscript/node/rule/run" -require "interscript/node/rule/funcall" diff --git a/lib/interscript/stdlib.rb b/lib/interscript/stdlib.rb index 60812946..a5a68392 100644 --- a/lib/interscript/stdlib.rb +++ b/lib/interscript/stdlib.rb @@ -189,88 +189,5 @@ def self.reverse_function } end - module Functions - def self.title_case(output, word_separator: " ") - output = output.gsub(/^(.)/, &:upcase) - output = output.gsub(/#{word_separator}(.)/, &:upcase) unless word_separator == "" - output - end - - def self.downcase(output, word_separator: nil) - if word_separator - output = output.gsub(/^(.)/, &:downcase) - output.gsub(/#{word_separator}(.)/, &:downcase) unless word_separator == "" - else - output.downcase - end - end - - def self.compose(output, _: nil) - output.unicode_normalize(:nfc) - end - - def self.decompose(output, _: nil) - output.unicode_normalize(:nfd) - end - - def self.separate(output, separator: " ") - output.split("").join(separator) - end - - def self.unseparate(output, separator: " ") - output.split(separator).join("") - end - - @secryst_models = {} - def self.secryst(output, model:) - begin - require "secryst" - rescue - nil - end # Try to load secryst, but don't fail hard if not possible. - unless defined? Secryst - raise Interscript::ExternalUtilError, "Secryst is not loaded. Please read docs/Usage_with_Secryst.adoc" - end - Interscript.secryst_index_locations.each do |remote| - Secryst::Provisioning.add_remote(remote) - end - @secryst_models[model] ||= Secryst::Translator.new(model_file: model) - output.split("\n").map(&:chomp).map do |i| - @secryst_models[model].translate(i) - end.join("\n") - end - - def self.rababa(output, config:) - begin - require "rababa" - rescue - nil - end # Try to load rababa, but don't fail hard if not possible. - unless defined? Rababa - raise Interscript::ExternalUtilError, "Rababa is not loaded. Please read docs/Usage_with_Rababa.adoc" - end - - config_value = Interscript.rababa_configs[config] - model_uri = config_value["model"] - rababa_config = config_value["config"] - model_path = Interscript.rababa_provision(config, model_uri) - - @rababa_diacritizer ||= Rababa::Diacritizer.new(model_path, rababa_config) - - @rababa_diacritizer.diacritize_text(output) - end - - def self.rababa_reverse(output, config:) - # require "rababa" rescue nil # Try to load rababa, but don't fail hard if not possible. - # unless defined? Rababa - # raise StandardError, "Rababa is not loaded. Please read docs/Usage_with_Rababa.adoc" - # end - - # A call to allocate allows us to remove diacritics without initializing the model - # Rababa::Diacritizer.allocate.remove_diacritics(output) - - # Unfortunately, this is broken as of now. - output.gsub(/[\u064e\u064b\u064f\u064c\u0650\u064d\u0652\u0651]/, "") - end - end + autoload :Functions, "interscript/stdlib/functions" end diff --git a/lib/interscript/stdlib/functions.rb b/lib/interscript/stdlib/functions.rb new file mode 100644 index 00000000..0f6cdbbd --- /dev/null +++ b/lib/interscript/stdlib/functions.rb @@ -0,0 +1,49 @@ +class Interscript::Stdlib + module Functions + autoload :RababaAdapter, "interscript/stdlib/functions/rababa_adapter" + autoload :SecrystAdapter, "interscript/stdlib/functions/secryst_adapter" + + def self.title_case(output, word_separator: " ") + output = output.gsub(/^(.)/, &:upcase) + output = output.gsub(/#{word_separator}(.)/, &:upcase) unless word_separator == "" + output + end + + def self.downcase(output, word_separator: nil) + if word_separator + output = output.gsub(/^(.)/, &:downcase) + output.gsub(/#{word_separator}(.)/, &:downcase) unless word_separator == "" + else + output.downcase + end + end + + def self.compose(output, _: nil) + output.unicode_normalize(:nfc) + end + + def self.decompose(output, _: nil) + output.unicode_normalize(:nfd) + end + + def self.separate(output, separator: " ") + output.split("").join(separator) + end + + def self.unseparate(output, separator: " ") + output.split(separator).join("") + end + + def self.secryst(output, model:) + SecrystAdapter.call(output, model: model) + end + + def self.rababa(output, config:) + RababaAdapter.call(output, config: config) + end + + def self.rababa_reverse(output, config: nil) + RababaAdapter.reverse(output) + end + end +end diff --git a/lib/interscript/stdlib/functions/rababa_adapter.rb b/lib/interscript/stdlib/functions/rababa_adapter.rb new file mode 100644 index 00000000..1cf79d77 --- /dev/null +++ b/lib/interscript/stdlib/functions/rababa_adapter.rb @@ -0,0 +1,56 @@ +class Interscript::Stdlib + module Functions + class RababaAdapter + @rababa_diacritizer = nil + @mutex = Mutex.new + + class << self + def call(output, config:) + diacritizer = diacritizer_for(config) + diacritizer.diacritize_text(output) + end + + def reverse(output, config: nil) + # The legacy rababa reverse path strips harakat directly. It does + # not need the model loaded — keep it dependency-free so maps + # that only call `rababa_reverse` work without Rababa installed. + output.gsub(/[ًٌٍَُِّْ]/, "") + end + + def reset_cache + @mutex.synchronize { @rababa_diacritizer = nil } + end + + private + + def diacritizer_for(config_key) + require_rababa! + @mutex.synchronize do + @rababa_diacritizer ||= build_diacritizer(config_key) + end + end + + def build_diacritizer(config_key) + config_value = Interscript.rababa_configs.fetch(config_key) do + raise Interscript::ExternalUtilError, + "No rababa config registered under '#{config_key}'" + end + model_uri = config_value["model"] + rababa_config = config_value["config"] + model_path = Interscript.rababa_provision(config_key, model_uri) + Rababa::Diacritizer.new(model_path, rababa_config) + end + + def require_rababa! + return if defined?(Rababa) + begin + require "rababa" + rescue LoadError + raise Interscript::ExternalUtilError, + "Rababa is not loaded. Please read docs/Usage_with_Rababa.adoc" + end + end + end + end + end +end diff --git a/lib/interscript/stdlib/functions/secryst_adapter.rb b/lib/interscript/stdlib/functions/secryst_adapter.rb new file mode 100644 index 00000000..9b6e009e --- /dev/null +++ b/lib/interscript/stdlib/functions/secryst_adapter.rb @@ -0,0 +1,45 @@ +class Interscript::Stdlib + module Functions + class SecrystAdapter + @translators = {} + @mutex = Mutex.new + + class << self + def call(output, model:) + translator = translator_for(model) + output.split("\n").map(&:chomp).map { |line| translator.translate(line) }.join("\n") + end + + def reset_cache + @mutex.synchronize { @translators.clear } + end + + private + + def translator_for(model_key) + require_secryst! + @mutex.synchronize do + @translators[model_key] ||= build_translator(model_key) + end + end + + def build_translator(model_key) + Interscript.secryst_index_locations.each do |remote| + Secryst::Provisioning.add_remote(remote) + end + Secryst::Translator.new(model_file: model_key) + end + + def require_secryst! + return if defined?(Secryst) + begin + require "secryst" + rescue LoadError + raise Interscript::ExternalUtilError, + "Secryst is not loaded. Please read docs/Usage_with_Secryst.adoc" + end + end + end + end + end +end diff --git a/lib/interscript/visualize.rb b/lib/interscript/visualize.rb index 9e4add80..d8065a83 100644 --- a/lib/interscript/visualize.rb +++ b/lib/interscript/visualize.rb @@ -1,12 +1,9 @@ require "erb" -require "interscript/visualize/nodes" -require "interscript/visualize/json" - -def h(str) - str.to_s.gsub("&", "&").gsub("<", "<").gsub(">", ">").gsub('"', """) -end class Interscript::Visualize + autoload :Nodes, "interscript/visualize/nodes" + autoload :JSON, "interscript/visualize/json" + def self.def_template(template) @template = ERB.new(File.read(__dir__ + "/visualize/#{template}.html.erb")) end diff --git a/spec/stdlib_functions_spec.rb b/spec/stdlib_functions_spec.rb new file mode 100644 index 00000000..17b1e716 --- /dev/null +++ b/spec/stdlib_functions_spec.rb @@ -0,0 +1,92 @@ +require "interscript" + +RSpec.describe Interscript::Stdlib::Functions do + it "autoloads the Functions namespace from stdlib.rb" do + expect(described_class).to eq(Interscript::Stdlib::Functions) + end + + it "lists rababa and secryst as available functions" do + expect(Interscript::Stdlib.available_functions).to include(:rababa, :secryst, :rababa_reverse) + end + + it "reverse-maps rababa to rababa_reverse" do + expect(Interscript::Stdlib.reverse_function[:rababa]).to eq(:rababa_reverse) + expect(Interscript::Stdlib.reverse_function[:rababa_reverse]).to eq(:rababa) + end + + describe ".title_case" do + it "capitalizes the first letter of each word" do + expect(described_class.title_case("hello world")).to eq("Hello World") + end + end + + describe ".downcase" do + it "lowercases everything when no separator given" do + expect(described_class.downcase("HELLO")).to eq("hello") + end + end + + describe ".compose / .decompose" do + it "round-trips through NFC and NFD" do + composed = described_class.compose("café") + expect(composed).to eq("café") + decomposed = described_class.decompose(composed) + expect(decomposed).to eq("café") + end + end + + describe ".separate / .unseparate" do + it "round-trips" do + separated = described_class.separate("abc") + expect(separated).to eq("a b c") + expect(described_class.unseparate(separated)).to eq("abc") + end + end + + describe ".rababa_reverse" do + it "strips harakat without loading the model" do + result = described_class.rababa_reverse("كَتَبَ") + expect(result).to eq("كتب") + end + + it "does not require the config kwarg" do + expect { described_class.rababa_reverse("كَتَبَ") }.not_to raise_error + end + end + + describe ".rababa (without registered config)" do + it "raises ExternalUtilError naming the missing config" do + expect { described_class.rababa("كتب", config: "default") }.to raise_error( + Interscript::ExternalUtilError, + /No rababa config registered under 'default'/ + ) + end + end + + describe ".secryst (without Secryst gem loaded)" do + it "raises ExternalUtilError with a helpful message" do + expect { described_class.secryst("hello", model: "default") }.to raise_error( + Interscript::ExternalUtilError, + /Secryst is not loaded/ + ) + end + end +end + +RSpec.describe Interscript::Stdlib::Functions::RababaAdapter do + it "is autoloaded from a separate file" do + expect(described_class.name).to eq("Interscript::Stdlib::Functions::RababaAdapter") + end + + describe ".reverse" do + it "strips harakat without touching the model" do + expect(described_class.reverse("كَتَبَ")).to eq("كتب") + end + end +end + +RSpec.describe Interscript::Stdlib::Functions::SecrystAdapter do + it "is autoloaded from a separate file" do + expect(described_class.name).to eq("Interscript::Stdlib::Functions::SecrystAdapter") + end +end