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

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
}