Oak

json.oak

← Standard library See on GitHub ↗

// libjson implements a JSON parser and serializer for Oak values

{
	default: default
	slice: slice
	map: map
} := import('std')
{
	space?: space?
	join: join
} := import('str')

// string escape '"'
fn esc(c) if c {
	'\t' -> '\\t'
	'\n' -> '\\n'
	'\r' -> '\\r'
	'\f' -> '\\f'
	'"' -> '\\"'
	'\\' -> '\\\\'
	_ -> c
}

// escapes whole string
fn escape(s) {
	max := len(s)
	fn sub(i, acc) if i {
		max -> acc
		_ -> sub(i + 1, acc << esc(s.(i)))
	}
	sub(0, '')
}

// serialize takes an Oak value and returns its JSON representation
fn serialize(c) if type(c) {
	// do not serialize functions
	:null, :empty, :function -> 'null'
	:string -> '"' << escape(c) << '"'
	:atom -> '"' << string(c) << '"'
	:int, :float, :bool -> string(c)
	// composite types
	:list -> '[' << c |> map(serialize) |> join(',') << ']'
	:object -> '{' << keys(c) |> map(fn(k) '"' << escape(k) << '":' << serialize(c.(k))) |> join(',') << '}'
}

// reader implementation with internal state for parsing
fn Reader(s) {
	index := 0
	// has there been a parse error?
	err? := false

	fn next {
		index <- index + 1
		default(s.(index - 1), '')
	}
	fn peek default(s.(index), '')
	fn nextWord(n) if index + n > len(s) {
		true -> {
			index <- len(s)
			?
		}
		_ -> {
			word := s |> slice(index, index + n)
			index <- index + n
			word
		}
	}
	// fast-forward through whitespace
	fn forward {
		fn sub if space?(peek()) -> {
			index <- index + 1
			sub()
		}
		sub()
	}

	{
		next: next
		peek: peek
		forward: forward
		nextWord: nextWord
		done?: fn() index >= len(s)
		err!: fn {
			err? <- true
			:error
		}
		err?: fn() err?
	}
}

fn parseNull(r) if r.nextWord(4) {
	'null' -> ?
	_ -> r.err!()
}

fn parseString(r) {
	next := r.next

	next() // eat the double quote

	fn sub(acc) if c := next() {
		'' -> r.err!()
		'\\' -> sub(acc << if c := next() {
			't' -> '\t'
			'n' -> '\n'
			'r' -> '\r'
			'f' -> '\f'
			'"' -> '"'
			_ -> c
		})
		'"' -> acc
		_ -> sub(acc << c)
	}
	sub('')
}

fn parseNumber(r) {
	peek := r.peek
	next := r.next

	decimal? := false
	negate? := if peek() {
		'-' -> {
			next()
			true
		}
		_ -> false
	}

	fn sub(acc) if peek() {
		'.' -> if decimal? {
			true -> r.err!()
			_ -> {
				decimal? <- true
				sub(acc << next())
			}
		}
		'0', '1', '2', '3', '4'
		'5', '6', '7', '8', '9' -> sub(acc << next())
		_ -> acc
	}
	result := sub('')

	if parsed := if decimal? {
		true -> float(result)
		_ -> int(result)
	} {
		? -> :error
		_ -> if negate? {
			true -> -parsed
			_ -> parsed
		}
	}
}

fn parseTrue(r) if r.nextWord(4) {
	'true' -> true
	_ -> r.err!()
}

fn parseFalse(r) if r.nextWord(5) {
	'false' -> false
	_ -> r.err!()
}

fn parseList(r) {
	err? := r.err?
	peek := r.peek
	next := r.next
	forward := r.forward

	next() // eat the [
	forward()

	fn sub(acc) if err?() {
		true -> :error
		_ -> if peek() {
			'' -> r.err!()
			']' -> {
				next() // eat the ]
				acc
			}
			_ -> {
				acc << _parseReader(r)
				forward()
				if peek() = ',' -> next()

				forward()
				sub(acc)
			}
		}
	}
	sub([])
}

fn parseObject(r) {
	err? := r.err?
	peek := r.peek
	next := r.next
	forward := r.forward

	next() // eat the {
	forward()

	fn sub(acc) if err?() {
		true -> :error
		_ -> if peek() {
			'' -> r.err!()
			'}' -> {
				next()
				acc
			}
			_ -> {
				key := parseString(r)
				if !err?() -> {
					forward()
					if peek() = ':' -> next()

					val := _parseReader(r)
					if !err?() -> {
						forward()
						if peek() = ',' -> next()

						forward()
						sub(acc.(key) := val)
					}
				}
			}
		}
	}
	sub({})
}

fn _parseReader(r) {
	// trim preceding whitespace
	r.forward()

	result := if r.peek() {
		'n' -> parseNull(r)
		'"' -> parseString(r)
		't' -> parseTrue(r)
		'f' -> parseFalse(r)
		'[' -> parseList(r)
		'{' -> parseObject(r)
		_ -> parseNumber(r)
	}

	// if there was a parse error, return :error
	if r.err?() {
		true -> :error
		_ -> result
	}
}

// parse takes a potentially valid JSON string, and returns its Oak
// representation if valid JSON, or :error if the parse fails.
fn parse(s) Reader(s) |> _parseReader()