τau / encoding/json /

json.tau

source
/Users/niconex/Documents/tau/stdlib/encoding/json/json.tau
1# json - reading and writing JSON.2#3# Unmarshal turns the text into the values of the language, an object into a4# map and an array into a list, and Marshal writes them back:5#6#	json = import("encoding/json")7#8#	# A raw string, or the braces would be read as interpolation and the9#	# quotes would need escaping.10#	v = json.Unmarshal(`{"name": "tau", "tags": [1, 2]}`)11#	println(v["name"], v["tags"][0])12#	println(json.MarshalIndent(v, "  "))13#14# What it reads is JSON and nothing more: no trailing commas, no comments, no15# single quotes. A text it will not take comes back as an error saying at16# which line and column it gave up. An escape of the form \uXXXX is kept as17# it was written rather than turned into the character it names.1819spaces = "\t\n\v\f\r "2021# --- tiny char helpers (avoid external deps) ---22containsChar = fn(set, ch) {23	for i = 0; i < len(set); ++i {24		if slice(set, i, i + 1) == ch {25			return true26		}27	}28	false29}3031isHex = fn(c) { (c >= "0" && c <= "9") || (c >= "a" && c <= "f") || (c >= "A" && c <= "F") }3233# --- Parser constructor ---34Parser = fn(src) {35	p = new()36	p.s = src37	p.i = 038	p.n = len(src)39	p.line = 140	p.col = 14142	p.inBounds = fn() { p.i >= 0 && p.i < p.n }43	p.peek = fn() { if p.inBounds() { p.s[p.i] } else { "" } }4445	p.advance = fn(n) {46		for k = 0; k < n && p.inBounds(); ++k {47			if p.peek() == "\n" {48				p.line += 149				p.col = 150			} else {51				p.col += 152			}53			p.i += 154		}55	}5657	p.errorf = fn(msg) { error("json parse error at {p.line}:{p.col} -> {msg}") }5859	p.skipSpace = fn() {60		for p.inBounds() && containsChar(spaces, p.peek()) { p.advance(1) }61	}6263	p.expectChar = fn(ch) {64		p.skipSpace()65		if !p.inBounds() || p.peek() != ch {66			return p.errorf("expected '" + ch + "'")67		}68		p.advance(1)69		true70	}7172	p.parseString = fn() {73		p.skipSpace()74		if !p.inBounds() || p.peek() != "\"" {75			return p.errorf(`expected '"' to start string`)76		}77		p.advance(1)7879		buf = ""80		for p.inBounds() {81			c = p.peek()82			p.advance(1)8384			if c == "\"" {85				return buf86			}8788			if c == "\\" {89				if !p.inBounds() {90					return p.errorf("unterminated escape")91				}9293				esc = p.peek()94				p.advance(1)9596				if esc == "\"" {97					buf += "\""98				} else if esc == "\\" {99					buf += "\\"100				} else if esc == "/" {101					buf += "/"102				} else if esc == "b" {103					buf += "\b"104				} else if esc == "f" {105					buf += "\f"106				} else if esc == "n" {107					buf += "\n"108				} else if esc == "r" {109					buf += "\r"110				} else if esc == "t" {111					buf += "\t"112				} else if esc == "u" {113					# keep \uXXXX verbatim114					if !(p.i + 3 < p.n) {115						return p.errorf("invalid \\u escape")116					}117118					h0 = p.s[p.i]; h1 = p.s[p.i + 1]119					h2 = p.s[p.i + 2]; h3 = p.s[p.i + 3]120121					if !isHex(h0) || !isHex(h1) || !isHex(h2) || !isHex(h3) {122						return p.errorf("invalid hex in \\u escape")123					}124					buf += "\\u" + h0 + h1 + h2 + h3125					p.advance(4)126				} else {127					return p.errorf("unknown escape sequence '\\" + esc + "'")128				}129			} else {130				buf += c131			}132		}133134		p.errorf("unterminated string")135	}136137	p.parseNumber = fn() {138		p.skipSpace()139		start = p.i140141		if p.inBounds() && p.peek() == "-" {142			p.advance(1)143		}144		if !p.inBounds() || p.peek() < "0" || p.peek() > "9" {145			return p.errorf("expected digit")146		}147		for p.inBounds() && p.peek() >= "0" && p.peek() <= "9" {148			p.advance(1)149		}150151		isFloat = false152153		if p.inBounds() && p.peek() == "." {154			isFloat = true155			p.advance(1)156			if !p.inBounds() || p.peek() < "0" || p.peek() > "9" {157				return p.errorf("expected digit after '.'")158			}159			for p.inBounds() && p.peek() >= "0" && p.peek() <= "9" {160				p.advance(1)161			}162		}163164		if p.inBounds() && (p.peek() == "e" || p.peek() == "E") {165			isFloat = true166			p.advance(1)167			if p.inBounds() && (p.peek() == "+" || p.peek() == "-") {168				p.advance(1)169			}170			if !p.inBounds() || p.peek() < "0" || p.peek() > "9" {171				return p.errorf("expected digit in the exponent")172			}173			for p.inBounds() && p.peek() >= "0" && p.peek() <= "9" {174				p.advance(1)175			}176		}177178		text = slice(p.s, start, p.i)179		return if isFloat { float(text) } else { int(text) }180	}181182	p.matchWord = fn(w) {183		for k = 0; k < len(w); ++k {184			if !(p.i + k < p.n) || p.s[p.i + k] != w[k] {185				return false186			}187		}188		p.i = p.i + len(w)189		p.col = p.col + len(w) # approximate (ignores newlines; literals don't have)190		true191	}192193	p.parseLiteral = fn() {194		p.skipSpace()195		if p.inBounds() && p.peek() == "t" && p.matchWord("true") { return true }196		if p.inBounds() && p.peek() == "f" && p.matchWord("false") { return false }197		if p.inBounds() && p.peek() == "n" && p.matchWord("null") { return null }198		p.errorf("expected literal true/false/null")199	}200201	p.parseArray = fn() {202		if failed(_ = p.expectChar("[")) { return _ }203204		arr = []205206		p.skipSpace()207		if p.inBounds() && p.peek() == "]" { p.advance(1); return arr }208209		for {210			if failed(v = p.parseValue()) { return v }211			arr = append(arr, v)212213			p.skipSpace()214			if !p.inBounds() { return p.errorf("unterminated array") }215216			if p.peek() == "," {217				p.advance(1)218				p.skipSpace()219				if !p.inBounds() { return p.errorf("value expected after ','") }220				continue221			}222223			if p.peek() == "]" { p.advance(1); return arr }224225			return p.errorf("expected ',' or ']'")226		}227	}228229	p.parseObject = fn() {230		if failed(_ = p.expectChar(`{`)) { return _ }231232		obj = {} # Tau map233234		p.skipSpace()235		if p.inBounds() && p.peek() == `}` { p.advance(1); return obj }236237		for {238			if failed(k = p.parseString()) { return k }239			if failed(_ = p.expectChar(":")) { return _ }240			if failed(v = p.parseValue()) { return v }241			obj[k] = v242243			p.skipSpace()244			if !p.inBounds() { return p.errorf("unterminated object") }245246			if p.peek() == "," {247				p.advance(1)248				p.skipSpace()249				if p.inBounds() && p.peek() == `}` {250					return p.errorf("trailing comma in object")251				}252				continue253			}254255			if p.peek() == `}` {256				p.advance(1)257				return obj258			}259260			return p.errorf("expected ',' or '}}'")261		}262	}263264	p.parseValue = fn() {265		p.skipSpace()266		if !p.inBounds() {267			return p.errorf("unexpected end while expecting value")268		}269270		c = p.peek()271		if c == `{` {272			return p.parseObject()273		}274		if c == `[` {275			return p.parseArray()276		}277		if c == `"` {278			return p.parseString()279		}280		if c == "-" || (c >= "0" && c <= "9") {281			return p.parseNumber()282		}283		p.parseLiteral()284	}285286	p.parse = fn() {287		val = p.parseValue()288		if failed(val) {289			return val290		}291		p.skipSpace()292		if p.i != p.n {293			return p.errorf("extra data after valid JSON value")294		}295		val296	}297298	p299}300301# Public API302Unmarshal = fn(s) {303	# should never fail, but be consistent304	if failed(p = Parser(s)) {305		return p306	}307	p.parse()308}309310escapeJSONString = fn(s) {311	buf = "\""312	for i = 0; i < len(s); ++i {313		ch = s[i]314315		# fast path316		if ch != "\"" && ch != "\\" && ch != "\b" && ch != "\f" && ch != "\n" && ch != "\r" && ch != "\t" && bytes(ch)[0] >= 32 {317			buf += ch318			continue319		}320321		# escapes / control chars322		if ch == "\"" {323			buf += "\\\""324		} else if ch == "\\" {325			buf += "\\\\"326		} else if ch == "\b" {327			buf += "\\b"328		} else if ch == "\f" {329			buf += "\\f"330		} else if ch == "\n" {331			buf += "\\n"332		} else if ch == "\r" {333			buf += "\\r"334		} else if ch == "\t" {335			buf += "\\t"336		} else {337			# control char < 0x20 -> \u00XX338			n = bytes(ch)[0]339			digits = "0123456789abcdef"340			hi = n / 16341			lo = n - hi * 16342			buf += "\\u00" + digits[hi] + digits[lo]343		}344	}345	buf + "\""346}347348encodeListJSON = fn(xs) {349	buf = "["350	for i = 0; i < len(xs); ++i {351		if i > 0 { buf += "," }352		if failed(v = encodeJSON(xs[i])) {353			return v354		}355		buf += v356	}357	buf + "]"358}359360encodeMapJSON = fn(m) {361	# JSON requires string keys362	ks = keys(m)363	buf = `{`364	for i = 0; i < len(ks); ++i {365		k = ks[i]366		if type(k) != "string" {367			return error("json encode error: object key is not a string")368		}369		if i > 0 { buf += "," }370		buf += escapeJSONString(k) + ":"371		if failed(v = encodeJSON(m[k])) {372			return v373		}374		buf += v375	}376	buf + `}`377}378379encodeJSON = fn(x) {380	t = type(x)381382	return if t == "null" {383		"null"384	} else if t == "string" {385		escapeJSONString(x)386	} else if t == "int" || t == "float" || t == "bool" {387		string(x)388	} else if t == "list" {389		encodeListJSON(x)390	} else if t == "map" || t == "object" {391		encodeMapJSON(x)392	} else {393		error("json encode error: unsupported type " + t)394	}395}396397# Marshal returns the JSON form of a value: null, booleans, numbers, strings,398# lists, maps and objects, which are written as objects the way a struct is399# in Go.400Marshal = fn(v) { encodeJSON(v) }401402# Valid reports whether s holds one well formed JSON value and nothing else.403Valid = fn(s) { !failed(Unmarshal(s)) }404405# MarshalIndent is Marshal with the output spread over several lines, indent406# being what one level is written with. With no indent a tab is used.407MarshalIndent = fn(v, indent) {408	if failed(s = Marshal(v)) {409		return s410	}411	return Indent(s, indent)412}413414# Indent rewrites JSON with one value per line. It walks the text rather than415# the value, so what is inside strings is left alone. The braces are written416# as raw strings: in an ordinary one a brace opens an interpolation.417Indent = fn(src, indent) {418	if indent == null {419		indent = "\t"420	}421422	out = ""423	depth = 0424	inString = false425426	newline = fn(d) {427		s = "\n"428		for i = 0; i < d; ++i {429			s = s + indent430		}431		return s432	}433434	for i = 0; i < len(src); ++i {435		c = src[i]436437		if inString {438			out = out + c439			# A quote closes the string unless it is escaped, and a backslash440			# takes the next character with it whatever it is.441			if c == "\\" && i + 1 < len(src) {442				++i443				out = out + src[i]444			} else if c == "\"" {445				inString = false446			}447			continue448		}449450		if c == "\"" {451			inString = true452			out = out + c453		} else if c == `{` || c == "[" {454			++depth455			out = out + c + newline(depth)456		} else if c == `}` || c == "]" {457			--depth458			out = out + newline(depth) + c459		} else if c == "," {460			out = out + c + newline(depth)461		} else if c == ":" {462			out = out + c + " "463		} else {464			out = out + c465		}466	}467	return out468}