commit 61a049ea3a7c419e4dff5e5b872aeda47c5c0f75 Author: sbyrne Date: Fri Jun 4 22:53:21 2021 -0400 init diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ec10551 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +target/ +.idea/ +*.iml diff --git a/README.md b/README.md new file mode 100644 index 0000000..7c93260 --- /dev/null +++ b/README.md @@ -0,0 +1,201 @@ +# Fig + +Fig is a human-readable data interchange format. + +Fig must be UTF-8. All valid UTF-8 is valid Fig. + +I am pretty sure Fig was an acronym, but I forgot what it stood for. + +I invented Fig in a hostel in Copenhagen during KotlinConf 2019. +I was jetlagged and consuming a bunch of coffee and Carlsberg, +which may explain why I forgot what it stands for and why it is so weird. + +## The format + +### Comments + +Comments are in angle brackets. + +``` + +``` + +You cannot have a > in a comment. Sorry. + +### Data types + +Fig has X data types: + +* null +* boolean +* number +* string +* list +* map + +### Nulls + +`null` is null. + +### Booleans + +`true` and `false` + +### Numbers + +Numbers are made of an optional `+` or `-`, +followed by one or more digits (`0` - `9`), +optionally followed by `.` and one or more digits, +optionally followed by `E` then optionally a `+` or `-` then one or more digits. + +There should probably be a way to indicate inifinity, negative infinity, and NaN. + +### Strings + +A sequence of non-whitespace characters or +a sequence of any characters quoted with `"`. + +`\` escapes characters. + +``` +"this has a double quote in it -> \" <- right there. and a backslash here:\\" +``` + +``` +a"b" < <-- that is 2 strings because the " starts a new one> +``` + +The value here is `"a`, not `a` because there is no trailing `"`. + +``` +"a +``` + +### Lists + +A list is a sequence of values in square brackets. + +``` +[ + "this is a list" + "of two strings and an integer" 9 +] +``` + +The closing `]` may be omitted at the end of the file. + +``` +[ this is a list +``` + +### Maps + +A map is a sequence of keys and values. + +Maps are enclosed in `{` and `}`. +Key/value pairs are separated by whitespace. +Keys and values are separated by `:`, with optionaly whitespace before and after the `:`. +A key with no `:` after it has a null value. +Keys are nullable strings. + +``` +{ + a:5 + b:"hello world" + :"this value has a null key" + c:[a list value in a map] + d:{a:map in:"a map"} + e: + f +} +``` + +The closing `}` may be omitted at the end of the file. + +This is valid: + +``` +{ this:is a:map with:[a list +``` + +### Map names + +Maps can have names. This is very useful to identify a type for deserializing. + +A named map has a `%` immediately after the `{`, followed by the name, +which is a sequence of non-whitespace characters. + +``` +[ + {%star + name:Sun + mass:1.9885E30 + location:"in the middle" + } + {%planet + name:Pluto + mass:1.303E22 + location:"way out there" + } + {%coment + name:"Halley's Comet" + mass:2.2E14 + location:"the central part of town" + } +] +``` + +### Whitespace + +The following UTF-8 codepoints are considered whitespace: + +* 0009 - CHARACTER TABULATION (\t) +* 000A - LINE FEED (\n) +* 000B - LINE TABULATION +* 000C - FORM FEED +* 000D - CARRIAGE RETURN (\r) +* 001C - INFORMATION SEPARATOR FOUR +* 001D - INFORMATION SEPARATOR THREE +* 001E - INFORMATION SEPARATOR TWO +* 001F - INFORMATION SEPARATOR ONE +* 0020 - SPACE +* 00A0 - NO-BREAK SPACE +* 1680 - OGHAM SPACE MARK +* 2000 - EN QUAD +* 2001 - EM QUAD +* 2002 - EN SPACE +* 2003 - EM SPACE +* 2004 - THREE-PER-EM SPACE +* 2005 - FOUR-PER-EM SPACE +* 2006 - SIX-PER-EM SPACE +* 2007 - FIGURE SPACE +* 2008 - PUNCTUATION SPACE +* 2009 - THIN SPACE +* 200A - HAIR SPACE +* 2028 - LINE SEPARATOR +* 2029 - PARAGRAPH SEPARATOR +* 202F - NARROW NO-BREAK SPACE +* 205F - MEDIUM MATHEMATICAL SPACE +* 3000 - IDEOGRAPHIC SPACE + +### Fig File + +A Fig file is a list or a map. If the content is not explicitly a list +(by starting with optional whitespace/comments and then a `[`) +or explicitly a map +(by starting with optional whitespace/comments and then a `{`) +then it is a list +(without a trailing `]`) + +``` +this is a list of 7 values +``` + +## Everything is valid? + +Yup. This is where it gets weird. And where the parser probably gets slow on large files. + +One advantage of making everything valid is that nobody can take advantage of undefined or +illegal syntax to make an extension of Fig. + +This can almost certainly be used as evidence to either my genius or my insanity. \ No newline at end of file diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..3fbb277 --- /dev/null +++ b/pom.xml @@ -0,0 +1,180 @@ + + + 4.0.0 + + net.sbyrne + fig + 1.0-SNAPSHOT + + ${project.groupId}:${project.artifactId} + Fig file format + https://gitlab.com/sbyrne/fig + + + + Stephen Byrne + gitlab@sbyrne.net + Stephen Byrne + https://sbyrne.net + + + + + + Apache License, Version 2.0 + https://www.apache.org/licenses/LICENSE-2.0.txt + + + + + 1.8 + 1.8 + 1.4.32 + 1.8 + UTF-8 + + + + + + org.jetbrains.kotlin + kotlin-maven-plugin + ${kotlin.version} + + + compile + compile + + compile + + + + ${project.basedir}/src/main/java + + + + + test-compile + test-compile + + test-compile + + + + ${project.basedir}/src/test/java + + + + + + + org.jetbrains.dokka + dokka-maven-plugin + ${kotlin.version} + + + prepare-package + + dokka + javadoc + javadocJar + + + + + + org.apache.maven.plugins + maven-source-plugin + 3.2.1 + + + attach-sources + + jar + + + + + + org.apache.maven.plugins + maven-surefire-plugin + 2.22.2 + + false + + + + org.apache.maven.plugins + maven-enforcer-plugin + 1.4.1 + + + enforce-no-snapshots + + enforce + + + + + Cannot have snapshot dependencies of a release! + true + true + true + + + true + + + + + + + + + + org.jetbrains.kotlin + kotlin-stdlib-jdk8 + ${kotlin.version} + + + + org.jetbrains.kotlin + kotlin-reflect + ${kotlin.version} + + + + org.jetbrains.kotlin + kotlin-test-junit + ${kotlin.version} + test + + + + org.parboiled + parboiled-core + 1.3.1 + + + + org.parboiled + parboiled-java + 1.3.1 + + + + + com.fasterxml.jackson.core + jackson-databind + 2.10.1 + + + com.fasterxml.jackson.module + jackson-module-kotlin + 2.10.1 + test + + + + + diff --git a/src/main/java/net/sbyrne/fig/FigModel.kt b/src/main/java/net/sbyrne/fig/FigModel.kt new file mode 100644 index 0000000..78457cb --- /dev/null +++ b/src/main/java/net/sbyrne/fig/FigModel.kt @@ -0,0 +1,22 @@ +package net.sbyrne.fig + +import java.math.BigDecimal + +sealed class FigValue {} + +data class FigList( val list:MutableList = mutableListOf() ): FigValue() + +data class FigMap( val name:String? = null, val map:MutableMap = mutableMapOf() ): FigValue() + +data class FigBoolean(val value:Boolean): FigValue() { + companion object { + val TRUE = FigBoolean(true) + val FALSE = FigBoolean(false) + } +} + +data class FigString( val value:String ): FigValue() + +data class FigNumber(val value:BigDecimal): FigValue() { + constructor(value:String) : this(BigDecimal(value)) +} diff --git a/src/main/java/net/sbyrne/fig/FigParser.kt b/src/main/java/net/sbyrne/fig/FigParser.kt new file mode 100644 index 0000000..a2a3eea --- /dev/null +++ b/src/main/java/net/sbyrne/fig/FigParser.kt @@ -0,0 +1,424 @@ +package net.sbyrne.fig + +import java.io.File + +import org.parboiled.BaseParser +import org.parboiled.Parboiled +import org.parboiled.Rule +import org.parboiled.annotations.BuildParseTree +import org.parboiled.errors.ErrorUtils +import org.parboiled.matchers.Matcher +import org.parboiled.matchers.TestMatcher +import org.parboiled.parserunners.TracingParseRunner +import org.parboiled.support.ParseTreeUtils + +object FigParser { + + fun parse(file: File): FigValue = parse(file.readText()) + + fun parse(text: String): FigValue { + println("====== parse <<<${text}>>>") + val parser = Parboiled.createParser(FigPegParser::class.java) + //val runner = ReportingParseRunner(parser.FigRule()) + val runner = TracingParseRunner(parser.FigRule()) + val result = runner.run(text) + if (result.hasErrors()) { + throw Exception(ErrorUtils.printParseError(result.parseErrors.first())) + } + println("-----[ ${text} ]-----") + println(ParseTreeUtils.printNodeTree(result)) + println("--------") + val value = result.valueStack.pop() as FigValue + if ( ! result.valueStack.isEmpty ) throw Exception( "Unexpected stuff on stack: ${result.valueStack.joinToString(",")}" ) + return value + } + +} + +@BuildParseTree +open class FigPegParser: BaseParser() { + + companion object { + const val WHITESPACE = + "\u0009" + // CHARACTER TABULATION (\t) + "\u000A" + // LINE FEED (\n) + "\u000B" + // LINE TABULATION + "\u000C" + // FORM FEED + "\u000D" + // CARRIAGE RETURN (\r) + "\u001C" + // INFORMATION SEPARATOR FOUR + "\u001D" + // INFORMATION SEPARATOR THREE + "\u001E" + // INFORMATION SEPARATOR TWO + "\u001F" + // INFORMATION SEPARATOR ONE + "\u0020" + // SPACE + "\u00A0" + // NO-BREAK SPACE + "\u1680" + // OGHAM SPACE MARK + "\u2000" + // EN QUAD + "\u2001" + // EM QUAD + "\u2002" + // EN SPACE + "\u2003" + // EM SPACE + "\u2004" + // THREE-PER-EM SPACE + "\u2005" + // FOUR-PER-EM SPACE + "\u2006" + // SIX-PER-EM SPACE + "\u2007" + // FIGURE SPACE + "\u2008" + // PUNCTUATION SPACE + "\u2009" + // THIN SPACE + "\u200A" + // HAIR SPACE + "\u202F" + // NARROW NO-BREAK SPACE + "\u205F" + // MEDIUM MATHEMATICAL SPACE + "\u3000" + // IDEOGRAPHIC SPACE + "\u2028" + // LINE SEPARATOR 
 + "\u2029" // PARAGRAPH SEPARATOR + } + + // pushes FigList or FigMap to stack + open fun FigRule(): Rule { + return FirstOf( + Sequence( + WhitespaceRule(), + FigListRule(), // pushes FigList + WhitespaceRule(), + EOI.skipNode() + ), + Sequence( + WhitespaceRule(), + FigMapRule(), // pushes FigMap + WhitespaceRule(), + EOI.skipNode() + ), + Sequence( + WhitespaceRule(), + ImpliedFigListRule(), // pushes FigList + WhitespaceRule(), + EOI.skipNode() + ) + ) + } + + open fun WhitespaceTerminatorOrEndRule(terminators:String?): Rule { + return FirstOf( + AnyOf("${terminators?:""}${WHITESPACE}"), + CommentRule(), + EOI + ) + } + + open fun CommentRule(): Rule { + return Sequence( + "<", + ZeroOrMore( + Sequence( + TestNot( ">" ), + ANY + ) + ), + ">" + ) + } + + open fun TestWhitespaceTerminatorOrEnd(terminators:String?): Matcher { + return TestMatcher( + WhitespaceTerminatorOrEndRule(terminators) + ) + } + + open fun WhitespaceRule(): Rule { + return ZeroOrMore( + FirstOf( + AnyOf(WHITESPACE), + CommentRule() + ) + ).suppressNode() + } + + // pushes FigList + open fun FigListRule(): Rule { + return Sequence( + AnyOf("[").skipNode(), + FigListContentRule("]"), // pushes FigList + FirstOf( + AnyOf("]"), + EOI + ).suppressNode() + ).skipNode() + } + + // pushes FigList + open fun ImpliedFigListRule(): Rule { + return FigListContentRule(null) // pushes FigList + } + + // pushes FigList + open fun FigListContentRule(terminators:String?): Rule { + return Sequence( + push(FigList()), + ZeroOrMore( + Sequence( + WhitespaceRule(), + ValueRule(terminators), // pushes FigValue? + addFigListValue(), // adds FigValue? on top of stack to FigList at next position on stack + WhitespaceRule() + ).skipNode() + ) + ) + } + + open fun addFigListValue(): Boolean { + ( peek(1) as FigList ).list.add( pop() as FigValue? ) + return true + } + + // pushes FigMap + open fun FigMapRule(): Rule { + return Sequence( + String("{").skipNode(), + FirstOf( + Sequence( + FigMapIdentifierSegmentRule(), // pushes String + push(FigMap(name=pop() as String)) + ), + push(FigMap()) + ).skipNode(), + FigMapContentRule(), // adds k/v to map on stack + FirstOf( + String("}"), + EOI + ).suppressNode() + ) + } + + // pushes String + open fun FigMapIdentifierSegmentRule(): Rule { + return Sequence( + String("%").skipNode(), + FigMapIdentifierRule(), // pushes String + TestWhitespaceTerminatorOrEnd("}") + ).skipNode() + } + + // pushes String + open fun FigMapIdentifierRule(): Rule { + return Sequence( + ZeroOrMore( + Sequence( + TestNot( + WhitespaceTerminatorOrEndRule("}") + ), + ANY + ) + ), + push(match()) + ).suppressSubnodes() + } + + // adds k/v's to FigMap on stack + open fun FigMapContentRule(): Rule { + return ZeroOrMore( + Sequence( + WhitespaceRule(), + FigMapKeyValueRule(), // adds k/v to FigMap on stack + TestWhitespaceTerminatorOrEnd("}"), + WhitespaceRule() + ).skipNode() + ).skipNode() + } + + // adds k/v to FigMap on stack + open fun FigMapKeyValueRule(): Rule { + return Sequence( + FirstOf( + FigMapKeyWithValueRule(), + FigMapValueWithoutKeyRule(), + FigMapKeyWithoutValueRule() + ), + addEntryToMap() + ).skipNode() + } + + data class FigMapKey(val key:FigString?) + data class FigMapValue(val value:FigValue?) + + open fun addEntryToMap(): Boolean { + var key:FigMapKey? = null + var value:FigMapValue? = null + while ( peek() !is FigMap ) { + val obj = pop() + when ( obj ) { + is FigMapKey -> key = obj + is FigMapValue -> value = obj + else -> throw Exception("Unexpected ${obj}" ) + } + } + ( peek() as FigMap ).map[key?.key?.value] = value?.value + return true + } + + open fun FigMapKeyWithValueRule(): Rule { + return Sequence( + StringLiteralRule(":}"), + push( FigMapKey( pop() as FigString ) ), + WhitespaceRule(), + String(":").suppressNode(), + WhitespaceRule(), + Optional( + Sequence( + ValueRule("}"), + push( FigMapValue( pop() as FigValue? ) ) + ) + ).skipNode() + ) + } + + open fun FigMapValueWithoutKeyRule(): Rule { + return Sequence( + WhitespaceRule(), + String(":").suppressNode(), + WhitespaceRule(), + ValueRule("}"), + push( FigMapValue( pop() as FigValue? ) ) + ) + } + + open fun FigMapKeyWithoutValueRule(): Rule { + return Sequence( + StringLiteralRule(":}"), + push( FigMapKey( pop() as FigString ) ) + ) + } + + // pushes FigValue? to stack + open fun ValueRule(terminators:String?): Rule { + return FirstOf( + NullLiteralRule(terminators), // pushes null to stack + BooleanLiteralRule(terminators), // pushes FigBoolean to stack + NumericLiteralRule(terminators), // pushes FigNumber to stack + FigListRule(), // pushes FigList to stack + FigMapRule(), // pushes FigMap to stack + StringLiteralRule(terminators) // pushes FigString to map + ).skipNode() + } + + // pushes null + open fun NullLiteralRule(terminators:String?): Rule { + return Sequence( + String( "null" ), + TestWhitespaceTerminatorOrEnd(terminators), + push(null) + ).suppressSubnodes() + } + + // pushes FigBoolean + open fun BooleanLiteralRule(terminators:String?): Rule { + return Sequence( + FirstOf( + Sequence( + "true", + push( FigBoolean.TRUE ) + ), + Sequence( + "false", + push( FigBoolean.FALSE ) + ) + ), + TestWhitespaceTerminatorOrEnd(terminators) + ).suppressSubnodes() + } + + // pushes FigNumber + open fun NumericLiteralRule(terminators:String?): Rule { + return Sequence( + Sequence( + Optional( + AnyOf( "+-" ) + ), + OneOrMore(DigitRule()), + Optional( + Sequence( + ".", + OneOrMore(DigitRule()) + ) + ), + Optional( + "E", + Optional( + AnyOf("+-") + ), + OneOrMore( + DigitRule() + ) + ), + TestWhitespaceTerminatorOrEnd(terminators) + ), + push( FigNumber(match() ) ) + ).suppressSubnodes() + } + + open fun DigitRule(): Rule { + return AnyOf( "0123456789" ) + } + + // pushes FigString + open fun StringLiteralRule( terminators:String? ): Rule { + return FirstOf( + Sequence( + "\"", + TextOrEmptyRule(charsRequiringEscape = "\""), + push( FigString( match() ) ), + "\"" + ), + Sequence( + TextRule(charsRequiringEscape = "${WHITESPACE}${terminators?:""}"), + push( FigString( match() ) ), + TestWhitespaceTerminatorOrEnd(terminators) + ) + ).suppressSubnodes() + } + + /** + * A text string where the specified characters and backslash must be escaped with a backslash. + * The text can be empty. + * + * @param charsRequiringEscape + */ + open fun TextOrEmptyRule(charsRequiringEscape: String): Rule { + return ZeroOrMore( + FirstOf( + Sequence( + '\\', + AnyOf(charsRequiringEscape + '\\') + ), + OneOrMore( + Sequence( + TestNot( + AnyOf(charsRequiringEscape + '\\') + ), + ANY + ) + ).suppressSubnodes() + ) + ) + } + + /** + * A text string where the specified characters and backslash must be escaped with a backslash. + * The text can be empty. + * + * @param charsRequiringEscape + */ + open fun TextRule(charsRequiringEscape: String): Rule { + return OneOrMore( + FirstOf( + Sequence( + '\\', + AnyOf(charsRequiringEscape + '\\') + ), + OneOrMore( + Sequence( + TestNot(AnyOf(charsRequiringEscape + '\\')), + ANY + ) + ).suppressSubnodes() + ) + ) + } + +} diff --git a/src/main/java/net/sbyrne/fig/Jackson.kt b/src/main/java/net/sbyrne/fig/Jackson.kt new file mode 100644 index 0000000..2bd0c78 --- /dev/null +++ b/src/main/java/net/sbyrne/fig/Jackson.kt @@ -0,0 +1,31 @@ +package net.sbyrne.fig + +import com.fasterxml.jackson.databind.JsonNode +import com.fasterxml.jackson.databind.node.* + +fun FigValue?.toJsonNode(): JsonNode = + when(this) { + null -> NullNode.instance + is FigMap -> ObjectNode( + JsonNodeFactory.instance, + map + .asSequence() + .map { it.key to it.value.toJsonNode() } + .let { m -> + if ( name != null ) { + // TODO a way to configure the attribute name. Or get rid of name? + m.plus( "@type" to TextNode(name) ) + } else { + m + } + } + .toMap() + ) + is FigList -> ArrayNode( + JsonNodeFactory.instance, + list.map { it.toJsonNode() } + ) + is FigNumber -> DecimalNode(value) + is FigString -> TextNode(value) + is FigBoolean -> if ( value ) BooleanNode.TRUE else BooleanNode.FALSE + } diff --git a/src/test/java/net/sbyrne/fig/FigParserTest.kt b/src/test/java/net/sbyrne/fig/FigParserTest.kt new file mode 100644 index 0000000..3485ce4 --- /dev/null +++ b/src/test/java/net/sbyrne/fig/FigParserTest.kt @@ -0,0 +1,242 @@ +package net.sbyrne.fig + +import java.math.BigDecimal +import kotlin.test.* + +class FigParserTest { + + fun test(input:String,ref:FigValue?=null) { + val result = FigParser.parse(input) + println( result ) + if ( ref != null ) { + assertEquals(ref,result) + } + } + + @Test + fun listOfNull() = test( + "[null]", + FigList(mutableListOf(null)) + ) + + @Test + fun listOfNullSpaced() = test( + "[ null ]", + FigList(mutableListOf(null)) + ) + + @Test + fun listOfNullAndBooleans() = test( + " [null true false null] ", + FigList(mutableListOf( + null, + FigBoolean(true), + FigBoolean(false), + null + )) + ) + + @Test + fun implicitListOfNull() = test( + "null", + FigList(mutableListOf(null)) + ) + + @Test + fun implicitListofNullSpaced() = test( + " null ", + FigList(mutableListOf(null)) + ) + + @Test + fun implicitListOfNullAndBooleans() = test( + " null true false null ", + FigList(mutableListOf( + null, + FigBoolean(true), + FigBoolean(false), + null + ) ) + ) + + @Test + fun stringStartingWithNumber() = test( + "5a", + FigList(mutableListOf( + FigString("5a") + )) + ) + + @Test + fun testNumber() = test( + "1", + FigList(mutableListOf( + FigNumber(BigDecimal("1")) + )) + ) + + @Test + fun listOfNumbers() = test( + "[ 1 2.5 -3.5 -4 1E2 -1E2 -1.0E4 -2.3E-23 5E-2 5 ]", + FigList(mutableListOf( + FigNumber("1"), + FigNumber("2.5"), + FigNumber("-3.5"), + FigNumber("-4"), + FigNumber("1E2"), + FigNumber("-1E2"), + FigNumber("-1.0E4"), + FigNumber("-2.3E-23"), + FigNumber("5E-2"), + FigNumber("5") + )) + ) + + @Test + fun escapeQuotedString() = test( + """"a\ b""", + FigList(mutableListOf( + FigString("a b") + )) + ) + + @Test + fun escapeUnquotedString() = test( + """a\ b""", + FigList(mutableListOf( + FigString("a b") + )) + ) + + @Test + fun listOfLiteralValues() = test( + """[ true null abc "A B" x ]""", + FigList(mutableListOf( + FigBoolean(true), + null, + FigString("abc"), + FigString("A B"), + FigString("x"), + )) + ) + + @Test + fun listWithSublists() = test( + """ [ [ null ] abc [ "hello world" whatever ] xx] """, + FigList(mutableListOf( + FigList(mutableListOf(null)), + FigString("abc"), + FigList(mutableListOf( + FigString("hello world"), + FigString("whatever") + )), + FigString("xx") + )) + ) + + @Test + fun mapWithNullKeysAndMissingValues() = test( + """{ a:b c:d :e f }""", + FigMap(map=mutableMapOf( + "a" to FigString("b"), + "c" to FigString("d"), + null to FigString("e"), + "f" to null + )) + ) + + @Test + fun mapWithNullKeysAndNoValues() = test( + """{ a:b c:d :e f: }""", + FigMap(map= mutableMapOf( + "a" to FigString("b"), + "c" to FigString("d"), + null to FigString("e"), + "f" to null + ) ) + ) + + @Test + fun namedMap() = test( + """{%abc a:b c:d :e f: }""", + FigMap("abc", mutableMapOf( + "a" to FigString("b"), + "c" to FigString("d"), + null to FigString("e"), + "f" to null + )) + ) + + @Test + fun mapSpaces() = test( + """{%abc a : b c : d : e f : }""", + FigMap("abc", mutableMapOf( + "a" to FigString("b"), + "c" to FigString("d"), + null to FigString("e"), + "f" to null + )) + ) + + @Test + fun namedMapWithSubObjects() = test( + """{%abc a:[b c] c:{%bar x:y} :e f: }""", + FigMap("abc", mutableMapOf( + "a" to FigList(mutableListOf(FigString("b"),FigString("c"))), + "c" to FigMap("bar", mutableMapOf("x" to FigString("y"))), + null to FigString("e"), + "f" to null + ) ) + ) + + @Test + fun trailingEnds() = test( + """{%abc a:{ A:one B:two C:[ a ] } }""", + FigMap("abc",mutableMapOf( + "a" to FigMap(map= mutableMapOf( + "A" to FigString("one"), + "B" to FigString("two"), + "C" to FigList(mutableListOf( + FigString("a") + )) + )) + )) + ) + + @Test + fun missingTrailingEnds() = test( + """{%abc a:{ A:one B:two C:[ a""", + FigMap("abc",mutableMapOf( + "a" to FigMap(map= mutableMapOf( + "A" to FigString("one"), + "B" to FigString("two"), + "C" to FigList(mutableListOf( + FigString("a") + )) + )) + )) + ) + + @Test + fun implicitListStartMap() = test( + """{a:b} c""", + FigList(mutableListOf( + FigMap(map= mutableMapOf("a" to FigString("b"))), + FigString("c") + ) ) + ) + + @Test + fun implicitListStartList() = test( + """[a b] c""", + FigList(mutableListOf( + FigList(mutableListOf(FigString("a"),FigString("b"))), + FigString("c") + )) + ) + + @Test + fun comments() = test("""{%abca:{A:1 B:2 C:[ a ] } } """) + +} + diff --git a/src/test/java/net/sbyrne/fig/JacksonTest.kt b/src/test/java/net/sbyrne/fig/JacksonTest.kt new file mode 100644 index 0000000..8a920dc --- /dev/null +++ b/src/test/java/net/sbyrne/fig/JacksonTest.kt @@ -0,0 +1,92 @@ +package net.sbyrne.fig + +import com.fasterxml.jackson.annotation.JsonSubTypes +import com.fasterxml.jackson.annotation.JsonTypeInfo +import com.fasterxml.jackson.core.Version +import com.fasterxml.jackson.databind.Module +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.module.kotlin.registerKotlinModule +import org.junit.Test + +class JacksonTest { + + val objectMapper = ObjectMapper() + .registerModule(ModelModule) + .registerKotlinModule() + + @Test + fun test() { + val jsonNode = FigParser + .parse("""{ title:"my group" description:"blah blah blah" attendees:[ { @type:Robot id:1 } { @type:Person name:"John Doe" email:jdoe@example.com department:TECH } ]""") + .toJsonNode() + println( jsonNode ) + val group = objectMapper.reader().treeToValue(jsonNode,Group::class.java) + println( group ) + } + + @Test + fun testName() { + val jsonNode = FigParser + .parse("""{ title:"my group" description:"blah blah blah" attendees:[ {%Robot id:1} {%Person name:"John Doe" email:jdoe@example.com department:TECH} ]""") + .toJsonNode() + println( jsonNode ) + val group = objectMapper.reader().treeToValue(jsonNode,Group::class.java) + println( group ) + } +} + +data class Group ( + val title:String, + val description:String, + val attendees:List +) + +interface Attendee + +data class Robot( + val id:Long +) : Attendee + +data class Person( + val name:String, + val department:Department, + val email:String +) : Attendee + +enum class Department { + TECH, + RESEARCH +} + +object ModelModule: Module() { + override fun getModuleName():String = "Kotlunch Model Module" + override fun version(): Version = Version.unknownVersion() + + override fun setupModule(context:SetupContext) { + context.setMixInAnnotations( Attendee::class.java, AttendeeMixin::class.java ) + context.setMixInAnnotations( Robot::class.java, RobotMixin::class.java ) + context.setMixInAnnotations( Person::class.java, PersonMixin::class.java ) + } + +} + +@JsonTypeInfo( + use = JsonTypeInfo.Id.NAME, + include = JsonTypeInfo.As.PROPERTY, + property = "@type" +) +@JsonSubTypes( + JsonSubTypes.Type(value=Person::class,name="Person"), + JsonSubTypes.Type(value=Robot::class,name="Robot") +) +abstract class AttendeeMixin + +class RobotMixin { + val id:Long? = null +} + +class PersonMixin { + val name:String? = null + val department:Department? = null + val email:String? = null +}