- The
SIONType which can store and manipulate SION data. - conversion between
- JSON - from and to
- Property List - from and to
- MsgPack - from and to
- YAML - output only
SIONEncoderandSIONDecoderwhich {en,de}code your ownCodabletypes, likeJSONEncoderandJSONDecoder.
You can build SION directly as a literal.
var sion:SION = [
"nil": nil,
"bool": true,
"int": -42,
"double": 42.195,
"string": "漢字、カタカナ、ひらがなの入ったstring😇",
"array": [nil, true, 1, 1.0, "one", [1], ["one":1.0]],
"dictionary": [
"nil":nil, "bool":false, "int":0, "double":0.0, "string":"","array":[], "object":[:]
],
"url":"https://github.com/dankogai/"
]
sion["data"] = .Data("R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7")
sion["date"] = .Date(0.0)
sion["ext"] = .Ext("1NTU")Note that the RHS of the expression below is not exactly nil or true or -42. They are SION.Nil and SION.Bool(-42) and SION.Int(-42) respectively. That is why .Data and .Date are added later. Swift gets confused when you say
var sion:SION = [
"string": "漢字、カタカナ、ひらがなの入ったstring😇",
"data": .Data("R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7"),
]Because it cannot tell they type of the content in. "". In which case you can explicitly say
var sion:SION = [
"string": .String("漢字、カタカナ、ひらがなの入ったstring😇"),
"data": .Data("R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7"),
]or add "data" later.
SION(string:"""
[
"array": [
nil,
true,
1, // Int
1.0, // Double
"one",
[1],
["one" : 1.0]
],
"bool": true,
"data": .Data("R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7"),
"date": .Date(0x0p+0),
"dictionary": [
"array" : [],
"bool" : false,
"double" : 0x0p+0,
"int" : 0,
"nil" : nil,
"object" : [:],
"string" : ""
],
"double": 0x1.518f5c28f5c29p+5, // double in hex
"ext": .Ext("1NTU"), // 0xd4,0xd4,0xd4
"int": -42, // int in hex
"nil": nil,
"string": "漢字、カタカナ、ひらがなの入ったstring😇",
"url": "https://github.com/dankogai/"
]
""")from JSON string or JSON-emitting URL
SION(jsonString:"""
{
"array" : [
null,
true,
1,
1.0,
"one",
[
1
],
{
"one" : 1.0
}
],
"bool" : true,
"dictionary" : {
"array" : [],
"bool" : false,
"double" : 0.0,
"int" : 0,
"nil" : null,
"object" : {},
"string" : ""
},
"double" : 42.195,
"int" : -42,
"nil" : null,
"string" : "漢字、カタカナ、ひらがなの入ったstring😇",
"url" : "https://github.com/dankogai/"
}
""")SION(jsonUrlString:"https://api.github.com")from Property List
let plistXML = """
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>array</key>
<array>
<true/>
<integer>1</integer>
<real>1</real>
<string>one</string>
<array>
<integer>1</integer>
</array>
<dict>
<key>one</key>
<real>1</real>
</dict>
</array>
<key>bool</key>
<true/>
<key>data</key>
<data>
R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7
</data>
<key>date</key>
<date>1970-01-01T00:00:00Z</date>
<key>dictionary</key>
<dict>
<key>array</key>
<array/>
<key>bool</key>
<false/>
<key>double</key>
<real>0.0</real>
<key>int</key>
<integer>0</integer>
<key>object</key>
<dict/>
<key>string</key>
<string></string>
</dict>
<key>double</key>
<real>42.195</real>
<key>int</key>
<integer>-42</integer>
<key>string</key>
<string>漢字、カタカナ、ひらがなの入ったstring😇</string>
<key>url</key>
<string>https://github.com/dankogai/</string>
</dict>
</plist>
"""
SION(propertyList:plistXML.data(using:.utf8)!, format:.xml)from msgPack
import Foundation
SION(msgPack: Data([
0x82,0xa7,0x63,0x6f,0x6d,0x70,0x61,0x63,
0x74,0xc3,0xa6,0x73,0x63,0x68,0x65,0x6d,
0x61,0x00
])) // ["compact":true,"schema":0]once you have the SION object, converting to other formats is simple.
to SION string, all you need is stringify it. .description or "\(json)" should be enough.
sion.description
"\(sion)" // SION is CustomStringConvertibleIf you need JSON, simply call .json.
sion.jsonAnd .msgPack gives you msgPack in Data.
sion.msgPackSince JSON does not support Date and Data, .json converts Date to Number and Date to Base64-encoded String respectively. .json also coverts all non-String keys to String. Do not expect JSON to round trip. It is less expressive than SION.
You can also get YAML via .yaml.
sion.yamlAnd .propertyList for Property List.
// pick() removes Nil
String(data:try! sion.pick{ !$0.isNil }.propertyList(format: .xml), encoding:.utf8)!a blank SION Array is as simple as:
var sion = SION([])and you can assign elements like an ordinary array
sion[0] = nil
sion[1] = true
sion[2] = 1note RHS literals are NOT nil, true and 1 but .Null, .Bool(true) and .Number(1). Therefore this does NOT work
let one = "one"
sion[3] = one // error: cannot assign value of type 'String' to type 'SION'In which case you do this instead.
sion[3].string = oneThey are all getters and setters…
sion[1].bool = true
sion[2].int = 1
sion[3].double = 1.0
sion[4].string = "one"
sion[5].array = [1]
sion[6].dictionary = ["one":1]except for .number which is a getter-only property that retrieves numerical value in Double or nil if not numeric.
swift[1].number // nil
swift[2].number! // 1.0 - Int is cast to Double
swift[3].number! // 1.0As a getter they are optional which returns nil when the type mismaches.
sion[1].bool // Optional(true)
sion[1].int // nilTherefore, you can mutate like so:
sion[2].int! += 1 // now 2
sion[3].double! *= 0.5 // now 0.5
sion[4].string!.removeLast() // now "on"
sion[5].array!.append(2) // now [1, 2]
sion[6].dictionary!["two"] = 2 // now ["one":1,"two":2]When you assign values to SION array with an out-of-bound index, it is automatically streched with unassigned elements set to null, just like an ECMAScript Array
sion[10] = false // sion[6...9] are nullAs you may have guessed by now, a blank SION dictionary is:
sion = SION([:])
And manipulate intuitively like so.
sion["nil"] = nil
sion["bool"] = false
sion["int"] = 0
sion["double"] = 0.0
sion["string"] = ""
sion["array"] = []
sion["dictionary"] = [:]SION is a recursive data type. For recursive data types, you need a recursive method that traverses the data deep down. For that purpuse, SION offers .pick and .walk.
.pick is a ".deepFilter" that filters recursively. You've already seen it above. It takes a filter function of type (SION)->Bool. That function is applied to all leaf values of the tree and leaves that do not meet the predicate are pruned.
// because property list does not accept null
let sion4plist = sion.pick{ !$0.isNil }.walk is a deepMap that transforms recursively. This one is a little harder because you have to consider what to do on node and leaves separately. To make your life easier three different versions of .walk are provided. The first one just takes a leaf node.
// square all ints and leave anything else
SION([0,[1,[2,3,[4,5,6]]], true]).walk {
guard let n = $0.int else { return $0 }
return SION(n * n)
}The second forms just takes a node. Instead of explaining it, let me show you how .pick is implemented by extending SION with .select that does exactly the same as .pick.
extension SION {
func select(picker:(SION)->Bool)->SION {
return self.walk{ node, pairs, depth in
switch node.type {
case .array:
return .Array(pairs.map{ $0.1 }.filter({ picker($0) }) )
case .dictionary:
var o = [Key:Value]()
pairs.filter{ picker($0.1) }.forEach{ o[$0.0.key!] = $0.1 }
return .Dictionary(o)
default:
return .Error(.notIterable(node.type))
}
}
}
}And the last form takes both. Unlike the previous ones this one can return other than SION . Here is how .yaml is implemented.
extension SION {
public var yaml:String {
return self.walk(depth:0, collect:{ node, pairs, depth in
let indent = Swift.String(repeating:" ", count:depth)
var result = ""
switch node.type {
case .array:
guard !pairs.isEmpty else { return "[]"}
result = pairs.map{ "- " + $0.1}.map{indent + $0}.joined(separator: "\n")
case .dictionary:
guard !pairs.isEmpty else { return "{}"}
result = pairs.sorted{ $0.0.description < $1.0.description }.map{
let k = $0.0.string ?? $0.0.description
let q = k.rangeOfCharacter(from: .newlines) != nil
return (q ? k.debugDescription : k) + ": " + $0.1
}.map{indent + $0}.joined(separator: "\n")
default:
break // never reaches here
}
return "\n" + result
},visit:{
if $0.isNil { return "~" }
if let s = $0.string {
return s.rangeOfCharacter(from: .newlines) == nil ? s : s.debugDescription
}
return $0.description
})
}
}Like JSONEncoder and JSONDecoder, SIONEncoder and SIONDecoder bridge your own Codable types and SION.
struct Person : Codable, Equatable {
let name:String
let birthday:Date
let avatar:Data
let tags:[String]
}
let dan = Person(
name: "dankogai",
birthday: Date(timeIntervalSince1970: 0x1p30),
avatar: Data([0xde, 0xad, 0xbe, 0xef]),
tags: ["swift", "perl"]
)
let sion = try SIONEncoder().encode(dan) // -> SION
let text = try SIONEncoder().encode(toString:dan, space:2) // -> String
let back = try SIONDecoder().decode(Person.self, from:text) // == danBecause SION is more expressive than JSON, the encoder needs no strategies:
DateandDataencode natively as.Dateand.Data— nodateEncodingStrategyordataEncodingStrategycounterparts, no doubles or base64 strings.- A
SION-typed property embeds as-is, so typed and free-form data mix freely. Int-keyed dictionaries keep nativeIntkeys instead of being stringified.- Non-finite doubles (
infinity,nan) just work, whereJSONEncoderthrows. niloptionals are omitted from output, asJSONEncoderdoes.
SIONDecoder.decode accepts a SION tree, SION text in String, or utf8-encoded Data:
struct Server : Codable { let host:String; let port:Int; let tls:Bool }
let server = try SIONDecoder().decode(Server.self, from:"""
[
"host" : "github.com", // comments welcome
"port" : 443,
"tls" : true,
]
""")For interoperability with trees that came from JSON — say via SION(json:) — the decoder is lenient where JSON is less expressive: Date also accepts a number (timeIntervalSince1970) and Data also accepts a base64 String.
Failures throw the standard EncodingError / DecodingError with full coding paths, so error handling works exactly as it does with JSONEncoder and JSONDecoder.
SIONisEquatableso you can check if two JSONs are the same.
SION(string:foo) == SION(jsonUrlString:"https://example.com/whereever")-
SIONisHashableso you can use it as a dictionary key. -
SIONisExpressibleBy*Literal. That's why you can initialize w/variable:JSONconstruct show above. -
SIONisCustomStringConvertiblewhose.descriptionis always a valid SION. -
SIONisCodable. AndSIONEncoder/SIONDecoderabove make yourCodabletypes SION-{en,de}codable. -
SIONisSequence. But when you iterate, be careful with the key.
let sa:SION = [nil, true, 1, "one", [1], ["one":1]]
// wrong!
for v in ja {
//
}// right!
for (i, v) in sa {
// i is NOT an Integer but SION
// To access its value, say i.int!
}let sd:SION = [
"null":nil, "bool":false, "number":0, "string":"",
"array":[], "object":[:]
]
for (k, v) in sd {
// k is NOT an Integer but SION.
// To access its value, say k.string!
}That is because swift demands to return same Element type. If you feel this counterintuitive, you can simply use .array or .dictionary:
for v in sa.array! {
// ...
}for (k, v) in dictionary! {
// ...
}Once inited, SION never fails. That is, it never becomes nil. Instead of being failable or throwing exceptions, SION has a special value .Error(.ErrorType) which propagates across the method invocations. The following code examines the error should it happen.
if let e = sion.error {
debugPrint(e.type)
if let nsError = e.nsError {
// do anything with nsError
}
}