///|
/// A compact, line-oriented scenario format for teams that want to keep their
/// covering-array model beside design docs, issues or CI configuration.
pub(all) struct ScenarioSpec {
name : String
model : Model
constraints : Array[Constraint]
options : GenerationOptions
included_cases : Array[TestCase]
excluded_patterns : Array[CasePattern]
value_risks : Array[ValueRisk]
interaction_risks : Array[InteractionRisk]
}
///|
/// A partial assignment such as `os=linux, browser=chrome`.
pub(all) struct CasePattern {
choices : Array[NamedChoice]
} derive(Debug, Eq)
///|
/// A risk attached to one parameter value.
pub(all) struct ValueRisk {
choice : NamedChoice
weight : Int
note : String
} derive(Debug, Eq)
///|
/// A risk attached to an interaction pattern.
pub(all) struct InteractionRisk {
pattern : CasePattern
weight : Int
note : String
} derive(Debug, Eq)
///|
/// Risk score for one concrete case.
pub(all) struct CaseRisk {
score : Int
reasons : Array[String]
} derive(Debug, Eq)
///|
/// A generated case ordered by descending risk score.
pub(all) struct RankedCase {
rank : Int
index : Int
test_case : TestCase
score : Int
reasons : Array[String]
} derive(Debug, Eq)
///|
/// End-to-end execution result for a parsed scenario spec.
pub(all) struct ScenarioRun {
spec_name : String
model : Model
suite : Suite
cases : Array[TestCase]
included_case_count : Int
coverage : CoverageReport
ranked_cases : Array[RankedCase]
}
///|
/// Non-blocking quality hints for scenario specifications.
pub(all) enum SpecWarningKind {
NoConstraints
NoRiskHints
NoIncludedCases
StrengthOne
CandidateLimitBelowCartesian(Int64, Int)
SingleValueParameter(String)
} derive(Debug, Eq)
///|
pub(all) struct SpecWarning {
kind : SpecWarningKind
message : String
} derive(Debug, Eq)
///|
priv struct TextParts {
left : String
right : String
}
///|
priv struct RawConstraintLine {
label : String
source : String
}
///|
priv struct RawPatternLine {
pattern : CasePattern
line : Int
}
///|
priv struct RawRiskLine {
pattern : CasePattern
weight : Int
note : String
line : Int
}
///|
priv struct RawSpecBuilder {
mut name : String
parameters : Array[Parameter]
constraints : Array[RawConstraintLine]
includes : Array[RawPatternLine]
excludes : Array[RawPatternLine]
risks : Array[RawRiskLine]
mut strength : Int
mut max_candidates : Int
mut minimize : Bool
}
///|
fn RawSpecBuilder::new() -> RawSpecBuilder {
{
name: "CaseWeave scenario",
parameters: [],
constraints: [],
includes: [],
excludes: [],
risks: [],
strength: 2,
max_candidates: 100000,
minimize: true,
}
}
///|
pub fn ScenarioSpec::parse(
source : String,
) -> Result[ScenarioSpec, CaseWeaveError] {
parse_scenario_spec(source)
}
///|
pub fn parse_scenario_spec(
source : String,
) -> Result[ScenarioSpec, CaseWeaveError] {
let builder = RawSpecBuilder::new()
let lines = split_lines(source)
for line_index = 0; line_index < lines.length(); line_index = line_index + 1 {
match parse_spec_line(builder, line_index + 1, lines[line_index]) {
Err(error) => return Err(error)
Ok(_) => ()
}
}
builder.finish()
}
///|
fn RawSpecBuilder::finish(
self : RawSpecBuilder,
) -> Result[ScenarioSpec, CaseWeaveError] {
let model = match Model::new(self.parameters) {
Err(error) => return Err(error)
Ok(value) => value
}
if self.strength < 1 || self.strength > model.parameter_count() {
return Err(InvalidStrength(self.strength, model.parameter_count()))
}
if self.max_candidates < 1 {
return Err(InvalidCandidateLimit(self.max_candidates))
}
let constraints : Array[Constraint] = []
for raw in self.constraints {
let parsed = match Constraint::parse(raw.label, raw.source) {
Err(error) => return Err(error)
Ok(value) => value
}
match parsed.validate(model) {
Err(error) => return Err(error)
Ok(_) => constraints.push(parsed)
}
}
let excluded_patterns : Array[CasePattern] = []
for raw in self.excludes {
match raw.pattern.validate(model) {
Err(error) => return Err(error)
Ok(_) => ()
}
let label = "exclude line \{raw.line}: \{raw.pattern.to_text()}"
let constraint = match raw.pattern.to_exclusion_constraint(label) {
Err(error) => return Err(error)
Ok(value) => value
}
match constraint.validate(model) {
Err(error) => return Err(error)
Ok(_) => ()
}
constraints.push(constraint)
excluded_patterns.push(raw.pattern)
}
let included_cases : Array[TestCase] = []
for raw in self.includes {
let test_case = match raw.pattern.to_case(model, line_no=raw.line) {
Err(error) => return Err(error)
Ok(value) => value
}
match
validate_case_against_constraints(
model,
test_case,
constraints,
included_cases.length(),
) {
Err(error) => return Err(error)
Ok(_) => included_cases.push(test_case)
}
}
let value_risks : Array[ValueRisk] = []
let interaction_risks : Array[InteractionRisk] = []
for raw in self.risks {
match raw.pattern.validate(model) {
Err(error) => return Err(error)
Ok(_) => ()
}
if raw.weight <= 0 {
return Err(ParseError(raw.line, "risk weight must be positive"))
}
if raw.pattern.choices.length() == 1 {
value_risks.push({
choice: raw.pattern.choices[0],
weight: raw.weight,
note: raw.note,
})
} else {
interaction_risks.push({
pattern: raw.pattern,
weight: raw.weight,
note: raw.note,
})
}
}
Ok({
name: self.name,
model,
constraints,
options: {
strength: self.strength,
max_candidates: self.max_candidates,
minimize: self.minimize,
},
included_cases,
excluded_patterns,
value_risks,
interaction_risks,
})
}
///|
fn validate_case_against_constraints(
model : Model,
test_case : TestCase,
constraints : Array[Constraint],
case_index : Int,
) -> Result[Unit, CaseWeaveError] {
match model.validate_case(test_case) {
Err(error) => return Err(error)
Ok(_) => ()
}
for constraint in constraints {
match constraint.matches(model, test_case) {
Err(error) => return Err(error)
Ok(true) => ()
Ok(false) => return Err(ConstraintViolation(case_index, constraint.label))
}
}
Ok(())
}
///|
fn parse_spec_line(
builder : RawSpecBuilder,
line_no : Int,
source : String,
) -> Result[Unit, CaseWeaveError] {
let line = trim_text(source)
if line.length() == 0 {
return Ok(())
}
if starts_with_text(line, "#") {
return Ok(())
}
let parts = match split_once_char(line, ':') {
None => return Err(ParseError(line_no, "expected ':' in scenario line"))
Some(value) => value
}
let head = trim_text(parts.left)
let body = trim_text(parts.right)
if head.length() == 0 {
return Err(ParseError(line_no, "missing directive name"))
}
if directive_is(head, "name") || directive_is(head, "title") {
if body.length() == 0 {
return Err(ParseError(line_no, "scenario name must not be empty"))
}
builder.name = body
return Ok(())
}
if directive_is(head, "strength") {
builder.strength = match parse_positive_int(body, line_no) {
Err(error) => return Err(error)
Ok(value) => value
}
return Ok(())
}
if directive_is(head, "max-candidates") ||
directive_is(head, "max_candidates") ||
directive_is(head, "limit") {
builder.max_candidates = match parse_positive_int(body, line_no) {
Err(error) => return Err(error)
Ok(value) => value
}
return Ok(())
}
if directive_is(head, "minimize") {
builder.minimize = match parse_bool(body, line_no) {
Err(error) => return Err(error)
Ok(value) => value
}
return Ok(())
}
match strip_directive(head, "param") {
Some(parameter_name) =>
return parse_parameter_directive(builder, line_no, parameter_name, body)
None => ()
}
match strip_directive(head, "parameter") {
Some(parameter_name) =>
return parse_parameter_directive(builder, line_no, parameter_name, body)
None => ()
}
match strip_directive(head, "constraint") {
Some(label) => {
let normalized_label = if label.length() == 0 {
"constraint line \{line_no}"
} else {
label
}
if body.length() == 0 {
return Err(ParseError(line_no, "constraint expression is empty"))
}
builder.constraints.push({ label: normalized_label, source: body })
return Ok(())
}
None => ()
}
if directive_is(head, "include") ||
directive_is(head, "case") ||
directive_is(head, "must") {
let pattern = match parse_case_pattern(body, line_no) {
Err(error) => return Err(error)
Ok(value) => value
}
builder.includes.push({ pattern, line: line_no })
return Ok(())
}
if directive_is(head, "exclude") ||
directive_is(head, "forbid") ||
directive_is(head, "never") {
let pattern = match parse_case_pattern(body, line_no) {
Err(error) => return Err(error)
Ok(value) => value
}
builder.excludes.push({ pattern, line: line_no })
return Ok(())
}
match strip_directive(head, "risk") {
Some(selector) =>
return parse_risk_directive(builder, line_no, selector, body)
None => ()
}
Err(ParseError(line_no, "unknown scenario directive"))
}
///|
fn parse_parameter_directive(
builder : RawSpecBuilder,
line_no : Int,
parameter_name : String,
body : String,
) -> Result[Unit, CaseWeaveError] {
let name = trim_text(parameter_name)
if name.length() == 0 {
return Err(ParseError(line_no, "parameter name must not be empty"))
}
let values = match split_csv_values(body, line_no) {
Err(error) => return Err(error)
Ok(value) => value
}
builder.parameters.push(Parameter::new(name, values))
Ok(())
}
///|
fn parse_risk_directive(
builder : RawSpecBuilder,
line_no : Int,
selector : String,
body : String,
) -> Result[Unit, CaseWeaveError] {
let pattern = match parse_case_pattern(selector, line_no) {
Err(error) => return Err(error)
Ok(value) => value
}
let parts = split_once_char(body, ':')
let weight_text = match parts {
None => body
Some(value) => trim_text(value.left)
}
let note = match parts {
None => ""
Some(value) => trim_text(value.right)
}
let weight = match parse_positive_int(weight_text, line_no) {
Err(error) => return Err(error)
Ok(value) => value
}
builder.risks.push({ pattern, weight, note, line: line_no })
Ok(())
}
///|
pub fn ScenarioSpec::generate_suite(
self : ScenarioSpec,
) -> Result[Suite, CaseWeaveError] {
generate(self.model, constraints=self.constraints, options=self.options)
}
///|
pub fn ScenarioSpec::run(
self : ScenarioSpec,
) -> Result[ScenarioRun, CaseWeaveError] {
let suite = match self.generate_suite() {
Err(error) => return Err(error)
Ok(value) => value
}
let cases = merge_cases(self.included_cases, suite.cases())
let coverage = match
audit(
self.model,
cases,
strength=self.options.strength,
constraints=self.constraints,
max_candidates=self.options.max_candidates,
) {
Err(error) => return Err(error)
Ok(value) => value
}
let ranked_cases = match self.rank_cases(cases) {
Err(error) => return Err(error)
Ok(value) => value
}
Ok({
spec_name: self.name,
model: self.model,
suite,
cases,
included_case_count: self.included_cases.length(),
coverage,
ranked_cases,
})
}
///|
fn merge_cases(
included : Array[TestCase],
generated : Array[TestCase],
) -> Array[TestCase] {
let cases : Array[TestCase] = []
for test_case in included {
if !contains_test_case(cases, test_case) {
cases.push(test_case)
}
}
for test_case in generated {
if !contains_test_case(cases, test_case) {
cases.push(test_case)
}
}
cases
}
///|
fn contains_test_case(cases : Array[TestCase], target : TestCase) -> Bool {
for test_case in cases {
if test_case == target {
return true
}
}
false
}
///|
pub fn ScenarioSpec::score_case(
self : ScenarioSpec,
test_case : TestCase,
) -> Result[CaseRisk, CaseWeaveError] {
match self.model.validate_case(test_case) {
Err(error) => return Err(error)
Ok(_) => ()
}
let reasons : Array[String] = []
let mut score = 0
for risk in self.value_risks {
if case_has_choice(self.model, test_case, risk.choice) {
score = score + risk.weight
reasons.push(risk_reason(risk.choice, risk.weight, risk.note))
}
}
for risk in self.interaction_risks {
match risk.pattern.matches(self.model, test_case) {
Err(error) => return Err(error)
Ok(true) => {
score = score + risk.weight
reasons.push(pattern_risk_reason(risk.pattern, risk.weight, risk.note))
}
Ok(false) => ()
}
}
Ok({ score, reasons })
}
///|
pub fn ScenarioSpec::rank_cases(
self : ScenarioSpec,
cases : Array[TestCase],
) -> Result[Array[RankedCase], CaseWeaveError] {
let ranked : Array[RankedCase] = []
for index = 0; index < cases.length(); index = index + 1 {
let risk = match self.score_case(cases[index]) {
Err(error) => return Err(error)
Ok(value) => value
}
ranked.push({
rank: 0,
index,
test_case: cases[index],
score: risk.score,
reasons: risk.reasons,
})
}
ranked.sort_by(compare_ranked_case)
for index = 0; index < ranked.length(); index = index + 1 {
let item = ranked[index]
ranked[index] = {
rank: index + 1,
index: item.index,
test_case: item.test_case,
score: item.score,
reasons: item.reasons,
}
}
Ok(ranked)
}
///|
fn compare_ranked_case(left : RankedCase, right : RankedCase) -> Int {
if left.score > right.score {
-1
} else if left.score < right.score {
1
} else if left.index < right.index {
-1
} else if left.index > right.index {
1
} else {
0
}
}
///|
fn case_has_choice(
model : Model,
test_case : TestCase,
choice : NamedChoice,
) -> Bool {
match test_case.value_for(model, choice.parameter) {
Some(value) => value == choice.value
None => false
}
}
///|
fn risk_reason(choice : NamedChoice, weight : Int, note : String) -> String {
let suffix = if note.length() == 0 { "" } else { " — \{note}" }
"\{choice.parameter}=\{choice.value} +\{weight}\{suffix}"
}
///|
fn pattern_risk_reason(
pattern : CasePattern,
weight : Int,
note : String,
) -> String {
let suffix = if note.length() == 0 { "" } else { " — \{note}" }
"\{pattern.to_text()} +\{weight}\{suffix}"
}
///|
pub fn CasePattern::validate(
self : CasePattern,
model : Model,
) -> Result[Unit, CaseWeaveError] {
if self.choices.length() == 0 {
return Err(ParseError(0, "case pattern must not be empty"))
}
for index = 0; index < self.choices.length(); index = index + 1 {
let choice = self.choices[index]
for previous = 0; previous < index; previous = previous + 1 {
if self.choices[previous].parameter == choice.parameter {
return Err(DuplicateParameter(choice.parameter))
}
}
let parameter_index = match model.parameter_index(choice.parameter) {
None => return Err(UnknownParameter(choice.parameter))
Some(value) => value
}
if model.value_index(parameter_index, choice.value) is None {
return Err(UnknownValue(choice.parameter, choice.value))
}
}
Ok(())
}
///|
pub fn CasePattern::matches(
self : CasePattern,
model : Model,
test_case : TestCase,
) -> Result[Bool, CaseWeaveError] {
match self.validate(model) {
Err(error) => return Err(error)
Ok(_) => ()
}
match model.validate_case(test_case) {
Err(error) => return Err(error)
Ok(_) => ()
}
for choice in self.choices {
if !case_has_choice(model, test_case, choice) {
return Ok(false)
}
}
Ok(true)
}
///|
pub fn CasePattern::to_exclusion_constraint(
self : CasePattern,
label : String,
) -> Result[Constraint, CaseWeaveError] {
if self.choices.length() == 0 {
return Err(ParseError(0, "case pattern must not be empty"))
}
Ok(Constraint::new(label, Not(pattern_to_expression(self))))
}
///|
fn pattern_to_expression(pattern : CasePattern) -> ConstraintExpr {
let mut expression : ConstraintExpr = Equal(
pattern.choices[0].parameter,
pattern.choices[0].value,
)
for index = 1; index < pattern.choices.length(); index = index + 1 {
expression = And(
expression,
Equal(pattern.choices[index].parameter, pattern.choices[index].value),
)
}
expression
}
///|
pub fn CasePattern::to_case(
self : CasePattern,
model : Model,
line_no? : Int = 0,
) -> Result[TestCase, CaseWeaveError] {
match self.validate(model) {
Err(error) => return Err(error)
Ok(_) => ()
}
let values = Array::make(model.parameter_count(), "")
let seen = Array::make(model.parameter_count(), false)
for choice in self.choices {
let parameter_index = match model.parameter_index(choice.parameter) {
None => return Err(UnknownParameter(choice.parameter))
Some(value) => value
}
values[parameter_index] = choice.value
seen[parameter_index] = true
}
for index = 0; index < seen.length(); index = index + 1 {
if !seen[index] {
let parameter = match model.parameter(index) {
None => "unknown"
Some(value) => value.name
}
return Err(
ParseError(line_no, "included case is missing parameter '\{parameter}'"),
)
}
}
Ok(TestCase::new(values))
}
///|
pub fn CasePattern::to_text(self : CasePattern) -> String {
let builder = StringBuilder::new()
for index = 0; index < self.choices.length(); index = index + 1 {
if index > 0 {
builder.write_string(", ")
}
let choice = self.choices[index]
builder.write_string(choice.parameter)
builder.write_char('=')
builder.write_string(choice.value)
}
builder.to_string()
}
///|
pub fn TestCase::assignment_text(self : TestCase, model : Model) -> String {
let builder = StringBuilder::new()
for index = 0; index < model.parameter_count(); index = index + 1 {
if index > 0 {
builder.write_string(", ")
}
let parameter = match model.parameter(index) {
None => "unknown"
Some(value) => value.name
}
builder.write_string(parameter)
builder.write_char('=')
match self.get(index) {
None => builder.write_string("?")
Some(value) => builder.write_string(value)
}
}
builder.to_string()
}
///|
pub fn ScenarioSpec::summary(self : ScenarioSpec) -> String {
"scenario=\{self.name}, parameters=\{self.model.parameter_count()}, strength=\{self.options.strength}, constraints=\{self.constraints.length()}, includes=\{self.included_cases.length()}, excludes=\{self.excluded_patterns.length()}, risks=\{self.value_risks.length() + self.interaction_risks.length()}"
}
///|
pub fn ScenarioSpec::lint(self : ScenarioSpec) -> Array[SpecWarning] {
let warnings : Array[SpecWarning] = []
if self.constraints.length() == 0 {
warnings.push({
kind: NoConstraints,
message: "No constraints are declared; verify that impossible real-world combinations are not being generated.",
})
}
if self.value_risks.length() == 0 && self.interaction_risks.length() == 0 {
warnings.push({
kind: NoRiskHints,
message: "No risk hints are declared; ranked output will preserve generation order only.",
})
}
if self.included_cases.length() == 0 {
warnings.push({
kind: NoIncludedCases,
message: "No included anchor cases are declared; add must-run smoke paths when a scenario has business-critical examples.",
})
}
if self.options.strength == 1 {
warnings.push({
kind: StrengthOne,
message: "Strength 1 covers individual values only; pairwise strength is a better default for configuration interactions.",
})
}
if self.model.combination_count() > self.options.max_candidates.to_int64() {
warnings.push({
kind: CandidateLimitBelowCartesian(
self.model.combination_count(),
self.options.max_candidates,
),
message: "The candidate limit is below the Cartesian space; generation may stop before all feasible rows are examined.",
})
}
for parameter in self.model.parameters {
if parameter.values.length() == 1 {
warnings.push({
kind: SingleValueParameter(parameter.name),
message: "Parameter '\{parameter.name}' has one value and does not increase coverage strength.",
})
}
}
warnings
}
///|
pub fn ScenarioSpec::lint_text(self : ScenarioSpec) -> String {
let warnings = self.lint()
if warnings.length() == 0 {
return "no scenario warnings\n"
}
let builder = StringBuilder::new()
for warning in warnings {
builder.write_string("- ")
builder.write_string(warning.message)
builder.write_char('\n')
}
builder.to_string()
}
///|
pub fn ScenarioRun::summary(self : ScenarioRun) -> String {
let top_score = if self.ranked_cases.length() == 0 {
0
} else {
self.ranked_cases[0].score
}
"scenario=\{self.spec_name}, cases=\{self.cases.length()}, included=\{self.included_case_count}, coverage=\{self.coverage.coverage_percent}%, top-risk=\{top_score}"
}
///|
pub fn ScenarioRun::cases_markdown(self : ScenarioRun) -> String {
cases_to_markdown(self.model, self.cases)
}
///|
pub fn ScenarioRun::risk_markdown(
self : ScenarioRun,
top? : Int = 10,
) -> String {
let builder = StringBuilder::new()
builder.write_string("| rank | case | score | reasons |\n")
builder.write_string("| --- | --- | --- | --- |\n")
let limit = if top < 0 {
self.ranked_cases.length()
} else if top > self.ranked_cases.length() {
self.ranked_cases.length()
} else {
top
}
for index = 0; index < limit; index = index + 1 {
let item = self.ranked_cases[index]
builder.write_string("| ")
builder.write_string(item.rank.to_string())
builder.write_string(" | ")
builder.write_string(
markdown_inline(item.test_case.assignment_text(self.model)),
)
builder.write_string(" | ")
builder.write_string(item.score.to_string())
builder.write_string(" | ")
builder.write_string(markdown_inline(join_reasons(item.reasons)))
builder.write_string(" |\n")
}
builder.to_string()
}
///|
fn join_reasons(reasons : Array[String]) -> String {
if reasons.length() == 0 {
return "none"
}
let builder = StringBuilder::new()
for index = 0; index < reasons.length(); index = index + 1 {
if index > 0 {
builder.write_string("; ")
}
builder.write_string(reasons[index])
}
builder.to_string()
}
///|
fn markdown_inline(value : String) -> String {
let builder = StringBuilder::new()
for char in value.to_array() {
match char {
'|' => builder.write_string("\\|")
'\n' => builder.write_string("
")
'\r' => ()
_ => builder.write_char(char)
}
}
builder.to_string()
}
///|
fn parse_case_pattern(
source : String,
line_no : Int,
) -> Result[CasePattern, CaseWeaveError] {
let parts = match split_csv_values(source, line_no) {
Err(error) => return Err(error)
Ok(value) => value
}
let choices : Array[NamedChoice] = []
for part in parts {
let assignment = match split_once_char(part, '=') {
None => return Err(ParseError(line_no, "expected parameter=value"))
Some(value) => value
}
let parameter = trim_text(assignment.left)
let value = trim_text(assignment.right)
if parameter.length() == 0 {
return Err(ParseError(line_no, "assignment parameter is empty"))
}
if value.length() == 0 {
return Err(ParseError(line_no, "assignment value is empty"))
}
for choice in choices {
if choice.parameter == parameter {
return Err(DuplicateParameter(parameter))
}
}
choices.push({ parameter, value })
}
if choices.length() == 0 {
return Err(ParseError(line_no, "case pattern must not be empty"))
}
Ok({ choices, })
}
///|
fn split_csv_values(
source : String,
line_no : Int,
) -> Result[Array[String], CaseWeaveError] {
let values : Array[String] = []
let chars = source.to_array()
let builder = StringBuilder::new()
let mut quoted = false
let mut index = 0
while index < chars.length() {
let char = chars[index]
if quoted {
if char == '"' {
quoted = false
index = index + 1
continue
}
if char == '\\' {
if index + 1 >= chars.length() {
return Err(ParseError(line_no, "unterminated escape sequence"))
}
let escaped = chars[index + 1]
match escaped {
'"' => builder.write_char('"')
'\\' => builder.write_char('\\')
'n' => builder.write_char('\n')
'r' => builder.write_char('\r')
't' => builder.write_char('\t')
',' => builder.write_char(',')
_ => return Err(ParseError(line_no, "unsupported escape sequence"))
}
index = index + 2
continue
}
builder.write_char(char)
index = index + 1
continue
}
if char == '"' {
quoted = true
index = index + 1
continue
}
if char == ',' {
let value = trim_text(builder.to_string())
if value.length() == 0 {
return Err(ParseError(line_no, "empty comma-separated value"))
}
values.push(value)
builder.reset()
index = index + 1
continue
}
builder.write_char(char)
index = index + 1
}
if quoted {
return Err(ParseError(line_no, "unterminated quoted value"))
}
let final_value = trim_text(builder.to_string())
if final_value.length() == 0 {
return Err(ParseError(line_no, "empty comma-separated value"))
}
values.push(final_value)
Ok(values)
}
///|
fn parse_positive_int(
text : String,
line_no : Int,
) -> Result[Int, CaseWeaveError] {
let value = trim_text(text)
if value.length() == 0 {
return Err(ParseError(line_no, "expected positive integer"))
}
let chars = value.to_array()
let mut result = 0
for char in chars {
if char < '0' || char > '9' {
return Err(ParseError(line_no, "expected positive integer"))
}
result = result * 10 + char_to_digit(char)
}
if result <= 0 {
return Err(ParseError(line_no, "expected positive integer"))
}
Ok(result)
}
///|
fn char_to_digit(char : Char) -> Int {
char.to_int() - '0'.to_int()
}
///|
fn parse_bool(text : String, line_no : Int) -> Result[Bool, CaseWeaveError] {
let value = trim_text(text).to_lower()
match value {
"true" | "yes" | "on" => Ok(true)
"false" | "no" | "off" => Ok(false)
_ => Err(ParseError(line_no, "expected boolean"))
}
}
///|
fn split_lines(source : String) -> Array[String] {
let lines : Array[String] = []
let chars = source.to_array()
let builder = StringBuilder::new()
for char in chars {
if char == '\n' {
lines.push(builder.to_string())
builder.reset()
} else {
builder.write_char(char)
}
}
lines.push(builder.to_string())
lines
}
///|
fn split_once_char(source : String, separator : Char) -> TextParts? {
let chars = source.to_array()
for index = 0; index < chars.length(); index = index + 1 {
if chars[index] == separator {
return Some({
left: slice_chars(chars, 0, index),
right: slice_chars(chars, index + 1, chars.length()),
})
}
}
None
}
///|
fn slice_chars(chars : Array[Char], start : Int, end : Int) -> String {
let builder = StringBuilder::new()
for index = start; index < end; index = index + 1 {
builder.write_char(chars[index])
}
builder.to_string()
}
///|
fn trim_text(source : String) -> String {
let chars = source.to_array()
let mut start = 0
let mut end = chars.length()
while start < end && is_spec_space(chars[start]) {
start = start + 1
}
while end > start && is_spec_space(chars[end - 1]) {
end = end - 1
}
slice_chars(chars, start, end)
}
///|
fn is_spec_space(char : Char) -> Bool {
char == ' ' || char == '\t' || char == '\r' || char == '\n'
}
///|
fn directive_is(head : String, expected : String) -> Bool {
trim_text(head).to_lower() == expected
}
///|
fn strip_directive(head : String, expected : String) -> String? {
let normalized = trim_text(head)
let lower = normalized.to_lower()
if lower == expected {
return Some("")
}
let prefix = expected + " "
if starts_with_text(lower, prefix) {
Some(
trim_text(
slice_chars(
normalized.to_array(),
prefix.length(),
normalized.to_array().length(),
),
),
)
} else {
None
}
}
///|
fn starts_with_text(source : String, prefix : String) -> Bool {
let source_chars = source.to_array()
let prefix_chars = prefix.to_array()
if prefix_chars.length() > source_chars.length() {
return false
}
for index = 0; index < prefix_chars.length(); index = index + 1 {
if source_chars[index] != prefix_chars[index] {
return false
}
}
true
}