Website rebuilt from scratch based on analysis of 11 beloved language websites (Elm, Zig, Gleam, Swift, Kotlin, Haskell, OCaml, Crystal, Roc, Rust, Go). New website structure: - Homepage with hero, playground, three pillars, install guide - Language Tour with interactive lessons (hello world, types, effects) - Examples cookbook with categorized sidebar - API documentation index - Installation guide (Nix and source) - Sleek/noble design (black/gold, serif typography) Also includes: - New stdlib/json.lux module for JSON serialization - Enhanced stdlib/http.lux with middleware and routing - New string functions (charAt, indexOf, lastIndexOf, repeat) - LSP improvements (rename, signature help, formatting) - Package manager transitive dependency resolution - Updated documentation for effects and stdlib - New showcase example (task_manager.lux) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
474 lines
16 KiB
Plaintext
474 lines
16 KiB
Plaintext
// JSON Serialization and Deserialization for Lux
|
|
//
|
|
// Provides type-safe JSON encoding and decoding with support
|
|
// for schema versioning and custom codecs.
|
|
//
|
|
// Usage:
|
|
// let json = Json.encode(user) // Serialize to JSON string
|
|
// let user = Json.decode(json) // Deserialize from JSON string
|
|
|
|
// ============================================================
|
|
// JSON Value Type
|
|
// ============================================================
|
|
|
|
// Represents any JSON value
|
|
type JsonValue =
|
|
| JsonNull
|
|
| JsonBool(Bool)
|
|
| JsonInt(Int)
|
|
| JsonFloat(Float)
|
|
| JsonString(String)
|
|
| JsonArray(List<JsonValue>)
|
|
| JsonObject(List<(String, JsonValue)>)
|
|
|
|
// ============================================================
|
|
// Encoding Primitives
|
|
// ============================================================
|
|
|
|
// Escape a string for JSON
|
|
pub fn escapeString(s: String): String = {
|
|
let escaped = String.replace(s, "\\", "\\\\")
|
|
let escaped = String.replace(escaped, "\"", "\\\"")
|
|
let escaped = String.replace(escaped, "\n", "\\n")
|
|
let escaped = String.replace(escaped, "\r", "\\r")
|
|
let escaped = String.replace(escaped, "\t", "\\t")
|
|
escaped
|
|
}
|
|
|
|
// Encode a JsonValue to a JSON string
|
|
pub fn encode(value: JsonValue): String = {
|
|
match value {
|
|
JsonNull => "null",
|
|
JsonBool(b) => if b then "true" else "false",
|
|
JsonInt(n) => toString(n),
|
|
JsonFloat(f) => toString(f),
|
|
JsonString(s) => "\"" + escapeString(s) + "\"",
|
|
JsonArray(items) => {
|
|
let encodedItems = List.map(items, encode)
|
|
"[" + String.join(encodedItems, ",") + "]"
|
|
},
|
|
JsonObject(fields) => {
|
|
let encodedFields = List.map(fields, fn(field: (String, JsonValue)): String => {
|
|
match field {
|
|
(key, val) => "\"" + escapeString(key) + "\":" + encode(val)
|
|
}
|
|
})
|
|
"{" + String.join(encodedFields, ",") + "}"
|
|
}
|
|
}
|
|
}
|
|
|
|
// Pretty-print a JsonValue with indentation
|
|
pub fn encodePretty(value: JsonValue): String =
|
|
encodePrettyIndent(value, 0)
|
|
|
|
fn encodePrettyIndent(value: JsonValue, indent: Int): String = {
|
|
let spaces = String.repeat(" ", indent)
|
|
let nextSpaces = String.repeat(" ", indent + 1)
|
|
|
|
match value {
|
|
JsonNull => "null",
|
|
JsonBool(b) => if b then "true" else "false",
|
|
JsonInt(n) => toString(n),
|
|
JsonFloat(f) => toString(f),
|
|
JsonString(s) => "\"" + escapeString(s) + "\"",
|
|
JsonArray(items) => {
|
|
if List.length(items) == 0 then "[]"
|
|
else {
|
|
let encodedItems = List.map(items, fn(item: JsonValue): String =>
|
|
nextSpaces + encodePrettyIndent(item, indent + 1)
|
|
)
|
|
"[\n" + String.join(encodedItems, ",\n") + "\n" + spaces + "]"
|
|
}
|
|
},
|
|
JsonObject(fields) => {
|
|
if List.length(fields) == 0 then "{}"
|
|
else {
|
|
let encodedFields = List.map(fields, fn(field: (String, JsonValue)): String => {
|
|
match field {
|
|
(key, val) => nextSpaces + "\"" + escapeString(key) + "\": " + encodePrettyIndent(val, indent + 1)
|
|
}
|
|
})
|
|
"{\n" + String.join(encodedFields, ",\n") + "\n" + spaces + "}"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// ============================================================
|
|
// Type-specific Encoders
|
|
// ============================================================
|
|
|
|
// Encode primitives
|
|
pub fn encodeNull(): JsonValue = JsonNull
|
|
pub fn encodeBool(b: Bool): JsonValue = JsonBool(b)
|
|
pub fn encodeInt(n: Int): JsonValue = JsonInt(n)
|
|
pub fn encodeFloat(f: Float): JsonValue = JsonFloat(f)
|
|
pub fn encodeString(s: String): JsonValue = JsonString(s)
|
|
|
|
// Encode a list
|
|
pub fn encodeList<A>(items: List<A>, encodeItem: fn(A): JsonValue): JsonValue =
|
|
JsonArray(List.map(items, encodeItem))
|
|
|
|
// Encode an optional value
|
|
pub fn encodeOption<A>(opt: Option<A>, encodeItem: fn(A): JsonValue): JsonValue =
|
|
match opt {
|
|
None => JsonNull,
|
|
Some(value) => encodeItem(value)
|
|
}
|
|
|
|
// Encode a Result
|
|
pub fn encodeResult<T, E>(
|
|
result: Result<T, E>,
|
|
encodeOk: fn(T): JsonValue,
|
|
encodeErr: fn(E): JsonValue
|
|
): JsonValue =
|
|
match result {
|
|
Ok(value) => JsonObject([
|
|
("ok", encodeOk(value))
|
|
]),
|
|
Err(error) => JsonObject([
|
|
("error", encodeErr(error))
|
|
])
|
|
}
|
|
|
|
// ============================================================
|
|
// Object Building Helpers
|
|
// ============================================================
|
|
|
|
// Create an empty JSON object
|
|
pub fn object(): JsonValue = JsonObject([])
|
|
|
|
// Add a field to a JSON object
|
|
pub fn withField(obj: JsonValue, key: String, value: JsonValue): JsonValue =
|
|
match obj {
|
|
JsonObject(fields) => JsonObject(List.concat(fields, [(key, value)])),
|
|
_ => obj // Not an object, return unchanged
|
|
}
|
|
|
|
// Add a string field
|
|
pub fn withString(obj: JsonValue, key: String, value: String): JsonValue =
|
|
withField(obj, key, JsonString(value))
|
|
|
|
// Add an int field
|
|
pub fn withInt(obj: JsonValue, key: String, value: Int): JsonValue =
|
|
withField(obj, key, JsonInt(value))
|
|
|
|
// Add a bool field
|
|
pub fn withBool(obj: JsonValue, key: String, value: Bool): JsonValue =
|
|
withField(obj, key, JsonBool(value))
|
|
|
|
// Add an optional field (only adds if Some)
|
|
pub fn withOptional<A>(obj: JsonValue, key: String, opt: Option<A>, encodeItem: fn(A): JsonValue): JsonValue =
|
|
match opt {
|
|
None => obj,
|
|
Some(value) => withField(obj, key, encodeItem(value))
|
|
}
|
|
|
|
// ============================================================
|
|
// Decoding
|
|
// ============================================================
|
|
|
|
// Result type for parsing
|
|
type ParseResult<A> = Result<A, String>
|
|
|
|
// Get a field from a JSON object
|
|
pub fn getField(obj: JsonValue, key: String): Option<JsonValue> =
|
|
match obj {
|
|
JsonObject(fields) => findField(fields, key),
|
|
_ => None
|
|
}
|
|
|
|
fn findField(fields: List<(String, JsonValue)>, key: String): Option<JsonValue> =
|
|
match List.head(fields) {
|
|
None => None,
|
|
Some(field) => match field {
|
|
(k, v) => if k == key then Some(v)
|
|
else findField(Option.getOrElse(List.tail(fields), []), key)
|
|
}
|
|
}
|
|
|
|
// Get a string field
|
|
pub fn getString(obj: JsonValue, key: String): Option<String> =
|
|
match getField(obj, key) {
|
|
Some(JsonString(s)) => Some(s),
|
|
_ => None
|
|
}
|
|
|
|
// Get an int field
|
|
pub fn getInt(obj: JsonValue, key: String): Option<Int> =
|
|
match getField(obj, key) {
|
|
Some(JsonInt(n)) => Some(n),
|
|
_ => None
|
|
}
|
|
|
|
// Get a bool field
|
|
pub fn getBool(obj: JsonValue, key: String): Option<Bool> =
|
|
match getField(obj, key) {
|
|
Some(JsonBool(b)) => Some(b),
|
|
_ => None
|
|
}
|
|
|
|
// Get an array field
|
|
pub fn getArray(obj: JsonValue, key: String): Option<List<JsonValue>> =
|
|
match getField(obj, key) {
|
|
Some(JsonArray(items)) => Some(items),
|
|
_ => None
|
|
}
|
|
|
|
// Get an object field
|
|
pub fn getObject(obj: JsonValue, key: String): Option<JsonValue> =
|
|
match getField(obj, key) {
|
|
Some(JsonObject(_) as obj) => Some(obj),
|
|
_ => None
|
|
}
|
|
|
|
// ============================================================
|
|
// Simple JSON Parser
|
|
// ============================================================
|
|
|
|
// Parse a JSON string into a JsonValue
|
|
// Note: This is a simplified parser for common cases
|
|
pub fn parse(json: String): Result<JsonValue, String> =
|
|
parseValue(String.trim(json), 0).mapResult(fn(r: (JsonValue, Int)): JsonValue => {
|
|
match r { (value, _) => value }
|
|
})
|
|
|
|
fn parseValue(json: String, pos: Int): Result<(JsonValue, Int), String> = {
|
|
let c = String.charAt(json, pos)
|
|
match c {
|
|
"n" => parseNull(json, pos),
|
|
"t" => parseTrue(json, pos),
|
|
"f" => parseFalse(json, pos),
|
|
"\"" => parseString(json, pos),
|
|
"[" => parseArray(json, pos),
|
|
"{" => parseObject(json, pos),
|
|
"-" => parseNumber(json, pos),
|
|
_ => if isDigit(c) then parseNumber(json, pos)
|
|
else if c == " " || c == "\n" || c == "\r" || c == "\t" then
|
|
parseValue(json, pos + 1)
|
|
else Err("Unexpected character at position " + toString(pos))
|
|
}
|
|
}
|
|
|
|
fn parseNull(json: String, pos: Int): Result<(JsonValue, Int), String> =
|
|
if String.substring(json, pos, pos + 4) == "null" then Ok((JsonNull, pos + 4))
|
|
else Err("Expected 'null' at position " + toString(pos))
|
|
|
|
fn parseTrue(json: String, pos: Int): Result<(JsonValue, Int), String> =
|
|
if String.substring(json, pos, pos + 4) == "true" then Ok((JsonBool(true), pos + 4))
|
|
else Err("Expected 'true' at position " + toString(pos))
|
|
|
|
fn parseFalse(json: String, pos: Int): Result<(JsonValue, Int), String> =
|
|
if String.substring(json, pos, pos + 5) == "false" then Ok((JsonBool(false), pos + 5))
|
|
else Err("Expected 'false' at position " + toString(pos))
|
|
|
|
fn parseString(json: String, pos: Int): Result<(JsonValue, Int), String> = {
|
|
// Skip opening quote
|
|
let start = pos + 1
|
|
let result = parseStringContent(json, start, "")
|
|
result.mapResult(fn(r: (String, Int)): (JsonValue, Int) => {
|
|
match r { (s, endPos) => (JsonString(s), endPos) }
|
|
})
|
|
}
|
|
|
|
fn parseStringContent(json: String, pos: Int, acc: String): Result<(String, Int), String> = {
|
|
let c = String.charAt(json, pos)
|
|
if c == "\"" then Ok((acc, pos + 1))
|
|
else if c == "\\" then {
|
|
let nextC = String.charAt(json, pos + 1)
|
|
let escaped = match nextC {
|
|
"n" => "\n",
|
|
"r" => "\r",
|
|
"t" => "\t",
|
|
"\"" => "\"",
|
|
"\\" => "\\",
|
|
_ => nextC
|
|
}
|
|
parseStringContent(json, pos + 2, acc + escaped)
|
|
}
|
|
else if c == "" then Err("Unterminated string")
|
|
else parseStringContent(json, pos + 1, acc + c)
|
|
}
|
|
|
|
fn parseNumber(json: String, pos: Int): Result<(JsonValue, Int), String> = {
|
|
let result = parseNumberDigits(json, pos, "")
|
|
result.mapResult(fn(r: (String, Int)): (JsonValue, Int) => {
|
|
match r {
|
|
(numStr, endPos) => {
|
|
// Check if it's a float
|
|
if String.contains(numStr, ".") then
|
|
(JsonFloat(parseFloat(numStr)), endPos)
|
|
else
|
|
(JsonInt(parseInt(numStr)), endPos)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
fn parseNumberDigits(json: String, pos: Int, acc: String): Result<(String, Int), String> = {
|
|
let c = String.charAt(json, pos)
|
|
if isDigit(c) || c == "." || c == "-" || c == "e" || c == "E" || c == "+" then
|
|
parseNumberDigits(json, pos + 1, acc + c)
|
|
else if acc == "" then Err("Expected number at position " + toString(pos))
|
|
else Ok((acc, pos))
|
|
}
|
|
|
|
fn parseArray(json: String, pos: Int): Result<(JsonValue, Int), String> = {
|
|
// Skip opening bracket and whitespace
|
|
let startPos = skipWhitespace(json, pos + 1)
|
|
|
|
if String.charAt(json, startPos) == "]" then Ok((JsonArray([]), startPos + 1))
|
|
else parseArrayItems(json, startPos, [])
|
|
}
|
|
|
|
fn parseArrayItems(json: String, pos: Int, acc: List<JsonValue>): Result<(JsonValue, Int), String> = {
|
|
match parseValue(json, pos) {
|
|
Err(e) => Err(e),
|
|
Ok((value, nextPos)) => {
|
|
let newAcc = List.concat(acc, [value])
|
|
let afterWhitespace = skipWhitespace(json, nextPos)
|
|
let c = String.charAt(json, afterWhitespace)
|
|
if c == "]" then Ok((JsonArray(newAcc), afterWhitespace + 1))
|
|
else if c == "," then parseArrayItems(json, skipWhitespace(json, afterWhitespace + 1), newAcc)
|
|
else Err("Expected ',' or ']' at position " + toString(afterWhitespace))
|
|
}
|
|
}
|
|
}
|
|
|
|
fn parseObject(json: String, pos: Int): Result<(JsonValue, Int), String> = {
|
|
// Skip opening brace and whitespace
|
|
let startPos = skipWhitespace(json, pos + 1)
|
|
|
|
if String.charAt(json, startPos) == "}" then Ok((JsonObject([]), startPos + 1))
|
|
else parseObjectFields(json, startPos, [])
|
|
}
|
|
|
|
fn parseObjectFields(json: String, pos: Int, acc: List<(String, JsonValue)>): Result<(JsonValue, Int), String> = {
|
|
// Parse key
|
|
match parseString(json, pos) {
|
|
Err(e) => Err(e),
|
|
Ok((keyValue, afterKey)) => {
|
|
match keyValue {
|
|
JsonString(key) => {
|
|
let colonPos = skipWhitespace(json, afterKey)
|
|
if String.charAt(json, colonPos) != ":" then
|
|
Err("Expected ':' at position " + toString(colonPos))
|
|
else {
|
|
let valuePos = skipWhitespace(json, colonPos + 1)
|
|
match parseValue(json, valuePos) {
|
|
Err(e) => Err(e),
|
|
Ok((value, afterValue)) => {
|
|
let newAcc = List.concat(acc, [(key, value)])
|
|
let afterWhitespace = skipWhitespace(json, afterValue)
|
|
let c = String.charAt(json, afterWhitespace)
|
|
if c == "}" then Ok((JsonObject(newAcc), afterWhitespace + 1))
|
|
else if c == "," then parseObjectFields(json, skipWhitespace(json, afterWhitespace + 1), newAcc)
|
|
else Err("Expected ',' or '}' at position " + toString(afterWhitespace))
|
|
}
|
|
}
|
|
}
|
|
},
|
|
_ => Err("Expected string key at position " + toString(pos))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn skipWhitespace(json: String, pos: Int): Int = {
|
|
let c = String.charAt(json, pos)
|
|
if c == " " || c == "\n" || c == "\r" || c == "\t" then
|
|
skipWhitespace(json, pos + 1)
|
|
else pos
|
|
}
|
|
|
|
fn isDigit(c: String): Bool =
|
|
c == "0" || c == "1" || c == "2" || c == "3" || c == "4" ||
|
|
c == "5" || c == "6" || c == "7" || c == "8" || c == "9"
|
|
|
|
// ============================================================
|
|
// Codec Type (for automatic serialization)
|
|
// ============================================================
|
|
|
|
// A codec can both encode and decode a type
|
|
type Codec<A> = {
|
|
encode: fn(A): JsonValue,
|
|
decode: fn(JsonValue): Result<A, String>
|
|
}
|
|
|
|
// Create a codec from encode/decode functions
|
|
pub fn codec<A>(
|
|
enc: fn(A): JsonValue,
|
|
dec: fn(JsonValue): Result<A, String>
|
|
): Codec<A> =
|
|
{ encode: enc, decode: dec }
|
|
|
|
// Built-in codecs
|
|
pub fn stringCodec(): Codec<String> =
|
|
codec(
|
|
encodeString,
|
|
fn(json: JsonValue): Result<String, String> => match json {
|
|
JsonString(s) => Ok(s),
|
|
_ => Err("Expected string")
|
|
}
|
|
)
|
|
|
|
pub fn intCodec(): Codec<Int> =
|
|
codec(
|
|
encodeInt,
|
|
fn(json: JsonValue): Result<Int, String> => match json {
|
|
JsonInt(n) => Ok(n),
|
|
_ => Err("Expected int")
|
|
}
|
|
)
|
|
|
|
pub fn boolCodec(): Codec<Bool> =
|
|
codec(
|
|
encodeBool,
|
|
fn(json: JsonValue): Result<Bool, String> => match json {
|
|
JsonBool(b) => Ok(b),
|
|
_ => Err("Expected bool")
|
|
}
|
|
)
|
|
|
|
pub fn listCodec<A>(itemCodec: Codec<A>): Codec<List<A>> =
|
|
codec(
|
|
fn(items: List<A>): JsonValue => encodeList(items, itemCodec.encode),
|
|
fn(json: JsonValue): Result<List<A>, String> => match json {
|
|
JsonArray(items) => decodeAll(items, itemCodec.decode),
|
|
_ => Err("Expected array")
|
|
}
|
|
)
|
|
|
|
fn decodeAll<A>(items: List<JsonValue>, decode: fn(JsonValue): Result<A, String>): Result<List<A>, String> = {
|
|
match List.head(items) {
|
|
None => Ok([]),
|
|
Some(item) => match decode(item) {
|
|
Err(e) => Err(e),
|
|
Ok(decoded) => match decodeAll(Option.getOrElse(List.tail(items), []), decode) {
|
|
Err(e) => Err(e),
|
|
Ok(rest) => Ok(List.concat([decoded], rest))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn optionCodec<A>(itemCodec: Codec<A>): Codec<Option<A>> =
|
|
codec(
|
|
fn(opt: Option<A>): JsonValue => encodeOption(opt, itemCodec.encode),
|
|
fn(json: JsonValue): Result<Option<A>, String> => match json {
|
|
JsonNull => Ok(None),
|
|
_ => itemCodec.decode(json).mapResult(fn(a: A): Option<A> => Some(a))
|
|
}
|
|
)
|
|
|
|
// ============================================================
|
|
// Helper for Result.mapResult
|
|
// ============================================================
|
|
|
|
fn mapResult<A, B>(result: Result<A, String>, f: fn(A): B): Result<B, String> =
|
|
match result {
|
|
Ok(a) => Ok(f(a)),
|
|
Err(e) => Err(e)
|
|
}
|