Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
175 changes: 172 additions & 3 deletions bindings/rescript/Bunsenite.affine
Original file line number Diff line number Diff line change
@@ -1,7 +1,176 @@
// SPDX-License-Identifier: MPL-2.0
// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell
// Ported via Harvard Engine bulk-processor
// Ported via Harvard Engine (Semantic pass)

module Bunsenite;

// TODO: Complete semantic implementation
// Bunsenite Rescript Bindings
// Type-safe Rescript bindings for Bunsenite via C FFI
//
// Usage:
// open Bunsenite
// fn config = parseNickel("{foo = 42}", "config.ncl")
// Js.log(config)

// External C FFI declarations
// These bind to the C ABI provided by the Zig layer

@module("./bunsenite_ffi")
external parseNickelRaw: (string, string) => Js.Nullable.t<string> = "parse_nickel"

@module("./bunsenite_ffi")
external validateNickelRaw: (string, string) => int = "validate_nickel"

@module("./bunsenite_ffi")
external versionRaw: unit => string = "version"

@module("./bunsenite_ffi")
external rsrTierRaw: unit => string = "rsr_tier"

@module("./bunsenite_ffi")
external tpcfPerimeterRaw: unit => int = "tpcf_perimeter"

// Result struct for error handling
struct result<'a, 'e> = Ok('a) | Error('e)

