This commit is contained in:
sbyrne
2021-06-04 22:53:21 -04:00
commit 61a049ea3a
8 changed files with 1195 additions and 0 deletions

3
.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
target/
.idea/
*.iml

201
README.md Normal file
View File

@@ -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.
```
<this is a comment>
```
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: <null value>
f <implicit null value>
}
```
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 <and a comment at the end>
```
## 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.

180
pom.xml Normal file
View File

@@ -0,0 +1,180 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>net.sbyrne</groupId>
<artifactId>fig</artifactId>
<version>1.0-SNAPSHOT</version>
<name>${project.groupId}:${project.artifactId}</name>
<description>Fig file format</description>
<url>https://gitlab.com/sbyrne/fig</url>
<developers>
<developer>
<name>Stephen Byrne</name>
<email>gitlab@sbyrne.net</email>
<organization>Stephen Byrne</organization>
<organizationUrl>https://sbyrne.net</organizationUrl>
</developer>
</developers>
<licenses>
<license>
<name>Apache License, Version 2.0</name>
<url>https://www.apache.org/licenses/LICENSE-2.0.txt</url>
</license>
</licenses>
<properties>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<kotlin.version>1.4.32</kotlin.version>
<kotlin.compiler.jvmTarget>1.8</kotlin.compiler.jvmTarget>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-maven-plugin</artifactId>
<version>${kotlin.version}</version>
<executions>
<execution>
<id>compile</id>
<phase>compile</phase>
<goals>
<goal>compile</goal>
</goals>
<configuration>
<sourceDirs>
<source>${project.basedir}/src/main/java</source>
</sourceDirs>
</configuration>
</execution>
<execution>
<id>test-compile</id>
<phase>test-compile</phase>
<goals>
<goal>test-compile</goal>
</goals>
<configuration>
<sourceDirs>
<source>${project.basedir}/src/test/java</source>
</sourceDirs>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.jetbrains.dokka</groupId>
<artifactId>dokka-maven-plugin</artifactId>
<version>${kotlin.version}</version>
<executions>
<execution>
<phase>prepare-package</phase>
<goals>
<goal>dokka</goal>
<goal>javadoc</goal>
<goal>javadocJar</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-source-plugin</artifactId>
<version>3.2.1</version>
<executions>
<execution>
<id>attach-sources</id>
<goals>
<goal>jar</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.22.2</version>
<configuration>
<useSystemClassLoader>false</useSystemClassLoader>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-enforcer-plugin</artifactId>
<version>1.4.1</version>
<executions>
<execution>
<id>enforce-no-snapshots</id>
<goals>
<goal>enforce</goal>
</goals>
<configuration>
<rules>
<requireReleaseDeps>
<message>Cannot have snapshot dependencies of a release!</message>
<onlyWhenRelease>true</onlyWhenRelease>
<searchTransitive>true</searchTransitive>
<failWhenParentIsSnapshot>true</failWhenParentIsSnapshot>
</requireReleaseDeps>
</rules>
<fail>true</fail>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
<dependencies>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-stdlib-jdk8</artifactId>
<version>${kotlin.version}</version>
<!-- Apache 2.0 -->
</dependency>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-reflect</artifactId>
<version>${kotlin.version}</version>
<!-- Apache 2.0 -->
</dependency>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-test-junit</artifactId>
<version>${kotlin.version}</version>
<scope>test</scope>
<!-- Apache 2.0 -->
</dependency>
<dependency>
<groupId>org.parboiled</groupId>
<artifactId>parboiled-core</artifactId>
<version>1.3.1</version>
<!-- Apache 2.0 -->
</dependency>
<dependency>
<groupId>org.parboiled</groupId>
<artifactId>parboiled-java</artifactId>
<version>1.3.1</version>
<!-- Apache 2.0 -->
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.10.1</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.module</groupId>
<artifactId>jackson-module-kotlin</artifactId>
<version>2.10.1</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,22 @@
package net.sbyrne.fig
import java.math.BigDecimal
sealed class FigValue {}
data class FigList( val list:MutableList<FigValue?> = mutableListOf() ): FigValue()
data class FigMap( val name:String? = null, val map:MutableMap<String?,FigValue?> = 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))
}

View File

@@ -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<FigValue>(parser.FigRule())
val runner = TracingParseRunner<Any?>(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<Any>() {
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()
)
)
}
}

View File

@@ -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
}

View File

@@ -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("""<comm ment>{%abc<comment a>a:{<second comment>A:1 B:2 C:[ a ] } }<comment> <another comment>""")
}

View File

@@ -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<Attendee>
)
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
}