shithub: purgatorio

ref: ec35f468e0eba87c9f09cbbe5fa8af2591e6f914
dir: /appl/lib/json.b/

View raw version
implement JSON;

#
# Javascript `Object' Notation (JSON): RFC4627
#

include "sys.m";
	sys: Sys;

include "bufio.m";
	bufio: Bufio;
	Iobuf: import bufio;

include "json.m";

init(b: Bufio)
{
	sys = load Sys Sys->PATH;
	bufio = b;
}

jvarray(a: array of ref JValue): ref JValue.Array
{
	return ref JValue.Array(a);
}

jvbig(i: big): ref JValue.Int
{
	return ref JValue.Int(i);
}

jvfalse(): ref JValue.False
{
	return ref JValue.False;
}

jvint(i: int): ref JValue.Int
{
	return ref JValue.Int(big i);
}

jvnull(): ref JValue.Null
{
	return ref JValue.Null;
}

jvobject(m: list of (string, ref JValue)): ref JValue.Object
{
	# could `uniq' the labels
	return ref JValue.Object(m);
}

jvreal(r: real): ref JValue.Real
{
	return ref JValue.Real(r);
}

jvstring(s: string): ref JValue.String
{
	return ref JValue.String(s);
}

jvtrue(): ref JValue.True
{
	return ref JValue.True;
}

Syntax: exception(string);
Badwrite: exception;

readjson(fd: ref Iobuf): (ref JValue, string)
{
	{
		p := Parse.mk(fd);
		c := p.getns();
		if(c == Bufio->EOF)
			return (nil, nil);
		p.unget(c);
		return (readval(p), nil);
	}exception e{
	Syntax =>
		return (nil, sys->sprint("JSON syntax error (offset %bd): %s", fd.offset(), e));
	}
}

writejson(fd: ref Iobuf, val: ref JValue): int
{
	{
		writeval(fd, val);
		return 0;
	}exception{
	Badwrite =>
		return -1;
	}
}

#
# value ::= string | number | object | array | 'true' | 'false' | 'null'
#
readval(p: ref Parse): ref JValue raises(Syntax)
{
	{
		while((c := p.getc()) == ' ' || c == '\t' || c == '\n' || c == '\r')
			{}
		if(c < 0){
			if(c == Bufio->EOF)
				raise Syntax("unexpected end-of-input");
			raise Syntax(sys->sprint("read error: %r"));
		}
		case c {
		'{' =>
			# object ::= '{' [pair (',' pair)*] '}'
			l:  list of (string, ref JValue);
			if((c = p.getns()) != '}'){
				p.unget(c);
				rl: list of (string, ref JValue);
				do{
					# pair ::= string ':' value
					c = p.getns();
					if(c != '"')
						raise Syntax("missing member name");
					name := readstring(p, c);
					if(p.getns() != ':')
						raise Syntax("missing ':'");
					rl = (name, readval(p)) :: rl;
				}while((c = p.getns()) == ',');
				for(; rl != nil; rl = tl rl)
					l = hd rl :: l;
			}
			if(c != '}')
				raise Syntax("missing '}' at end of object");
			return ref JValue.Object(l);
		'[' =>
			#	array ::= '[' [value (',' value)*] ']'
			l: list of ref JValue;
			n := 0;
			if((c = p.getns()) != ']'){
				p.unget(c);
				do{
					l = readval(p) :: l;
					n++;
				}while((c = p.getns()) == ',');
			}
			if(c != ']')
				raise Syntax("missing ']' at end of array");			
			a := array[n] of ref JValue;
			for(; --n >= 0; l = tl l)
				a[n] = hd l;
			return ref JValue.Array(a);
		'"' =>
			return ref JValue.String(readstring(p, c));
		'-' or '0' to '9' =>
			#	number ::=	int frac? exp?
			#	int ::= '-'? [0-9] | [1-9][0-9]+
			#	frac ::= '.' [0-9]+
			#	exp ::= [eE][-+]? [0-9]+
			if(c == '-')
				intp := "-";
			else
				p.unget(c);
			intp += readdigits(p);		# we don't enforce the absence of leading zeros
			fracp: string;
			c = p.getc();
			if(c == '.'){
				fracp = readdigits(p);
				c = p.getc();
			}
			exp := "";
			if(c == 'e' || c == 'E'){
				exp[0] = c;
				c = p.getc();
				if(c == '-' || c == '+')
					exp[1] = c;
				else
					p.unget(c);
				exp += readdigits(p);
			}else
				p.unget(c);
			if(fracp != nil || exp != nil)
				return ref JValue.Real(real (intp+"."+fracp+exp));
			return ref JValue.Int(big intp);
		'a' to 'z' =>
			# 'true' | 'false' | 'null'
			s: string;
			do{
				s[len s] = c;
			}while((c = p.getc()) >= 'a' && c <= 'z');
			p.unget(c);
			case s {
			"true" =>	return ref JValue.True();
			"false" =>	return ref JValue.False();
			"null" =>	return ref JValue.Null();
			* =>	raise Syntax("invalid literal: "+s);
			}
		* =>
			raise Syntax(sys->sprint("unexpected character #%.4ux", c));
		}
	}exception{
	Syntax =>
		raise;
	}
}