// Error struct
struct error {
| ParseError(string)
| ValidationError(string)
| InvalidInput(string)

// Parse and evaluate a Nickel configuration string
//
// Example:
// fn config = parseNickel("{name = \"example\", port = 8080}", "config.ncl")
// switch config {
// | Ok(json) => Js.log(json)
// | Error(err) => Js.log2("Error:", err)
// }
fn parseNickel = (source: string, name: string): result<Js.Json.t, error> => {
fn result = parseNickelRaw(source, name)

switch Js.Nullable.toOption(result) {
| Some(jsonString) =>
try {
fn parsed = Js.Json.parseExn(jsonString)
Ok(parsed)
} catch {
| _ => Error(ParseError("Failed to parse JSON result"))
}
| None => Error(ParseError("Failed to parse Nickel configuration: " ++ name))
}
}

// Validate a Nickel configuration without evaluating it
//
// Example:
// fn result = validateNickel("{foo = 42}", "config.ncl")
// switch result {
// | Ok() => Js.log("Valid!")
// | Error(err) => Js.log2("Invalid:", err)
// }
fn validateNickel = (source: string, name: string): result<unit, error> => {
fn resultCode = validateNickelRaw(source, name)

if resultCode == 0 {
Ok()
} else {
Error(ValidationError("Validation failed for: " ++ name))
}
}

// Get library version
//
// Example:
// fn ver = getVersion()
// Js.log2("Version:", ver)
fn getVersion = (): string => {
versionRaw()
}

// Get RSR compliance tier
//
// Example:
// fn tier = getRSRTier()
// Js.log2("RSR Tier:", tier)
fn getRSRTier = (): string => {
rsrTierRaw()
}

// Get TPCF perimeter number
//
// Example:
// fn perimeter = getTPCFPerimeter()
// Js.log2("TPCF Perimeter:", perimeter)
fn getTPCFPerimeter = (): int => {
tpcfPerimeterRaw()
}

// Helper: Parse Nickel file from filesystem
// Requires Node.js fs module
//
// Example:
// fn config = parseFile("./config.ncl")
// switch config {
// | Ok(json) => Js.log(json)
// | Error(err) => Js.log2("Error:", err)
// }
@module("fs")
external readFileSync: (string, string) => string = "readFileSync"

fn parseFile = (path: string): result<Js.Json.t, error> => {
try {
fn source = readFileSync(path, "utf8")
parseNickel(source, path)
} catch {
| _ => Error(InvalidInput("Failed to read file: " ++ path))
}
}

// Helper: Validate Nickel file from filesystem
//
// Example:
// fn result = validateFile("./config.ncl")
// switch result {
// | Ok() => Js.log("Valid!")
// | Error(err) => Js.log2("Invalid:", err)
// }
fn validateFile = (path: string): result<unit, error> => {
try {
fn source = readFileSync(path, "utf8")
validateNickel(source, path)
} catch {
| _ => Error(InvalidInput("Failed to read file: " ++ path))
}
}

// Helper: Get config value by key path
// Example: getConfigValue(config, ["server", "port"])
fn rec getConfigValue = (json: Js.Json.t, path: list<string>): option<Js.Json.t> => {
switch path {
| list{} => Some(json)
| list{key, ...rest} =>
switch Js.Json.decodeObject(json) {
| Some(obj) =>
switch Js.Dict.get(obj, key) {
| Some(value) => getConfigValue(value, rest)
| None => None
}
| None => None
}
}
}

// Helper: Convert error to string for display
fn errorToString = (err: error): string => {
switch err {
| ParseError(msg) => "Parse Error: " ++ msg
| ValidationError(msg) => "Validation Error: " ++ msg
| InvalidInput(msg) => "Invalid Input: " ++ msg
}
}

// Re-export result struct for convenience
struct parseResult { result<Js.Json.t, error>
struct validateResult { result<unit, error>

183 changes: 180 additions & 3 deletions bindings/rescript/Bunsenite_test.affine
Original file line number Diff line number Diff line change
@@ -1,7 +1,184 @@
// SPDX-License-Identifier: MPL-2.0
// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell
// Ported via Harvard Engine bulk-processor
// Ported via Harvard Engine (Semantic pass)

module Bunsenite_test;

// TODO: Complete semantic implementation
// SPDX-License-Identifier: MPL-2.0
// Bunsenite ReScript Bindings Test Suite

open Bunsenite

// Test helpers
fn assertEqual = (actual, expected, testName) => {
if actual == expected {
Console.log(`✓ ${testName}`)
} else {
Console.error(`✗ ${testName}`)
Console.error(` Expected: ${expected->Js.Json.stringify}`)
Console.error(` Actual: ${actual->Js.Json.stringify}`)
}
}

fn assertOk = (result, testName) => {
switch result {
| Ok(_) => Console.log(`✓ ${testName}`)
| Error(err) => {
Console.error(`✗ ${testName}`)
Console.error(` Error: ${errorToString(err)}`)
}
}
}

fn assertError = (result, testName) => {
switch result {
| Error(_) => Console.log(`✓ ${testName}`)
| Ok(_) => Console.error(`✗ ${testName}: Expected error but got Ok`)
}
}

// Test suite
fn runTests = () => {
Console.log("\n🧪 Bunsenite ReScript Bindings Test Suite\n")

// Test 1: Parse simple Nickel configuration
Console.log("Parse Tests:")
fn simpleConfig = parseNickel("{foo = 42}", "test.ncl")
assertOk(simpleConfig, "Parse simple number configuration")

// Test 2: Parse object configuration
fn objectConfig = parseNickel("{name = \"test\", value = 100}", "object.ncl")
assertOk(objectConfig, "Parse object configuration")

// Test 3: Parse nested configuration
fn nestedConfig = parseNickel("{server = {port = 8080, host = \"localhost\"}}", "nested.ncl")
assertOk(nestedConfig, "Parse nested configuration")

// Test 4: Parse array configuration
fn arrayConfig = parseNickel("{items = [1, 2, 3, 4, 5]}", "array.ncl")
assertOk(arrayConfig, "Parse array configuration")

// Test 5: Parse invalid syntax (should error)
fn invalidConfig = parseNickel("{foo = }", "invalid.ncl")
assertError(invalidConfig, "Parse invalid syntax returns error")

// Test 6: Parse empty configuration
fn emptyConfig = parseNickel("{}", "empty.ncl")
assertOk(emptyConfig, "Parse empty configuration")

// Validation Tests
Console.log("\nValidation Tests:")

fn validConfig = validateNickel("{foo = 42}", "valid.ncl")
assertOk(validConfig, "Validate correct configuration")

fn invalidValidation = validateNickel("{foo = }", "invalid-validate.ncl")
assertError(invalidValidation, "Validate incorrect configuration returns error")

// Test 7: Validate complex configuration
fn complexValid = validateNickel(
"{
app = {
name = \"example\",
version = \"1.0.0\",
config = {
debug = true,
port = 3000
}
}
}",
"complex.ncl",
)
assertOk(complexValid, "Validate complex nested configuration")

// Library Info Tests
Console.log("\nLibrary Info Tests:")

fn version = getVersion()
Console.log(`✓ Got version: ${version}`)

fn tier = getRSRTier()
Console.log(`✓ Got RSR tier: ${tier}`)

fn perimeter = getTPCFPerimeter()
Console.log(`✓ Got TPCF perimeter: ${perimeter->Int.toString}`)

// Config Value Tests
Console.log("\nConfig Value Extraction Tests:")

switch objectConfig {
| Ok(json) => {
// Test extracting top-level value
fn nameValue = getConfigValue(json, list{"name"})
switch nameValue {
| Some(_) => Console.log("✓ Extract top-level value")
| None => Console.error("✗ Failed to extract top-level value")
}

// Test extracting non-existent value
fn missingValue = getConfigValue(json, list{"missing"})
switch missingValue {
| None => Console.log("✓ Non-existent value returns None")
| Some(_) => Console.error("✗ Non-existent value should return None")
}
}
| Error(_) => Console.error("✗ Could not test config value extraction")
}

switch nestedConfig {
| Ok(json) => {
// Test extracting nested value
fn portValue = getConfigValue(json, list{"server", "port"})
switch portValue {
| Some(_) => Console.log("✓ Extract nested value")
| None => Console.error("✗ Failed to extract nested value")
}

// Test extracting with invalid path
fn invalidPath = getConfigValue(json, list{"server", "nonexistent", "deep"})
switch invalidPath {
| None => Console.log("✓ Invalid nested path returns None")
| Some(_) => Console.error("✗ Invalid path should return None")
}
}
| Error(_) => Console.error("✗ Could not test nested value extraction")
}

// Error handling tests
Console.log("\nError Handling Tests:")

fn parseErr = parseNickel("{invalid syntax here}", "error-test.ncl")
switch parseErr {
| Error(err) => {
fn errStr = errorToString(err)
Console.log(`✓ Error converted to string: ${errStr}`)
}
| Ok(_) => Console.error("✗ Expected parse error")
}

// Result struct tests
Console.log("\nResult Type Tests:")

fn successResult: parseResult = Ok(Js.Json.null)
switch successResult {
| Ok(_) => Console.log("✓ parseResult Ok variant works")
| Error(_) => Console.error("✗ parseResult Ok variant failed")
}

fn errorResult: parseResult = Error(ParseError("test"))
switch errorResult {
| Error(_) => Console.log("✓ parseResult Error variant works")
| Ok(_) => Console.error("✗ parseResult Error variant failed")
}

fn validateSuccess: validateResult = Ok()
switch validateSuccess {
| Ok() => Console.log("✓ validateResult Ok variant works")
| Error(_) => Console.error("✗ validateResult Ok variant failed")
}

Console.log("\n✅ Test suite complete\n")
}

// Run tests
runTests()

Loading