feat: rebuild website with full learning funnel
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>
This commit is contained in:
473
stdlib/json.lux
Normal file
473
stdlib/json.lux
Normal file
@@ -0,0 +1,473 @@
|
||||
// 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)
|
||||
}
|
||||
Reference in New Issue
Block a user