# string ::= '"' char* '"'
# char ::= [^\x00-\x1F"\\] | '\"' | '\/' | '\b' | '\f' | '\n' | '\r' | '\t' | '\u' hex hex hex hex
readstring(p: ref Parse, delim: int): string raises(Syntax)
{
	{
		s := "";
		while((c := p.getc()) != delim && c >= 0){
			if(c == '\\'){
				c = p.getc();
				if(c < 0)
					break;
				case c {
				'b' =>	c =  '\b';
				'f' =>		c =  '\f';
				'n' =>	c =  '\n';
				'r' =>		c =  '\r';
				't' =>		c =  '\t';
				'u' =>
					c = 0;
					for(i := 0; i < 4; i++)
						c = (c<<4) | hex(p.getc());
				* =>		;	# identity, including '"', '/', and '\'
				}
			}
			s[len s] = c;
		}
		if(c < 0){
			if(c == Bufio->ERROR)
				raise Syntax(sys->sprint("read error: %r"));
			raise Syntax("unterminated string");
		}
		return s;
	}exception{
	Syntax =>
		raise;
	}
}

# hex ::= [0-9a-fA-F]
hex(c: int): int raises(Syntax)
{
	case c {
	'0' to '9' =>
		return c-'0';
	'a' to 'f' =>
		return 10+(c-'a');
	'A' to 'F' =>
		return 10+(c-'A');
	* =>
		raise Syntax("invalid hex digit");
	}
}

# digits ::= [0-9]+
readdigits(p: ref Parse): string raises(Syntax)
{
	c := p.getc();
	if(!(c >= '0' && c <= '9'))
		raise Syntax("expected integer literal");
	s := "";
	s[0] = c;
	while((c = p.getc()) >= '0' && c <= '9')
		s[len s] = c;
	p.unget(c);
	return s;
}

writeval(out: ref Iobuf, o: ref JValue) raises(Badwrite)
{
	{
		if(o == nil){
			puts(out, "null");
			return;
		}
		pick r := o {
		String =>
			writestring(out, r.s);
		Int =>
			puts(out, r.text());
		Real =>
			puts(out, r.text());
		Object =>	# '{' [pair (',' pair)*] '}'
			putc(out, '{');
			for(l := r.mem; l != nil; l = tl l){
				if(l != r.mem)
					putc(out, ',');
				(n, v) := hd l;
				writestring(out, n);
				putc(out, ':');
				writeval(out, v);
			}
			putc(out, '}');
		Array =>	# '[' [value (',' value)*] ']'
			putc(out, '[');
			for(i := 0; i < len r.a; i++){
				if(i != 0)
					putc(out, ',');
				writeval(out, r.a[i]);
			}
			putc(out, ']');
		True =>
			puts(out, "true");
		False =>
			puts(out, "false");
		Null =>
			puts(out, "null");
		* =>
			raise "writeval: unknown value";	# can't happen
		}
	}exception{
	Badwrite =>
		raise;
	}
}

writestring(out: ref Iobuf, s: string) raises(Badwrite)
{
	{
		putc(out, '"');
		for(i := 0; i < len s; i++){
			c := s[i];
			if(needesc(c))
				puts(out, escout(c));
			else
				putc(out, c);
		}
		putc(out, '"');
	}exception{
	Badwrite =>
		raise;
	}
}

escout(c: int): string
{
	case c {
	'"' =>		return "\\\"";
	'\\' =>	return "\\\\";
	'/' =>	return "\\/";
	'\b' =>	return "\\b";
	'\f' =>	return "\\f";
	'\n' =>	return "\\n";
	'\t' =>	return "\\t";
	'\r' =>	return "\\r";
	* =>		return sys->sprint("\\u%.4ux", c);
	}
}

puts(out: ref Iobuf, s: string) raises(Badwrite)
{
	if(out.puts(s) == Bufio->ERROR)
		raise Badwrite;
}

putc(out: ref Iobuf, c: int) raises(Badwrite)
{
	if(out.putc(c) == Bufio->ERROR)
		raise Badwrite;
}

Parse: adt {
	input:	ref Iobuf;
	eof:		int;

	mk:		fn(io: ref Iobuf): ref Parse;
	getc:		fn(nil: self ref Parse): int;
	unget:	fn(nil: self ref Parse, c: int);
	getns:	fn(nil: self ref Parse): int;
};

Parse.mk(io: ref Iobuf): ref Parse
{
	return ref Parse(io, 0);
}

Parse.getc(p: self ref Parse): int
{
	if(p.eof)
		return p.eof;
	c := p.input.getc();
	if(c < 0)
		p.eof = c;
	return c;
}

Parse.unget(p: self ref Parse, c: int)
{
	if(c >= 0)
		p.input.ungetc();
}

