From 29676c717eac0f2c5e6cc02941b479dd5d81727c Mon Sep 17 00:00:00 2001 From: Leah Neukirchen Date: Tue, 24 Jul 2018 17:18:01 +0200 Subject: initial revision --- SPEC.md | 106 ++++++++++++++++++++++++++++++++ lib/sson.rb | 199 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 305 insertions(+) create mode 100644 SPEC.md create mode 100644 lib/sson.rb diff --git a/SPEC.md b/SPEC.md new file mode 100644 index 0000000..78b6d30 --- /dev/null +++ b/SPEC.md @@ -0,0 +1,106 @@ +# SSON - S-Expression Standard Object Notation +a faithful embedding of JSON (RFC 8259) into a S-Expression syntax. + +To the extent possible under law, Leah Neukirchen +has waived all copyright and related or neighboring rights to this work. +http://creativecommons.org/publicdomain/zero/1.0/ + +## Syntax + + value = "#n" / "#t" / "#f" / + "(" value* ")" / "#(" (string value)* ")" / + json-number / string + string = json-string / literal + literal = [^0-9#;()" \t\r\n+-][^#;()" \t\r\n]* + ";" starts a comment until end of line, it is treated as whitespace + +The grammar can be parsed with 1 byte lookahead. + +## Example + + [{ + "created_at": "Thu Jun 22 21:00:00 +0000 2017", + "id": 877994604561387500, + "id_str": "877994604561387520", + "text": "Creating a Grocery List Manager Using Angular, Part 1: Add & Display Items https://t.co/xFox78juL1 #Angular", + "truncated": false, + "entities": { + "hashtags": [{ + "text": "Angular", + "indices": [103, 111] + }], + "symbols": [], + "user_mentions": [], + "urls": [{ + "url": "https://t.co/xFox78juL1", + "expanded_url": "http://buff.ly/2sr60pf", + "display_url": "buff.ly/2sr60pf", + "indices": [79, 102] + }] + }, + "source": "Buffer", + "user": { + "id": 772682964, + "id_str": "772682964", + "name": "SitePoint JavaScript", + "screen_name": "SitePointJS", + "location": "Melbourne, Australia", + "description": "Keep up with JavaScript tutorials, tips, tricks and articles at SitePoint.", + "url": "http://t.co/cCH13gqeUK", + "entities": { + "url": { + "urls": [{ + "url": "http://t.co/cCH13gqeUK", + "expanded_url": "http://sitepoint.com/javascript", + "display_url": "sitepoint.com/javascript", + "indices": [0, 22] + }] + }, + "description": { + "urls": [] + } + }, + "protected": false, + "followers_count": 2145, + "friends_count": 18, + "listed_count": 328, + "created_at": "Wed Aug 22 02:06:33 +0000 2012", + "favourites_count": 57, + "utc_offset": 43200, + "time_zone": "Wellington" + } + }] + + (#(created_at "Thu Jun 22 21:00:00 +0000 2017" + id 877994604561387500 + id_str "877994604561387520" + text "Creating a Grocery List Manager Using Angular, Part 1: Add & Display Items https://t.co/xFox78juL1 #Angular" + truncated #f + entities #(hashtags (#(text Angular indices (103 111))) + symbols () + user_mentions () + urls (#(url https://t.co/xFox78juL1 + expanded_url http://buff.ly/2sr60pf + display_url buff.ly/2sr60pf + indices (79 102)))) + source "Buffer" + user #(id 772682964 + id_str "772682964" + name "SitePoint JavaScript" + screen_name SitePointJS + location "Melbourne, Australia" + description "Keep up with JavaScript tutorials, tips, tricks and articles at SitePoint." + url http://t.co/cCH13gqeUK + entities #(url #(urls (#(url http://t.co/cCH13gqeUK + expanded_url http://sitepoint.com/javascript + display_url sitepoint.com/javascript + indices (0 22)))) + description #(urls ())) + protected #f + followers_count 2145 + friends_count 18 + listed_count 328 + created_at "Wed Aug 22 02:06:33 +0000 2012" + favourites_count 57 + utc_offset 43200 + time_zone Wellington))) diff --git a/lib/sson.rb b/lib/sson.rb new file mode 100644 index 0000000..684749a --- /dev/null +++ b/lib/sson.rb @@ -0,0 +1,199 @@ +# SSON - S-Expression Standard Object Notation +# a faithful embedding of JSON (RFC 8259) into a S-Expression syntax + +# +# value = "#n" / "#t" / "#f" / +# "(" value* ")" / "#(" (string value)* ")" / +# json-number / string +# string = json-string / literal +# literal = [^0-9#;()" \t\r\n+-][^#;()" \t\r\n]* +# ";" starts a comment until end of line, it is treated as whitespace +# + +# To the extent possible under law, Leah Neukirchen +# has waived all copyright and related or neighboring rights to this work. +# http://creativecommons.org/publicdomain/zero/1.0/ + +require 'json' +require 'prettyprint' +require 'strscan' + +module SSON; end +class << SSON + VERSION = "0.1" + + SCANIDENT = /[^0-9#;()" \t\r\n+-][^#;()" \t\r\n]*/ + IDENT = /\A#{SCANIDENT}\z/ + + class SSONError < StandardError; end + class GeneratorError < SSONError; end + class ParserError < SSONError; end + + def generate(o) + case o + when Hash + r = "#(" + o.each { |k, v| + r << " " if r.size > 2 + r << generate(k.to_s) << " " << generate(v) + } + r << ")" + r + when Array + r = "(" + o.each { |e| + r << " " if r.size > 1 + r << generate(e) + } + r << ")" + r + when true + "#t" + when false + "#f" + when nil + "#n" + when Integer + o.to_s + when Float + if o.nan? + raise GeneratorError, "NaN not allowed in SSON" + elsif o.infinite? + raise GeneratorError, "Infinity not allowed in SSON" + end + o.to_s + when String + if o =~ IDENT + o + else + JSON.generate(o) + end + else + # like JSON.generate + generate(o.to_s) + end + end + + def pretty_generate(obj) + return PrettyPrint.format('', 80) { |q| + inner = lambda { |o| + case o + when Array + q.group(1) { + q.text "(" + b = false + o.each { |e| + q.breakable " " if b + b = true + inner[e] + } + q.text ")" + } + when Hash + q.group(2) { + q.text "#(" + b = false + o.each { |k, v| + q.breakable " " if b + b = true + q.group { + inner[k] + q.text " " + inner[v] + } + } + q.text ")" + } + + when true; q.text "#t" + when false; q.text "#f" + when nil; q.text "#n" + when Integer + q.text o.to_s + when Float + if o.nan? + raise GeneratorError, "NaN not allowed in SSON" + elsif o.infinite? + raise GeneratorError, "Infinity not allowed in SSON" + end + q.text o.to_s + when String + if o =~ IDENT + q.text o # ES5 identifier + else + q.text JSON.generate(o) + end + end + } + inner[obj] + } + end + + def parse(str) + e = SSON.enum_for(:tok, str) + r = parse_form e + begin + e.next + rescue StopIteration + r + else + raise ParserError, "trailing garbage" + end + end + + private + + def tok(str) + ss = StringScanner.new(str) + until ss.eos? + if ss.scan(/\A[ \t\r\n]+/) || ss.scan(/;[^\n]*$/) + # ignore + elsif ss.scan(/\(/); yield :OPEN + elsif ss.scan(/\)/); yield :CLOSE + elsif ss.scan(/#\(/); yield :HASH + elsif ss.scan(/#n/); yield :NULL + elsif ss.scan(/#t/); yield :TRUE + elsif ss.scan(/#f/); yield :FALSE + elsif s = ss.scan(/"(\\"|[^"])*"/) + yield JSON.parse(s) + elsif s = ss.scan(SCANIDENT) + yield s + elsif s = ss.scan(/[-+]?(?:\d*\.?\d+|\d+\.?\d*)(?:[eE][-+]?\d+)?/) + yield Float(s) + else + raise ParserError, "invalid SSON: " + ss.peek(20).dump + end + end + end + + def parse_form(e) + case t = e.next + when :OPEN + r = [] + while e.peek != :CLOSE + r << parse_form(e) + end + e.next + r + when :HASH + r = {} + while e.peek != :CLOSE + k = e.next + raise "non string key" unless k.kind_of?(String) + v = parse_form(e) + r[k] = v + end + e.next + r + when :CLOSE; raise ParserError, "invalid SSON, toplevel )" + when :NULL; nil + when :TRUE; true + when :FALSE; false + when String; t + when Float; t + end + rescue StopIteration + raise ParserError, "early EOF" + end + +end -- cgit 1.4.1