init
This commit is contained in:
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
target/
|
||||
.idea/
|
||||
*.iml
|
||||
201
README.md
Normal file
201
README.md
Normal 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
180
pom.xml
Normal 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>
|
||||
22
src/main/java/net/sbyrne/fig/FigModel.kt
Normal file
22
src/main/java/net/sbyrne/fig/FigModel.kt
Normal 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))
|
||||
}
|
||||
424
src/main/java/net/sbyrne/fig/FigParser.kt
Normal file
424
src/main/java/net/sbyrne/fig/FigParser.kt
Normal 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()
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
31
src/main/java/net/sbyrne/fig/Jackson.kt
Normal file
31
src/main/java/net/sbyrne/fig/Jackson.kt
Normal 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
|
||||
}
|
||||
242
src/test/java/net/sbyrne/fig/FigParserTest.kt
Normal file
242
src/test/java/net/sbyrne/fig/FigParserTest.kt
Normal 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>""")
|
||||
|
||||
}
|
||||
|
||||
92
src/test/java/net/sbyrne/fig/JacksonTest.kt
Normal file
92
src/test/java/net/sbyrne/fig/JacksonTest.kt
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user