# skip white space
Parse.getns(p: self ref Parse): int
{
	while((c := p.getc()) == ' ' || c == '\t' || c == '\n' || c == '\r')
		{}
	return c;
}

JValue.isarray(v: self ref JValue): int
{
	return tagof v == tagof JValue.Array;
}

JValue.isint(v: self ref JValue): int
{
	return tagof v == tagof JValue.Int;
}

JValue.isnumber(v: self ref JValue): int
{
	return tagof v == tagof JValue.Int || tagof v == tagof JValue.Real;
}

JValue.isobject(v: self ref JValue): int
{
	return tagof v == tagof JValue.Object;
}

JValue.isreal(v: self ref JValue): int
{
	return tagof v == tagof JValue.Real;
}

JValue.isstring(v: self ref JValue): int
{
	return tagof v == tagof JValue.String;
}

JValue.istrue(v: self ref JValue): int
{
	return tagof v == tagof JValue.True;
}

JValue.isfalse(v: self ref JValue): int
{
	return tagof v == tagof JValue.False;
}

JValue.isnull(v: self ref JValue): int
{
	return tagof v == tagof JValue.Null;
}

JValue.copy(v: self ref JValue): ref JValue
{
	pick r := v {
	True or False or Null =>
		return ref *r;
	Int =>
		return ref *r;
	Real =>
		return ref *r;
	String =>
		return ref *r;
	Array =>
		a := array[len r.a] of ref JValue;
		a[0:] = r.a;
		return ref JValue.Array(a);
	Object =>
		return ref *r;
	* =>
		raise "json: bad copy";	# can't happen
	}
}

JValue.eq(a: self ref JValue, b: ref JValue): int
{
	if(a == b)
		return 1;
	if(a == nil || b == nil || tagof a != tagof b)
		return 0;
	pick r := a {
	True or False or Null =>
		return 1;	# tags were equal above
	Int =>
		pick s := b {
		Int =>
			return r.value == s.value;
		}
	Real =>
		pick s := b {
		Real =>
			return r.value == s.value;
		}
	String =>
		pick s := b {
		String =>
			return r.s == s.s;
		}
	Array =>
		pick s := b {
		Array =>
			if(len r.a != len s.a)
				return 0;
			for(i := 0; i < len r.a; i++)
				if(r.a[i] == nil){
					if(s.a[i] != nil)
						return 0;
				}else if(!r.a[i].eq(s.a[i]))
					return 0;
			return 1;
		}
	Object =>
		pick s := b {
		Object =>
			ls := s.mem;
			for(lr := r.mem; lr != nil; lr = tl lr){
				if(ls == nil)
					return 0;
				(rn, rv) := hd lr;
				(sn, sv) := hd ls;
				if(rn != sn)
					return 0;
				if(rv == nil){
					if(sv != nil)
						return 0;
				}else if(!rv.eq(sv))
					return 0;
			}
			return ls == nil;
		}
	}
	return 0;
}

JValue.get(v: self ref JValue, mem: string): ref JValue
{
	pick r := v {
	Object =>
		for(l := r.mem; l != nil; l = tl l)
			if((hd l).t0 == mem)
				return (hd l).t1;
		return nil;
	* =>
		return nil;
	}
}

# might be better if the interface were applicative?
# this is similar to behaviour of Limbo's own ref adt, though
JValue.set(v: self ref JValue, mem: string, val: ref JValue)
{
	pick j := v {
	Object =>
		ol: list of (string, ref JValue);
		for(l := j.mem; l != nil; l = tl l)
			if((hd l).t0 == mem){
				l = tl l;
				for(; ol != nil; ol = tl ol)
					l = hd ol :: l;
				j.mem = l;
				return;
			}else
				ol = hd l :: ol;
		j.mem = (mem, val) :: j.mem;
	* =>
		raise "json: set non-object";
	}
}

JValue.text(v: self ref JValue): string
{
	if(v == nil)
		return "null";
	pick r := v {
	True =>
		return "true";
	False =>
		return "false";
	Null =>
		return "null";
	Int =>
		return string r.value;
	Real =>
		return sys->sprint("%f", r.value);
	String =>
		return quote(r.s);		# quoted, or not?
	Array =>
		s := "[";
		for(i := 0; i < len r.a; i++){
			if(i != 0)
				s += ", ";
			s += r.a[i].text();
		}
		return s+"]";
	Object =>
		s := "{";
		for(l := r.mem; l != nil; l = tl l){
			if(l != r.mem)
				s += ", ";
			s += quote((hd l).t0)+": "+(hd l).t1.text();
		}
		return s+"}";
	* =>
		return nil;
	}
}

quote(s: string): string
{
	ns := "\"";
	for(i := 0; i < len s; i++){
		c := s[i];
		if(needesc(c))
			ns += escout(c);
		else
			ns[len ns] = c;
	}
	return ns+"\"";
}

needesc(c: int): int
{
	return c == '"' || c == '\\' || c == '/' || c <= 16r1F;  # '/' is escaped to prevent "</xyz>" looking like an XML end tag(!)
}