From b11cc76927f613659b959c5ebd1429d7cb3f26ad Mon Sep 17 00:00:00 2001 From: Greg Zoller Date: Wed, 29 May 2024 23:59:25 -0500 Subject: [PATCH] Newworld (#234) * first success * config ability added * core json write working * ugly but inTermsOf works * Added caching for trait writers * writing json is working * Rudimentary read working * fixed seq parsing * enum parse works * Classes with default values work * SelfRefs working * Maps and more work * modularize reader * tests started * New faster writer * fix benchmark table * more fixes * more fixes * more write types * More write progress * sealed traits and more * most of write finished * ZIO based parser experiments * checkpoint * checkpoint * performance jump on writes * big perf bump * another big perf bump * final write optimization * Update README.md * Update README.md * readme fidgeting * JsonConfig initial working * about a third thru write tests * more tests and fixes * basic write tests complete * NeoType writes work * writes done * First tentative steps into reading * Got rudimentary class parsing working * reworked read codegen * test of Jsoniter parse string * Massive performance boost * Another massive performance improvement * checkpoint * more perf tweaks * lastest tweaks * More write performance improvements * implement read primitives * Continue read implementations * more read cases implement * fine tuning either and option * checkpoint-lots of fixes and problems solved * Java collections and maps * Many fixes for classes * enum tests done * almost code complete * docs and fixes * lots of docs and a refactor of SJConfig * doc tweak * initial msgpack support * remove msgpack * wrapping up * coveralls tweak * tweak readme --------- Co-authored-by: Greg Zoller --- .github/workflows/ci.yml | 132 +- .github/workflows/clean.yml | 59 + .github/workflows/coveralls.yml | 59 + .github/workflows/publish.yml | 27 - .gitignore | 10 +- .scalafmt.conf | 23 +- LICENSE | 24 +- README.md | 113 +- benchmark/README.md | 116 + benchmark/ReadingPerformance.png | Bin 0 -> 69457 bytes benchmark/WritingPerformance.png | Bin 0 -> 86164 bytes benchmark/build.sbt | 64 + benchmark/project/build.properties | 1 + benchmark/project/plugins.sbt | 1 + .../src/main/scala/co.blocke/Argonaut.scala | 33 + .../src/main/scala/co.blocke/Benchmark.scala | 63 + .../src/main/scala/co.blocke/Circe.scala | 34 + .../src/main/scala/co.blocke/Fabric.scalax | 24 + .../src/main/scala/co.blocke/Jawn.scalax | 17 + .../src/main/scala/co.blocke/Jsoniter.scala | 19 + .../src/main/scala/co.blocke/PlayJson.scala | 60 + .../src/main/scala/co.blocke/Record.scala | 100 + benchmark/src/main/scala/co.blocke/Run.scala | 17 + .../src/main/scala/co.blocke/ScalaJack.scala | 29 + .../src/main/scala/co.blocke/ZIOJson.scala | 27 + build.sbt | 116 +- .../main/java/co/blocke/scalajack/DBKey.java | 12 - .../main/java/co/blocke/scalajack/Ignore.java | 13 - .../java/co/blocke/scalajack/JavaStuff.java | 11 - .../java/co/blocke/scalajack/Optional.java | 10 - .../co.blocke.scalajack/Converters.scala | 70 - .../scala/co.blocke.scalajack/Main.scalax | 37 - .../co.blocke.scalajack/Performance.scalax | 106 - .../scala/co.blocke.scalajack/SJCapture.scala | 9 - .../scala/co.blocke.scalajack/ScalaJack.scala | 20 - .../DelimitedEitherTypeAdapterFactory.scala | 46 - .../delimited/DelimitedFlavor.scala | 72 - .../DelimitedOptionTypeAdapterFactory.scala | 31 - .../delimited/DelimitedParser.scala | 221 -- .../delimited/DelimitedWriter.scala | 109 - .../co.blocke.scalajack/json/JsonFlavor.scala | 63 - .../co.blocke.scalajack/json/JsonParser.scala | 428 ---- .../co.blocke.scalajack/json/JsonWriter.scala | 188 -- .../json4s/JValueBuilder.scala | 22 - .../json4s/Json4sFlavor.scala | 68 - .../json4s/Json4sParser.scala | 201 -- .../json4s/Json4sWriter.scala | 138 -- .../json4s/StringWrapTypeAdapter.scala | 75 - .../model/ClassFieldMember.scala | 20 - .../model/HintValueModifier.scala | 38 - .../model/JackFlavor.scala | 115 - .../model/LazyTypeAdapter.scala | 38 - .../co.blocke.scalajack/model/Parser.scala | 50 - .../model/StringBuilder.scala | 18 - .../model/TypeAdapaterCache.scala | 140 -- .../model/TypeAdapter.scala | 72 - .../model/TypeAdapterFactory.scala | 8 - .../model/ViewSplice.scala | 84 - .../co.blocke.scalajack/model/Writer.scala | 34 - .../typeadapter/AnyTypeAdapter.scala | 101 - .../typeadapter/ArrayTypeAdatper.scala | 71 - .../typeadapter/CollectionTypeAdapter.scala | 191 -- .../typeadapter/EitherTypeAdapter.scala | 78 - .../typeadapter/EnumTypeAdapter.scala | 178 -- .../typeadapter/FallbackTypeAdapter.scala | 30 - .../typeadapter/IntersectionTypeAdapter.scala | 55 - .../typeadapter/JavaPrimitives.scala | 237 -- .../MaybeStringWrapTypeAadapter.scala | 49 - .../typeadapter/OptionTypeAdapter.scala | 118 - .../PermissiveJavaPrimitives.scala | 191 -- .../PermissiveScalaPrimitives.scala | 169 -- .../typeadapter/ScalaPrimitives.scala | 236 -- .../typeadapter/SealedTraitTypeAdapter.scala | 118 - .../typeadapter/StringWrapTypeAdapter.scala | 44 - .../typeadapter/TimePrimitives.scala | 323 --- .../typeadapter/TraitTypeAdapter.scala | 75 - .../typeadapter/TryTypeAdapter.scala | 48 - .../typeadapter/TupleTypeAdapter.scala | 54 - .../typeadapter/UUIDTypeAdapter.scala | 44 - .../typeadapter/UnionTypeAdapter.scala | 67 - .../typeadapter/ValueClassTypeAdapter.scala | 43 - .../classes/CaseClassTypeAdapter.scala | 41 - .../classes/ClassTypeAdapterBase.scala | 25 - .../classes/JavaClassTypeAdapter.scala | 116 - .../classes/NonCaseClassTypeAdapter.scala | 52 - .../classes/ScalaClassTypeAdapter.scala | 161 -- .../ScalaClassTypeAdapterFactory.scala | 116 - .../collection/JavaMapLikeTypeAdapter.scala | 73 - .../collection/JavaSeqLikeTypeAdapter.scala | 57 - .../collection/JavaStackTypeAdapter.scala | 59 - .../collection/MapLikeTypeAdapter.scala | 70 - .../collection/SeqLikeTypeAdapter.scala | 53 - .../util/BijectiveFunction.scala | 74 - .../co.blocke.scalajack/util/FixFloat.scala | 11 - .../yaml/YamlBuilder.scala | 20 - .../co.blocke.scalajack/yaml/YamlFlavor.scala | 77 - .../co.blocke.scalajack/yaml/YamlParser.scala | 278 --- .../co.blocke.scalajack/yaml/YamlWriter.scala | 177 -- .../java/co/blocke/scalajack/JavaArray.java | 53 - .../java/co/blocke/scalajack/JavaCap.java | 12 - .../java/co/blocke/scalajack/JavaEnum.java | 9 - .../co/blocke/scalajack/JavaSimpleBase.java | 22 - .../co/blocke/scalajack/JavaSimpleChild.java | 4 - .../test/java/co/blocke/scalajack/Maybe.java | 15 - .../test/java/co/blocke/scalajack/Maybe2.java | 15 - .../java/co/blocke/scalajack/OnSetter.java | 11 - .../java/co/blocke/scalajack/Temperature.java | 6 - .../java/co/blocke/scalajack/Unsupported.java | 5 - .../co.blocke.scalajack/ConverterSpec.scala | 173 -- .../co.blocke.scalajack/JsonMatcher.scala | 21 - .../delimited/DelimitedSpec.scala | 374 --- .../co.blocke.scalajack/delimited/Model.scala | 36 - .../json/collections/AnyCollections.scala | 66 - .../json/collections/Arrays.scala | 244 -- .../json/collections/Maps.scala | 342 --- .../json/collections/Model.scala | 105 - .../json/collections/Options.scala | 435 ---- .../json/collections/Seqs.scala | 272 -- .../json/collections/Tuples.scala | 52 - .../json/custom/CustomAdapter.scala | 20 - .../json/custom/CustomTypeHints.scala | 134 - .../json/custom/Model.scala | 59 - .../json/custom/ParseOrElse.scala | 19 - .../json/mapkeys/ClassPrimKeys.scala | 319 --- .../json/mapkeys/JavaPrimKeys.scala | 500 ---- .../json/mapkeys/ListCollKeys.scala | 125 - .../json/mapkeys/MapCollKeys.scala | 166 -- .../json/mapkeys/Model.scala | 176 -- .../json/mapkeys/ScalaPrimKeys.scala | 248 -- .../json/mapkeys/TupleCollKeys.scala | 236 -- .../json/mapkeys/ValueClassKeys.scala | 454 ---- .../json/parameters/ClassParams.scala | 103 - .../json/parameters/Model.scala | 60 - .../json/parameters/TraitParams.scala | 143 -- .../json/plainclass/Inheritance.scala | 141 -- .../json/plainclass/Misc.scala | 81 - .../json/plainclass/Model.scala | 132 - .../json/plainclass/TryAndCapture.scala | 80 - .../json/plainclass/ValueClass.scala | 31 - .../json/primitives/AnyPrim.scala | 159 -- .../json/primitives/Enums.scala | 137 - .../json/primitives/JavaPrim.scala | 326 --- .../json/primitives/Model.scala | 197 -- .../primitives/PermissivePrimitives.scala | 233 -- .../json/primitives/ScalaPrim.scala | 334 --- .../json/primitives/TimePrim.scala | 299 --- .../json/primitives/ValueClassPrim.scala | 179 -- .../json/structures/Eithers.scala | 96 - .../json/structures/Model.scala | 55 - .../json/structures/SealedTraits.scala | 104 - .../json/structures/TryAndCapture.scala | 60 - .../json/structures/TypeMembers.scala | 92 - .../structures/UnionsAndIntersections.scala | 111 - .../co.blocke.scalajack/json4s/AnyColl.scala | 77 - .../co.blocke.scalajack/json4s/Custom.scala | 90 - .../json4s/Json4sSpec.scala | 238 -- .../co.blocke.scalajack/json4s/Model.scala | 118 - .../co.blocke.scalajack/json4s/Parsing.scala | 103 - .../json4s/ScalaPrim.scala | 154 -- .../yaml/collections/AnyColl.scala | 115 - .../yaml/mapkeys/ClassPrimKeys.scala | 271 -- .../yaml/mapkeys/JavaPrimKeys.scala | 357 --- .../yaml/mapkeys/ListCollKeys.scala | 212 -- .../yaml/mapkeys/MapCollKeys.scala | 311 --- .../yaml/mapkeys/Model.scala | 175 -- .../yaml/mapkeys/ScalaPrimKeys.scala | 308 --- .../yaml/mapkeys/TupleCollKeys.scala | 450 ---- .../yaml/mapkeys/ValueClassKeys.scala | 569 ----- .../yaml/misc/ReadWriterSpec.scala | 95 - .../yaml/misc/TypeMembers.scala | 164 -- .../yaml/misc/YamlFlavorSpec.scala | 89 - .../yaml/parameters/ClassParams.scala | 170 -- .../yaml/parameters/Model.scala | 57 - .../yaml/parameters/TraitParams.scala | 161 -- .../yaml/primitives.plain/Misc.scala | 84 - .../yaml/primitives.plain/Model.scala | 114 - .../yaml/primitives.plain/TryAndCapture.scala | 119 - .../primitives.plain/ValueClassPrim.scala | 34 - .../yaml/primitives/AnyPrim.scala | 163 -- .../yaml/primitives/JavaPrim.scala | 400 --- .../yaml/primitives/ScalaPrim.scala | 424 ---- .../yaml/primitives/TimePrim.scala | 321 --- .../yaml/primitives/ValueClassPrim.scala | 161 -- doc/.DS_Store | Bin 0 -> 6148 bytes doc/any.md | 60 +- doc/classesAndTraits.md | 85 +- doc/config.md | 108 - doc/custom.md | 99 - doc/delimited.md | 44 - doc/dynamo.md | 41 - doc/externalTypes.md | 69 - doc/filter.md | 30 - doc/json4s.md | 10 - doc/map.md | 86 - doc/mapname.md | 36 +- doc/mongo.md | 47 - doc/neotype.md | 69 + doc/noncase.md | 60 +- doc/nullAndNone.md | 51 +- doc/parameterized.md | 46 +- doc/parseOrElse.md | 63 - doc/tryAndCapture.md | 74 - doc/typeHint.md | 97 +- doc/union.md | 34 +- doc/valueClass.md | 22 +- doc/viewSplice.md | 38 - doc/yaml.md | 21 - .../dynamodb/DynamoFlavor.scala | 139 -- .../scala/co.blocke.scalajack/TestUtil.scala | 42 - .../co.blocke.scalajack/dynamodb/Basics.scala | 145 -- .../dynamodb/CreateTableRequest.scala | 41 - .../dynamodb/JsonDiff.scala | 58 - .../dynamodb/JsonMatcher.scala | 21 - .../co.blocke.scalajack/dynamodb/Model.scala | 110 - .../co.blocke.scalajack/dynamodb/Plain.scala | 72 - .../co.blocke.scalajack/dynamodb/README.md | 10 - .../mongo/BsonBuilder.scala | 22 - .../mongo/BsonParser.scala | 216 -- .../mongo/Converters.scala | 37 - .../mongo/MongoFlavor.scala | 79 - .../mongo/MongoWriter.scala | 169 -- .../typeadapter/ObjectIdTypeAdapter.scala | 28 - .../OffsetDateTimeTypeAdapter.scala | 35 - .../typeadapter/StringWrapTypeAdapter.scala | 69 - .../ZonedDateTimeTypeAdapter.scala | 38 - .../scala/co.blocke.scalajack/JsonDiff.scala | 58 - .../co.blocke.scalajack/JsonMatcher.scala | 21 - .../scala/co.blocke.scalajack/TestUtil.scala | 42 - .../co.blocke.scalajack/mongo/AnySpec.scala | 209 -- .../mongo/ConverterSpec.scala | 134 - .../co.blocke.scalajack/mongo/Custom.scala | 159 -- .../mongo/LooseChange.scala | 376 --- .../co.blocke.scalajack/mongo/MapKeys.scala | 96 - .../co.blocke.scalajack/mongo/Model.scala | 272 -- .../co.blocke.scalajack/mongo/MongoSpec.scala | 891 ------- .../mongo/TryAndCapture.scala | 57 - .../co.blocke.scalajack/mongo/TupleSpec.scala | 81 - project/build.properties | 2 +- project/metals.sbt | 8 + project/plugins.sbt | 4 +- .../main/java/co/blocke/scalajack/Change.java | 0 .../java/co/blocke/scalajack/TypeHint.java | 4 +- .../json/schema/additionalProperties.java | 9 + .../scalajack/json/schema/description.java | 9 + .../json/schema/exclusiveMaximum.java | 9 + .../json/schema/exclusiveMinimum.java | 9 + .../blocke/scalajack/json/schema/format.java | 9 + .../co/blocke/scalajack/json/schema/id.java | 9 + .../blocke/scalajack/json/schema/items.java | 9 + .../scalajack/json/schema/maxItems.java | 9 + .../scalajack/json/schema/maxLength.java | 9 + .../blocke/scalajack/json/schema/maximum.java | 9 + .../scalajack/json/schema/minItems.java | 9 + .../scalajack/json/schema/minLength.java | 9 + .../blocke/scalajack/json/schema/minimum.java | 9 + .../scalajack/json/schema/multipleOf.java | 9 + .../blocke/scalajack/json/schema/pattern.java | 9 + .../blocke/scalajack/json/schema/title.java | 9 + .../scalajack/json/schema/uniqueItems.java | 9 + .../scalajack/util/ByteArrayAccess.java | 50 + .../scala/co.blocke.scalajack/SJConfig.scala | 241 ++ .../scala/co.blocke.scalajack/ScalaJack.scala | 39 + .../internal/CodePrinter.scala | 34 + .../internal/Numbers.scala | 840 +++++++ .../json/FastStringBuilder.scala | 94 + .../co.blocke.scalajack/json/JsonCodec.scala | 10 + .../json/JsonCodecMaker.scala | 2200 +++++++++++++++++ .../co.blocke.scalajack/json/JsonError.scala | 30 + .../json/StringMatrix.scala | 84 + .../co.blocke.scalajack/json/package.scala | 20 + .../json/reading/JsonSource.scala | 451 ++++ .../json/writing/AnyWriter.scala | 157 ++ .../json/writing/JsonOutput.scala | 528 ++++ .../scala/co.blocke.scalajack/package.scala | 25 + .../scala/co.blocke.scalajack/run/Play.scalax | 15 + .../co.blocke.scalajack/run/Record.scalax | 185 ++ .../co.blocke.scalajack/run/Sample.scalax | 47 + .../co/blocke/scalajack/json/SampleClass.java | 47 + .../scalajack/json/collections/CarEnum.java | 7 + .../scala/co.blocke.scalajack/JsonDiff.scala | 15 +- .../co.blocke.scalajack/JsonMatcher.scala | 28 + .../scala/co.blocke.scalajack/TestUtil.scala | 23 +- .../json/classes/ClassSpec.scala | 172 ++ .../json/classes/Model.scala | 90 + .../json/classes/Model2.scala | 5 + .../json/classes/TraitSpec.scala | 72 + .../json/collections/JavaCollSpec.scala | 275 +++ .../json/collections/JavaMapSpec.scala | 229 ++ .../json/collections/MapSpec.scala | 288 +++ .../json/collections/Model.scala | 46 + .../json/collections/SeqSetArraySpec.scala | 268 ++ .../json/collections/TupleSpec.scala | 68 + .../json/misc/AliasSpec.scala | 59 + .../json/misc/EnumSpec.scala | 113 + .../json/misc/LRSpec.scala | 143 ++ .../json/misc/MiscSpec.scala | 92 + .../co.blocke.scalajack/json/misc/Model.scala | 138 ++ .../json/misc/OptionSpec.scala | 187 ++ .../json/misc/TrySpec.scala | 96 + .../json/primitives/JavaPrimSpec.scala | 288 +++ .../json}/primitives/Model.scala | 90 +- .../json/primitives/ScalaPrimSpec.scala | 336 +++ .../json/primitives/SimpleSpec.scala | 377 +++ 303 files changed, 9979 insertions(+), 25511 deletions(-) create mode 100644 .github/workflows/clean.yml create mode 100644 .github/workflows/coveralls.yml delete mode 100644 .github/workflows/publish.yml create mode 100644 benchmark/README.md create mode 100644 benchmark/ReadingPerformance.png create mode 100644 benchmark/WritingPerformance.png create mode 100644 benchmark/build.sbt create mode 100644 benchmark/project/build.properties create mode 100644 benchmark/project/plugins.sbt create mode 100644 benchmark/src/main/scala/co.blocke/Argonaut.scala create mode 100644 benchmark/src/main/scala/co.blocke/Benchmark.scala create mode 100644 benchmark/src/main/scala/co.blocke/Circe.scala create mode 100644 benchmark/src/main/scala/co.blocke/Fabric.scalax create mode 100644 benchmark/src/main/scala/co.blocke/Jawn.scalax create mode 100644 benchmark/src/main/scala/co.blocke/Jsoniter.scala create mode 100644 benchmark/src/main/scala/co.blocke/PlayJson.scala create mode 100644 benchmark/src/main/scala/co.blocke/Record.scala create mode 100644 benchmark/src/main/scala/co.blocke/Run.scala create mode 100644 benchmark/src/main/scala/co.blocke/ScalaJack.scala create mode 100644 benchmark/src/main/scala/co.blocke/ZIOJson.scala delete mode 100644 core/src/main/java/co/blocke/scalajack/DBKey.java delete mode 100644 core/src/main/java/co/blocke/scalajack/Ignore.java delete mode 100644 core/src/main/java/co/blocke/scalajack/JavaStuff.java delete mode 100644 core/src/main/java/co/blocke/scalajack/Optional.java delete mode 100644 core/src/main/scala/co.blocke.scalajack/Converters.scala delete mode 100644 core/src/main/scala/co.blocke.scalajack/Main.scalax delete mode 100644 core/src/main/scala/co.blocke.scalajack/Performance.scalax delete mode 100644 core/src/main/scala/co.blocke.scalajack/SJCapture.scala delete mode 100644 core/src/main/scala/co.blocke.scalajack/ScalaJack.scala delete mode 100644 core/src/main/scala/co.blocke.scalajack/delimited/DelimitedEitherTypeAdapterFactory.scala delete mode 100644 core/src/main/scala/co.blocke.scalajack/delimited/DelimitedFlavor.scala delete mode 100644 core/src/main/scala/co.blocke.scalajack/delimited/DelimitedOptionTypeAdapterFactory.scala delete mode 100644 core/src/main/scala/co.blocke.scalajack/delimited/DelimitedParser.scala delete mode 100644 core/src/main/scala/co.blocke.scalajack/delimited/DelimitedWriter.scala delete mode 100644 core/src/main/scala/co.blocke.scalajack/json/JsonFlavor.scala delete mode 100644 core/src/main/scala/co.blocke.scalajack/json/JsonParser.scala delete mode 100644 core/src/main/scala/co.blocke.scalajack/json/JsonWriter.scala delete mode 100644 core/src/main/scala/co.blocke.scalajack/json4s/JValueBuilder.scala delete mode 100644 core/src/main/scala/co.blocke.scalajack/json4s/Json4sFlavor.scala delete mode 100644 core/src/main/scala/co.blocke.scalajack/json4s/Json4sParser.scala delete mode 100644 core/src/main/scala/co.blocke.scalajack/json4s/Json4sWriter.scala delete mode 100644 core/src/main/scala/co.blocke.scalajack/json4s/StringWrapTypeAdapter.scala delete mode 100644 core/src/main/scala/co.blocke.scalajack/model/ClassFieldMember.scala delete mode 100644 core/src/main/scala/co.blocke.scalajack/model/HintValueModifier.scala delete mode 100644 core/src/main/scala/co.blocke.scalajack/model/JackFlavor.scala delete mode 100644 core/src/main/scala/co.blocke.scalajack/model/LazyTypeAdapter.scala delete mode 100644 core/src/main/scala/co.blocke.scalajack/model/Parser.scala delete mode 100644 core/src/main/scala/co.blocke.scalajack/model/StringBuilder.scala delete mode 100644 core/src/main/scala/co.blocke.scalajack/model/TypeAdapaterCache.scala delete mode 100644 core/src/main/scala/co.blocke.scalajack/model/TypeAdapter.scala delete mode 100644 core/src/main/scala/co.blocke.scalajack/model/TypeAdapterFactory.scala delete mode 100644 core/src/main/scala/co.blocke.scalajack/model/ViewSplice.scala delete mode 100644 core/src/main/scala/co.blocke.scalajack/model/Writer.scala delete mode 100644 core/src/main/scala/co.blocke.scalajack/typeadapter/AnyTypeAdapter.scala delete mode 100644 core/src/main/scala/co.blocke.scalajack/typeadapter/ArrayTypeAdatper.scala delete mode 100644 core/src/main/scala/co.blocke.scalajack/typeadapter/CollectionTypeAdapter.scala delete mode 100644 core/src/main/scala/co.blocke.scalajack/typeadapter/EitherTypeAdapter.scala delete mode 100644 core/src/main/scala/co.blocke.scalajack/typeadapter/EnumTypeAdapter.scala delete mode 100644 core/src/main/scala/co.blocke.scalajack/typeadapter/FallbackTypeAdapter.scala delete mode 100644 core/src/main/scala/co.blocke.scalajack/typeadapter/IntersectionTypeAdapter.scala delete mode 100644 core/src/main/scala/co.blocke.scalajack/typeadapter/JavaPrimitives.scala delete mode 100644 core/src/main/scala/co.blocke.scalajack/typeadapter/MaybeStringWrapTypeAadapter.scala delete mode 100644 core/src/main/scala/co.blocke.scalajack/typeadapter/OptionTypeAdapter.scala delete mode 100644 core/src/main/scala/co.blocke.scalajack/typeadapter/PermissiveJavaPrimitives.scala delete mode 100644 core/src/main/scala/co.blocke.scalajack/typeadapter/PermissiveScalaPrimitives.scala delete mode 100644 core/src/main/scala/co.blocke.scalajack/typeadapter/ScalaPrimitives.scala delete mode 100644 core/src/main/scala/co.blocke.scalajack/typeadapter/SealedTraitTypeAdapter.scala delete mode 100644 core/src/main/scala/co.blocke.scalajack/typeadapter/StringWrapTypeAdapter.scala delete mode 100644 core/src/main/scala/co.blocke.scalajack/typeadapter/TimePrimitives.scala delete mode 100644 core/src/main/scala/co.blocke.scalajack/typeadapter/TraitTypeAdapter.scala delete mode 100644 core/src/main/scala/co.blocke.scalajack/typeadapter/TryTypeAdapter.scala delete mode 100644 core/src/main/scala/co.blocke.scalajack/typeadapter/TupleTypeAdapter.scala delete mode 100644 core/src/main/scala/co.blocke.scalajack/typeadapter/UUIDTypeAdapter.scala delete mode 100644 core/src/main/scala/co.blocke.scalajack/typeadapter/UnionTypeAdapter.scala delete mode 100644 core/src/main/scala/co.blocke.scalajack/typeadapter/ValueClassTypeAdapter.scala delete mode 100644 core/src/main/scala/co.blocke.scalajack/typeadapter/classes/CaseClassTypeAdapter.scala delete mode 100644 core/src/main/scala/co.blocke.scalajack/typeadapter/classes/ClassTypeAdapterBase.scala delete mode 100644 core/src/main/scala/co.blocke.scalajack/typeadapter/classes/JavaClassTypeAdapter.scala delete mode 100644 core/src/main/scala/co.blocke.scalajack/typeadapter/classes/NonCaseClassTypeAdapter.scala delete mode 100644 core/src/main/scala/co.blocke.scalajack/typeadapter/classes/ScalaClassTypeAdapter.scala delete mode 100644 core/src/main/scala/co.blocke.scalajack/typeadapter/classes/ScalaClassTypeAdapterFactory.scala delete mode 100644 core/src/main/scala/co.blocke.scalajack/typeadapter/collection/JavaMapLikeTypeAdapter.scala delete mode 100644 core/src/main/scala/co.blocke.scalajack/typeadapter/collection/JavaSeqLikeTypeAdapter.scala delete mode 100644 core/src/main/scala/co.blocke.scalajack/typeadapter/collection/JavaStackTypeAdapter.scala delete mode 100644 core/src/main/scala/co.blocke.scalajack/typeadapter/collection/MapLikeTypeAdapter.scala delete mode 100644 core/src/main/scala/co.blocke.scalajack/typeadapter/collection/SeqLikeTypeAdapter.scala delete mode 100644 core/src/main/scala/co.blocke.scalajack/util/BijectiveFunction.scala delete mode 100644 core/src/main/scala/co.blocke.scalajack/util/FixFloat.scala delete mode 100644 core/src/main/scala/co.blocke.scalajack/yaml/YamlBuilder.scala delete mode 100644 core/src/main/scala/co.blocke.scalajack/yaml/YamlFlavor.scala delete mode 100644 core/src/main/scala/co.blocke.scalajack/yaml/YamlParser.scala delete mode 100644 core/src/main/scala/co.blocke.scalajack/yaml/YamlWriter.scala delete mode 100644 core/src/test/java/co/blocke/scalajack/JavaArray.java delete mode 100644 core/src/test/java/co/blocke/scalajack/JavaCap.java delete mode 100644 core/src/test/java/co/blocke/scalajack/JavaEnum.java delete mode 100644 core/src/test/java/co/blocke/scalajack/JavaSimpleBase.java delete mode 100644 core/src/test/java/co/blocke/scalajack/JavaSimpleChild.java delete mode 100644 core/src/test/java/co/blocke/scalajack/Maybe.java delete mode 100644 core/src/test/java/co/blocke/scalajack/Maybe2.java delete mode 100644 core/src/test/java/co/blocke/scalajack/OnSetter.java delete mode 100644 core/src/test/java/co/blocke/scalajack/Temperature.java delete mode 100644 core/src/test/java/co/blocke/scalajack/Unsupported.java delete mode 100644 core/src/test/scala/co.blocke.scalajack/ConverterSpec.scala delete mode 100644 core/src/test/scala/co.blocke.scalajack/JsonMatcher.scala delete mode 100644 core/src/test/scala/co.blocke.scalajack/delimited/DelimitedSpec.scala delete mode 100644 core/src/test/scala/co.blocke.scalajack/delimited/Model.scala delete mode 100644 core/src/test/scala/co.blocke.scalajack/json/collections/AnyCollections.scala delete mode 100644 core/src/test/scala/co.blocke.scalajack/json/collections/Arrays.scala delete mode 100644 core/src/test/scala/co.blocke.scalajack/json/collections/Maps.scala delete mode 100644 core/src/test/scala/co.blocke.scalajack/json/collections/Model.scala delete mode 100644 core/src/test/scala/co.blocke.scalajack/json/collections/Options.scala delete mode 100644 core/src/test/scala/co.blocke.scalajack/json/collections/Seqs.scala delete mode 100644 core/src/test/scala/co.blocke.scalajack/json/collections/Tuples.scala delete mode 100644 core/src/test/scala/co.blocke.scalajack/json/custom/CustomAdapter.scala delete mode 100644 core/src/test/scala/co.blocke.scalajack/json/custom/CustomTypeHints.scala delete mode 100644 core/src/test/scala/co.blocke.scalajack/json/custom/Model.scala delete mode 100644 core/src/test/scala/co.blocke.scalajack/json/custom/ParseOrElse.scala delete mode 100644 core/src/test/scala/co.blocke.scalajack/json/mapkeys/ClassPrimKeys.scala delete mode 100644 core/src/test/scala/co.blocke.scalajack/json/mapkeys/JavaPrimKeys.scala delete mode 100644 core/src/test/scala/co.blocke.scalajack/json/mapkeys/ListCollKeys.scala delete mode 100644 core/src/test/scala/co.blocke.scalajack/json/mapkeys/MapCollKeys.scala delete mode 100644 core/src/test/scala/co.blocke.scalajack/json/mapkeys/Model.scala delete mode 100644 core/src/test/scala/co.blocke.scalajack/json/mapkeys/ScalaPrimKeys.scala delete mode 100644 core/src/test/scala/co.blocke.scalajack/json/mapkeys/TupleCollKeys.scala delete mode 100644 core/src/test/scala/co.blocke.scalajack/json/mapkeys/ValueClassKeys.scala delete mode 100644 core/src/test/scala/co.blocke.scalajack/json/parameters/ClassParams.scala delete mode 100644 core/src/test/scala/co.blocke.scalajack/json/parameters/Model.scala delete mode 100644 core/src/test/scala/co.blocke.scalajack/json/parameters/TraitParams.scala delete mode 100644 core/src/test/scala/co.blocke.scalajack/json/plainclass/Inheritance.scala delete mode 100644 core/src/test/scala/co.blocke.scalajack/json/plainclass/Misc.scala delete mode 100644 core/src/test/scala/co.blocke.scalajack/json/plainclass/Model.scala delete mode 100644 core/src/test/scala/co.blocke.scalajack/json/plainclass/TryAndCapture.scala delete mode 100644 core/src/test/scala/co.blocke.scalajack/json/plainclass/ValueClass.scala delete mode 100644 core/src/test/scala/co.blocke.scalajack/json/primitives/AnyPrim.scala delete mode 100644 core/src/test/scala/co.blocke.scalajack/json/primitives/Enums.scala delete mode 100644 core/src/test/scala/co.blocke.scalajack/json/primitives/JavaPrim.scala delete mode 100644 core/src/test/scala/co.blocke.scalajack/json/primitives/Model.scala delete mode 100644 core/src/test/scala/co.blocke.scalajack/json/primitives/PermissivePrimitives.scala delete mode 100644 core/src/test/scala/co.blocke.scalajack/json/primitives/ScalaPrim.scala delete mode 100644 core/src/test/scala/co.blocke.scalajack/json/primitives/TimePrim.scala delete mode 100644 core/src/test/scala/co.blocke.scalajack/json/primitives/ValueClassPrim.scala delete mode 100644 core/src/test/scala/co.blocke.scalajack/json/structures/Eithers.scala delete mode 100644 core/src/test/scala/co.blocke.scalajack/json/structures/Model.scala delete mode 100644 core/src/test/scala/co.blocke.scalajack/json/structures/SealedTraits.scala delete mode 100644 core/src/test/scala/co.blocke.scalajack/json/structures/TryAndCapture.scala delete mode 100644 core/src/test/scala/co.blocke.scalajack/json/structures/TypeMembers.scala delete mode 100644 core/src/test/scala/co.blocke.scalajack/json/structures/UnionsAndIntersections.scala delete mode 100644 core/src/test/scala/co.blocke.scalajack/json4s/AnyColl.scala delete mode 100644 core/src/test/scala/co.blocke.scalajack/json4s/Custom.scala delete mode 100644 core/src/test/scala/co.blocke.scalajack/json4s/Json4sSpec.scala delete mode 100644 core/src/test/scala/co.blocke.scalajack/json4s/Model.scala delete mode 100644 core/src/test/scala/co.blocke.scalajack/json4s/Parsing.scala delete mode 100644 core/src/test/scala/co.blocke.scalajack/json4s/ScalaPrim.scala delete mode 100644 core/src/test/scala/co.blocke.scalajack/yaml/collections/AnyColl.scala delete mode 100644 core/src/test/scala/co.blocke.scalajack/yaml/mapkeys/ClassPrimKeys.scala delete mode 100644 core/src/test/scala/co.blocke.scalajack/yaml/mapkeys/JavaPrimKeys.scala delete mode 100644 core/src/test/scala/co.blocke.scalajack/yaml/mapkeys/ListCollKeys.scala delete mode 100644 core/src/test/scala/co.blocke.scalajack/yaml/mapkeys/MapCollKeys.scala delete mode 100644 core/src/test/scala/co.blocke.scalajack/yaml/mapkeys/Model.scala delete mode 100644 core/src/test/scala/co.blocke.scalajack/yaml/mapkeys/ScalaPrimKeys.scala delete mode 100644 core/src/test/scala/co.blocke.scalajack/yaml/mapkeys/TupleCollKeys.scala delete mode 100644 core/src/test/scala/co.blocke.scalajack/yaml/mapkeys/ValueClassKeys.scala delete mode 100644 core/src/test/scala/co.blocke.scalajack/yaml/misc/ReadWriterSpec.scala delete mode 100644 core/src/test/scala/co.blocke.scalajack/yaml/misc/TypeMembers.scala delete mode 100644 core/src/test/scala/co.blocke.scalajack/yaml/misc/YamlFlavorSpec.scala delete mode 100644 core/src/test/scala/co.blocke.scalajack/yaml/parameters/ClassParams.scala delete mode 100644 core/src/test/scala/co.blocke.scalajack/yaml/parameters/Model.scala delete mode 100644 core/src/test/scala/co.blocke.scalajack/yaml/parameters/TraitParams.scala delete mode 100644 core/src/test/scala/co.blocke.scalajack/yaml/primitives.plain/Misc.scala delete mode 100644 core/src/test/scala/co.blocke.scalajack/yaml/primitives.plain/Model.scala delete mode 100644 core/src/test/scala/co.blocke.scalajack/yaml/primitives.plain/TryAndCapture.scala delete mode 100644 core/src/test/scala/co.blocke.scalajack/yaml/primitives.plain/ValueClassPrim.scala delete mode 100644 core/src/test/scala/co.blocke.scalajack/yaml/primitives/AnyPrim.scala delete mode 100644 core/src/test/scala/co.blocke.scalajack/yaml/primitives/JavaPrim.scala delete mode 100644 core/src/test/scala/co.blocke.scalajack/yaml/primitives/ScalaPrim.scala delete mode 100644 core/src/test/scala/co.blocke.scalajack/yaml/primitives/TimePrim.scala delete mode 100644 core/src/test/scala/co.blocke.scalajack/yaml/primitives/ValueClassPrim.scala create mode 100644 doc/.DS_Store delete mode 100644 doc/config.md delete mode 100644 doc/custom.md delete mode 100644 doc/delimited.md delete mode 100644 doc/dynamo.md delete mode 100644 doc/externalTypes.md delete mode 100644 doc/filter.md delete mode 100644 doc/json4s.md delete mode 100644 doc/map.md delete mode 100644 doc/mongo.md create mode 100644 doc/neotype.md delete mode 100644 doc/parseOrElse.md delete mode 100644 doc/tryAndCapture.md delete mode 100644 doc/viewSplice.md delete mode 100644 doc/yaml.md delete mode 100644 dynamodb/src/main/scala/co.block.scalajack/dynamodb/DynamoFlavor.scala delete mode 100644 dynamodb/src/test/scala/co.blocke.scalajack/TestUtil.scala delete mode 100644 dynamodb/src/test/scala/co.blocke.scalajack/dynamodb/Basics.scala delete mode 100644 dynamodb/src/test/scala/co.blocke.scalajack/dynamodb/CreateTableRequest.scala delete mode 100644 dynamodb/src/test/scala/co.blocke.scalajack/dynamodb/JsonDiff.scala delete mode 100644 dynamodb/src/test/scala/co.blocke.scalajack/dynamodb/JsonMatcher.scala delete mode 100644 dynamodb/src/test/scala/co.blocke.scalajack/dynamodb/Model.scala delete mode 100644 dynamodb/src/test/scala/co.blocke.scalajack/dynamodb/Plain.scala delete mode 100644 dynamodb/src/test/scala/co.blocke.scalajack/dynamodb/README.md delete mode 100644 mongo/src/main/scala/co.blocke.scalajack/mongo/BsonBuilder.scala delete mode 100644 mongo/src/main/scala/co.blocke.scalajack/mongo/BsonParser.scala delete mode 100644 mongo/src/main/scala/co.blocke.scalajack/mongo/Converters.scala delete mode 100644 mongo/src/main/scala/co.blocke.scalajack/mongo/MongoFlavor.scala delete mode 100644 mongo/src/main/scala/co.blocke.scalajack/mongo/MongoWriter.scala delete mode 100644 mongo/src/main/scala/co.blocke.scalajack/mongo/typeadapter/ObjectIdTypeAdapter.scala delete mode 100644 mongo/src/main/scala/co.blocke.scalajack/mongo/typeadapter/OffsetDateTimeTypeAdapter.scala delete mode 100644 mongo/src/main/scala/co.blocke.scalajack/mongo/typeadapter/StringWrapTypeAdapter.scala delete mode 100644 mongo/src/main/scala/co.blocke.scalajack/mongo/typeadapter/ZonedDateTimeTypeAdapter.scala delete mode 100644 mongo/src/test/scala/co.blocke.scalajack/JsonDiff.scala delete mode 100644 mongo/src/test/scala/co.blocke.scalajack/JsonMatcher.scala delete mode 100644 mongo/src/test/scala/co.blocke.scalajack/TestUtil.scala delete mode 100644 mongo/src/test/scala/co.blocke.scalajack/mongo/AnySpec.scala delete mode 100644 mongo/src/test/scala/co.blocke.scalajack/mongo/ConverterSpec.scala delete mode 100644 mongo/src/test/scala/co.blocke.scalajack/mongo/Custom.scala delete mode 100644 mongo/src/test/scala/co.blocke.scalajack/mongo/LooseChange.scala delete mode 100644 mongo/src/test/scala/co.blocke.scalajack/mongo/MapKeys.scala delete mode 100644 mongo/src/test/scala/co.blocke.scalajack/mongo/Model.scala delete mode 100644 mongo/src/test/scala/co.blocke.scalajack/mongo/MongoSpec.scala delete mode 100644 mongo/src/test/scala/co.blocke.scalajack/mongo/TryAndCapture.scala delete mode 100644 mongo/src/test/scala/co.blocke.scalajack/mongo/TupleSpec.scala create mode 100644 project/metals.sbt rename {core/src => src}/main/java/co/blocke/scalajack/Change.java (100%) rename core/src/main/java/co/blocke/scalajack/Collection.java => src/main/java/co/blocke/scalajack/TypeHint.java (71%) create mode 100644 src/main/java/co/blocke/scalajack/json/schema/additionalProperties.java create mode 100644 src/main/java/co/blocke/scalajack/json/schema/description.java create mode 100644 src/main/java/co/blocke/scalajack/json/schema/exclusiveMaximum.java create mode 100644 src/main/java/co/blocke/scalajack/json/schema/exclusiveMinimum.java create mode 100644 src/main/java/co/blocke/scalajack/json/schema/format.java create mode 100644 src/main/java/co/blocke/scalajack/json/schema/id.java create mode 100644 src/main/java/co/blocke/scalajack/json/schema/items.java create mode 100644 src/main/java/co/blocke/scalajack/json/schema/maxItems.java create mode 100644 src/main/java/co/blocke/scalajack/json/schema/maxLength.java create mode 100644 src/main/java/co/blocke/scalajack/json/schema/maximum.java create mode 100644 src/main/java/co/blocke/scalajack/json/schema/minItems.java create mode 100644 src/main/java/co/blocke/scalajack/json/schema/minLength.java create mode 100644 src/main/java/co/blocke/scalajack/json/schema/minimum.java create mode 100644 src/main/java/co/blocke/scalajack/json/schema/multipleOf.java create mode 100644 src/main/java/co/blocke/scalajack/json/schema/pattern.java create mode 100644 src/main/java/co/blocke/scalajack/json/schema/title.java create mode 100644 src/main/java/co/blocke/scalajack/json/schema/uniqueItems.java create mode 100644 src/main/java/co/blocke/scalajack/util/ByteArrayAccess.java create mode 100644 src/main/scala/co.blocke.scalajack/SJConfig.scala create mode 100644 src/main/scala/co.blocke.scalajack/ScalaJack.scala create mode 100644 src/main/scala/co.blocke.scalajack/internal/CodePrinter.scala create mode 100644 src/main/scala/co.blocke.scalajack/internal/Numbers.scala create mode 100644 src/main/scala/co.blocke.scalajack/json/FastStringBuilder.scala create mode 100644 src/main/scala/co.blocke.scalajack/json/JsonCodec.scala create mode 100644 src/main/scala/co.blocke.scalajack/json/JsonCodecMaker.scala create mode 100644 src/main/scala/co.blocke.scalajack/json/JsonError.scala create mode 100644 src/main/scala/co.blocke.scalajack/json/StringMatrix.scala create mode 100644 src/main/scala/co.blocke.scalajack/json/package.scala create mode 100644 src/main/scala/co.blocke.scalajack/json/reading/JsonSource.scala create mode 100644 src/main/scala/co.blocke.scalajack/json/writing/AnyWriter.scala create mode 100644 src/main/scala/co.blocke.scalajack/json/writing/JsonOutput.scala create mode 100644 src/main/scala/co.blocke.scalajack/package.scala create mode 100644 src/main/scala/co.blocke.scalajack/run/Play.scalax create mode 100644 src/main/scala/co.blocke.scalajack/run/Record.scalax create mode 100644 src/main/scala/co.blocke.scalajack/run/Sample.scalax create mode 100644 src/test/java/co/blocke/scalajack/json/SampleClass.java create mode 100644 src/test/java/co/blocke/scalajack/json/collections/CarEnum.java rename {core/src => src}/test/scala/co.blocke.scalajack/JsonDiff.scala (80%) create mode 100644 src/test/scala/co.blocke.scalajack/JsonMatcher.scala rename {core/src => src}/test/scala/co.blocke.scalajack/TestUtil.scala (62%) create mode 100644 src/test/scala/co.blocke.scalajack/json/classes/ClassSpec.scala create mode 100644 src/test/scala/co.blocke.scalajack/json/classes/Model.scala create mode 100644 src/test/scala/co.blocke.scalajack/json/classes/Model2.scala create mode 100644 src/test/scala/co.blocke.scalajack/json/classes/TraitSpec.scala create mode 100644 src/test/scala/co.blocke.scalajack/json/collections/JavaCollSpec.scala create mode 100644 src/test/scala/co.blocke.scalajack/json/collections/JavaMapSpec.scala create mode 100644 src/test/scala/co.blocke.scalajack/json/collections/MapSpec.scala create mode 100644 src/test/scala/co.blocke.scalajack/json/collections/Model.scala create mode 100644 src/test/scala/co.blocke.scalajack/json/collections/SeqSetArraySpec.scala create mode 100644 src/test/scala/co.blocke.scalajack/json/collections/TupleSpec.scala create mode 100644 src/test/scala/co.blocke.scalajack/json/misc/AliasSpec.scala create mode 100644 src/test/scala/co.blocke.scalajack/json/misc/EnumSpec.scala create mode 100644 src/test/scala/co.blocke.scalajack/json/misc/LRSpec.scala create mode 100644 src/test/scala/co.blocke.scalajack/json/misc/MiscSpec.scala create mode 100644 src/test/scala/co.blocke.scalajack/json/misc/Model.scala create mode 100644 src/test/scala/co.blocke.scalajack/json/misc/OptionSpec.scala create mode 100644 src/test/scala/co.blocke.scalajack/json/misc/TrySpec.scala create mode 100644 src/test/scala/co.blocke.scalajack/json/primitives/JavaPrimSpec.scala rename {core/src/test/scala/co.blocke.scalajack/yaml => src/test/scala/co.blocke.scalajack/json}/primitives/Model.scala (60%) create mode 100644 src/test/scala/co.blocke.scalajack/json/primitives/ScalaPrimSpec.scala create mode 100644 src/test/scala/co.blocke.scalajack/json/primitives/SimpleSpec.scala diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 33b94f0f..3ce76a45 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,41 +1,119 @@ -name: Package Build -# This workflow is triggered on pushes to the repository. -on: [pull_request] +# This file was automatically generated by sbt-github-actions using the +# githubWorkflowGenerate task. You should add and commit this file to +# your git repository. It goes without saying that you shouldn't edit +# this file by hand! Instead, if you wish to make changes, you should +# change your sbt build configuration to revise the workflow description +# to meet your needs, then regenerate this file. + +name: Continuous Integration + +on: + pull_request: + branches: ['**'] + push: + branches: ['**'] + tags: [v*] + +env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + +concurrency: + group: ${{ github.workflow }} @ ${{ github.ref }} + cancel-in-progress: true jobs: - build-and-test: - runs-on: ubuntu-latest + build: name: Build and Test - + strategy: + matrix: + os: [ubuntu-latest] + scala: [3.4.2] + java: [zulu@21] + runs-on: ${{ matrix.os }} + timeout-minutes: 60 steps: - - name: Ignore line ending differences in git - if: contains(runner.os, 'windows') - shell: bash - run: git config --global core.autocrlf false + - name: Checkout current branch (full) + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup Java (zulu@21) + id: setup-java-zulu-21 + if: matrix.java == 'zulu@21' + uses: actions/setup-java@v4 + with: + distribution: zulu + java-version: 21 + cache: sbt + - name: sbt update + if: matrix.java == 'zulu@21' && steps.setup-java-zulu-21.outputs.cache-hit == 'false' + run: sbt +update + + - name: Check that workflows are up to date + run: sbt githubWorkflowCheck + + - name: Build project + run: sbt '++ ${{ matrix.scala }}' test + + - name: Make target directories + if: github.event_name != 'pull_request' && (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v')) + run: mkdir -p target project/target + + - name: Compress target directories + if: github.event_name != 'pull_request' && (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v')) + run: tar cf targets.tar target project/target + + - name: Upload target directories + if: github.event_name != 'pull_request' && (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v')) + uses: actions/upload-artifact@v4 + with: + name: target-${{ matrix.os }}-${{ matrix.java }}-${{ matrix.scala }} + path: targets.tar + + publish: + name: Publish Artifacts + needs: [build] + if: github.event_name != 'pull_request' && (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v')) + strategy: + matrix: + os: [ubuntu-latest] + java: [zulu@21] + runs-on: ${{ matrix.os }} + steps: - name: Checkout current branch (full) - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: fetch-depth: 0 - - name: Setup Java (zulu@13) - uses: actions/setup-java@v3 + - name: Setup Java (zulu@21) + id: setup-java-zulu-21 + if: matrix.java == 'zulu@21' + uses: actions/setup-java@v4 with: distribution: zulu - java-version: 13 + java-version: 21 + cache: sbt - - name: Cache sbt - uses: actions/cache@v3 + - name: sbt update + if: matrix.java == 'zulu@21' && steps.setup-java-zulu-21.outputs.cache-hit == 'false' + run: sbt +update + + - name: Download target directories (3.4.2) + uses: actions/download-artifact@v4 with: - path: | - ~/.sbt - ~/.ivy2/cache - ~/.coursier/cache/v1 - ~/.cache/coursier/v1 - ~/AppData/Local/Coursier/Cache/v1 - ~/Library/Caches/Coursier/v1 - key: ${{ runner.os }}-sbt-cache-v2-${{ hashFiles('**/*.sbt') }}-${{ hashFiles('project/build.properties') }} + name: target-${{ matrix.os }}-${{ matrix.java }}-3.4.2 - - name: Build project - shell: bash - run: sbt test \ No newline at end of file + - name: Inflate target directories (3.4.2) + run: | + tar xf targets.tar + rm targets.tar + + - env: + CI_SNAPSHOT_RELEASE: +publishSigned + PGP_PASSPHRASE: ${{ secrets.PGP_PASSPHRASE }} + SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }} + SONATYPE_USERNAME: ${{ secrets.SONATYPE_USERNAME }} + PGP_SECRET: ${{ secrets.PGP_SECRET }} + run: sbt ci-release diff --git a/.github/workflows/clean.yml b/.github/workflows/clean.yml new file mode 100644 index 00000000..547aaa43 --- /dev/null +++ b/.github/workflows/clean.yml @@ -0,0 +1,59 @@ +# This file was automatically generated by sbt-github-actions using the +# githubWorkflowGenerate task. You should add and commit this file to +# your git repository. It goes without saying that you shouldn't edit +# this file by hand! Instead, if you wish to make changes, you should +# change your sbt build configuration to revise the workflow description +# to meet your needs, then regenerate this file. + +name: Clean + +on: push + +jobs: + delete-artifacts: + name: Delete Artifacts + runs-on: ubuntu-latest + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + steps: + - name: Delete artifacts + run: | + # Customize those three lines with your repository and credentials: + REPO=${GITHUB_API_URL}/repos/${{ github.repository }} + + # A shortcut to call GitHub API. + ghapi() { curl --silent --location --user _:$GITHUB_TOKEN "$@"; } + + # A temporary file which receives HTTP response headers. + TMPFILE=/tmp/tmp.$$ + + # An associative array, key: artifact name, value: number of artifacts of that name. + declare -A ARTCOUNT + + # Process all artifacts on this repository, loop on returned "pages". + URL=$REPO/actions/artifacts + while [[ -n "$URL" ]]; do + + # Get current page, get response headers in a temporary file. + JSON=$(ghapi --dump-header $TMPFILE "$URL") + + # Get URL of next page. Will be empty if we are at the last page. + URL=$(grep '^Link:' "$TMPFILE" | tr ',' '\n' | grep 'rel="next"' | head -1 | sed -e 's/.*.*//') + rm -f $TMPFILE + + # Number of artifacts on this page: + COUNT=$(( $(jq <<<$JSON -r '.artifacts | length') )) + + # Loop on all artifacts on this page. + for ((i=0; $i < $COUNT; i++)); do + + # Get name of artifact and count instances of this name. + name=$(jq <<<$JSON -r ".artifacts[$i].name?") + ARTCOUNT[$name]=$(( $(( ${ARTCOUNT[$name]} )) + 1)) + + id=$(jq <<<$JSON -r ".artifacts[$i].id?") + size=$(( $(jq <<<$JSON -r ".artifacts[$i].size_in_bytes?") )) + printf "Deleting '%s' #%d, %'d bytes\n" $name ${ARTCOUNT[$name]} $size + ghapi -X DELETE $REPO/actions/artifacts/$id + done + done diff --git a/.github/workflows/coveralls.yml b/.github/workflows/coveralls.yml new file mode 100644 index 00000000..eea5f11c --- /dev/null +++ b/.github/workflows/coveralls.yml @@ -0,0 +1,59 @@ +name: Coveralls Publish + +on: + push: + branches: [master] + tags: ["v*"] + +env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + +concurrency: + group: ${{ github.workflow }} @ ${{ github.ref }} + cancel-in-progress: true + +jobs: + build: + name: Build and Test + strategy: + matrix: + os: [ubuntu-latest, windows-latest] + scala: [3.4.2] + java: [zulu@21] + runs-on: ${{ matrix.os }} + timeout-minutes: 60 + steps: + - name: Ignore line ending differences in git + if: contains(runner.os, 'windows') + shell: bash + run: git config --global core.autocrlf false + + - name: Checkout current branch (full) + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup Java (zulu@21) + id: setup-java-zulu-21 + if: matrix.java == 'zulu@21' + uses: actions/setup-java@v4 + with: + distribution: zulu + java-version: 21 + cache: sbt + + - name: sbt update + if: matrix.java == 'zulu@21' && steps.setup-java-zulu-21.outputs.cache-hit == 'false' + shell: bash + run: sbt +update + + - name: Build project + run: sbt '++ ${{ matrix.scala }}' coverage test + + - run: sbt '++ ${{ matrix.scala }}' coverageReport + + - name: Coveralls + uses: coverallsapp/github-action@v2 + with: + git-branch: main \ No newline at end of file diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml deleted file mode 100644 index 5b210239..00000000 --- a/.github/workflows/publish.yml +++ /dev/null @@ -1,27 +0,0 @@ -name: Release -on: - create: # Publish release on tag creation - -jobs: - publish: - runs-on: ubuntu-20.04 - steps: -# - run: git checkout "${GITHUB_REF:11}" - - uses: actions/checkout@v2.3.4 - if: github.event.ref_type == 'tag' - with: - fetch-depth: 0 - - uses: olafurpg/setup-scala@v10 - with: - java-version: openjdk@1.13 - if: github.event.ref_type == 'tag' - - uses: olafurpg/setup-gpg@v3 - if: github.event.ref_type == 'tag' - - run: sbt ci-release - if: github.event.ref_type == 'tag' - env: - CI_SNAPSHOT_RELEASE: +publishSigned - PGP_PASSPHRASE: ${{ secrets.PGP_PASSPHRASE }} - PGP_SECRET: ${{ secrets.PGP_SECRET }} - SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }} - SONATYPE_USERNAME: ${{ secrets.SONATYPE_USERNAME }} diff --git a/.gitignore b/.gitignore index 1df0bca6..288ecd8b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,8 @@ +.DS_Store out/ -.bsp/ +.vscode/ +.metals/ +.bloop/ .idea/ .idea_modules/ target/ @@ -7,6 +10,8 @@ reports/ lib_managed/ src_managed/ project/boot/ +project/project/ +project/project/target/ project/plugins/project java_pid*.hprof *.swp @@ -16,3 +21,6 @@ tags .history *~ wip/ +.dotty-ide* +_site/ +.bsp/ diff --git a/.scalafmt.conf b/.scalafmt.conf index 9d7a428f..f0e03d4f 100644 --- a/.scalafmt.conf +++ b/.scalafmt.conf @@ -1,3 +1,20 @@ -align = more // For pretty alignment. -maxColumn = 175 // For my wide 30" display. -assumeStandardLibraryStripMargin = true +version = 3.7.14 +project.git = true +maxColumn = 256 +runner.dialect = Scala3 + +align.preset = some + +rewrite.rules = [Imports, RedundantBraces, SortModifiers] +rewrite.imports.sort = scalastyle +rewrite.redundantBraces.stringInterpolation = true + +rewrite.scala3.convertToNewSyntax = true +rewrite.scala3.removeOptionalBraces = false + +docstrings.blankFirstLine = no +docstrings.style = SpaceAsterisk +docstrings.wrap = no + +newlines.sometimesBeforeColonInMethodReturnType = true +lineEndings=unix diff --git a/LICENSE b/LICENSE index 80629b49..38b4d1c5 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -MIT License +## MIT License Copyright (c) 2020 Greg Zoller @@ -19,3 +19,25 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +## ZIO-Json Attribution + +Parts of JSON reading software were used either directly or derived from the +[ZIO-Json project](https://github.com/zio/zio-json) +licensed under the [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0). +In most cases, "derived" means removal of features not needed by ScalaJack or +other changes needed to adapt the code to the ScalaJack ecosystem. Regardless, +they are credited here as materially the same as those in the ZIO-Json project. + +The files used directly or derived are: +* FastStringBuilder.scala +* FieldKeyDecoder.scala +* JsonDecoder.scala +* JsonParser.scala +* JsonReader.scala +* Numbers.scala +* StringMatrix.scala + +The terms, privileges, and restrictions provided by the Apache License 2.0 +fully apply to these files, where these differ from the MIT license, which +applies to the rest of ScalaJack's code. \ No newline at end of file diff --git a/README.md b/README.md index 16632ebc..808b39da 100644 --- a/README.md +++ b/README.md @@ -1,84 +1,79 @@ - # ScalaJack [![license](https://img.shields.io/github/license/mashape/apistatus.svg?maxAge=86400)](https://opensource.org/licenses/MIT) -[![Maven Central](https://maven-badges.herokuapp.com/maven-central/co.blocke/scalajack_3/badge.svg)](https://search.maven.org/artifact/co.blocke/scalajack_3/7.0.0/jar) -ScalaJack 7 is an all-new ScalaJack implmenation built on Scala 3. For Scala 2.13 ScalaJack, please use (frozen) version 6.2.0. ScalaJack 7 is built on JDK 13+. +[![Maven Central](https://maven-badges.herokuapp.com/maven-central/co.blocke/scalajack_3/badge.svg)](https://search.maven.org/artifact/co.blocke/scalajack_3/8.0.0/jar) -ScalaJack is a very fast, seamless serialization engine for JSON, and other protocols, designed to require the minimum amount of help possible when serializing a class. +ScalaJack 8 is an all-new ScalaJack serializer implemenation built on Scala 3. For Scala 2.13 ScalaJack, please use the frozen version 6.2.0. ScalaJack 8 is built +using Scala 3.4.2 on JDK 21 LTS version. This is done to be as current as possible and also because Scala 3.4.2 provides improvements to code test coverage instrumentation. -Advanced Features: - - Handles tuples - - 'Any' support - - Handles default values for case class fields - - Rich configuration of trait type hint/value - - Supports value classes - - Sealed trait-style enumerations - - Extensible to other encodings (JSON, CSV, MongoDB, and DynamoDB are provided by ScalaJack, but you can roll your own too!) +ScalaJack is a very fast, seamless serialization engine for non-schema data designed to require a bare minimum of extra code +to serialize a class. ScalaJack currently only supports JSON, however when we looked at adding MsgPack support to our great surprise benchmarks +showed that MsgPack serialization had about 25% slower write performance and 45% slower read performance than JSON, so we're sticking with JSON for the time being. ## Use - ScalaJack is extremely simple to use. Include the following in your build.sbt: ``` - libraryDependencies ++= Seq("co.blocke" %% "scalajack" % SJ_VERSION) -``` - -To use the **highly-recommended** reflection compiler plug-in, add to build.sbt: +libraryDependencies ++= Seq("co.blocke" %% "scalajack" % SJ_VERSION) ``` -addCompilerPlugin("co.blocke" %% "scala-reflection" % VERSION) -``` -where VERSION is the latest scala-reflection version found by looking at the 'maven central' badge from this repo: [www.blocke.co/scala-reflection](http://www.blocke.co/scala-reflection) +Now you're good to go! Let's use ScalaJack in your project to serialize/deserialize a case class object into JSON: +```scala +// File1.scala +case class Person(name: String, age: Int) -If you want to use the optional MongoDB serialization support include this as well: -``` - libraryDependencies ++= Seq("co.blocke" %% "scalajack_mongo" % SJ_VERSION) -``` +// File2.scala +import co.blocke.scalajack.* -DynamoDB helpers are available here: -``` - libraryDependencies ++= Seq("co.blocke" %% "scalajack_dynamo" % SJ_VERSION) +given sjPerson: ScalaJack[Person] = sjCodecOf[Person] // create a re-usable Person codec +... +val inst = Person("Mike",34) +val js = sjPerson.toJson(inst) // """{"name":"Mike","age":34}""" +sjPerson.fromJson(js) // re-constitutes original Person ``` -where SJ_VERSION is this version of ScalaJack (see 'maven central' badge of this repo). +Couldn't be simpler! -Now you're good to go! Let's use ScalaJack in your project to serialize/de-serialize a case class object into JSON: +| **NOTE:** Classes must be defined in a different file from where ScalaJack is called. +| This is a Scala macro requirement, not a ScalaJack limitation. -```scala -import co.blocke.scalajack._ +### A word about performance... -case class Person(name: String, age: Int) +Compared to pre-8.0 ScalaJack, which used Scala 2.x runtime reflection, ScalaJack is dramatically faster in almost every case. How does this work? ScalaJack 8 uses compile-time macros to generate all the serialization code for you (the codecs). It's very much like writing hand-tooled, field-by-field serialization code yourself, except ScalaJack does it at compile-time. Wherever you see ```sjCodecOf``` is where the compiler will generate all the serialization code. **(That also means you should try not to use sjCodecOf more than once for any given class or you'll generate a lot of redundant code!)** -val sj = ScalaJack() -val js = sj.render(Person("Mike",34)) // js == """{"name":"Mike","age":34}""" -val inst = sj.read[Person](js) // re-constitutes original Person -``` +### Easy codecs +You only need to worry about generating codecs for your top-most level classes. Some serialization libraries require all nested classes in an object hierarchy to be +specifically called out for codec generation, which can get pretty burdensome. ScalaJack doesn't require this. For example: -Couldn't be simpler! +```scala +case class Dog(name: String, numLegs: Int) +case class Person(name: String, age: Int, dog: Dog) -### A word about performance... -Compared to pre-7.0 ScalaJack, which used Scala 2.x runtime reflection, ScalaJack is up to 30% faster in many cases when used with the highly-recommended scala-reflection compiler plugin. +// create a re-usable Person codec (includes Dog for free!) +given sjPerson: ScalaJack[Person] = sjCodecOf[Person] +``` +In this example, the contained Dog class is automatically detected and genrated by ScalaJack, so if all you care about is Person, and would never serialize a Dog as a top-level value, then Persion is the only codec you need. ### A word about macros... -ScalaJack 7 uses Scala 3 macros to the fullest extent possible to do the hard work of reflecting on types. Macros impact the compile/test cycle in ways that are non-intuitive at first. Think of this example: + +ScalaJack 8 uses Scala 3 macros to the fullest extent possible to do the hard work of reflecting on types. Macros impact the compile/test cycle in ways that are non-intuitive at first. Think of this example: ```scala // File1.scala -case class Foo(name: String) +case class Foo(name: String) // File2.scala -val js = sj.read[Foo](someJson) +given sjFoo: ScalaJack[Foo] = sjCodecOf[Foo] +val js = sjFoo.fromJson(someJson) ``` -In a non-macro implementation (e.g. Scala 2 runtime reflection) if you update Foo in File1.scala you naturally expect sbt to re-compile this file, and anything that depends on Foo, and the changes will be picked up in your program, and all will be well. +In a non-macro program (e.g. something using Scala 2 runtime reflection) let's say you add a new field to class Foo in File1.scala. You naturally expect sbt to re-compile this file, and anything that depends on Foo, and the changes will be picked up in your program, and all will be well. -That's **not** necessarily what happens with macros! Remember, the macro code is run at compile-time. File2.scala needs to be re-compiled because the macro needs to be re-run to pick up your changes to Foo class in File1.scala. **Unfortunately sbt doesn't pick up this dependency!** If you don't know any better you'll just re-run your program after a change to File1.scala, like normal, and get a **spectacular exception with exotic errors** that won't mean much to you. The solution is you need to also recompile File2.scala. +That's **not** necessarily what happens with macros! Remember, the macro code is run/expnded at compile-time. File2.scala needs to be re-compiled because the macro that gets expanded at sjCodecOf[Foo] needs to be re-generated to pick up your changes to Foo class in File1.scala. **Unfortunately sbt can't detect this dependency!** If you don't know any better you'll just re-run your program after a change to File1.scala, like normal, and you'll get a spectacular exception with exotic errors that won't mean much to you. The simple, but non-intuitive, solution is you need to also recompile File2.scala. -This means you will be doing more re-compiling with macro-based code than you would without the macros. It's an unfortunate cost of inconvenience and time, but the payoff is a *dramatic* gain in speed at runtime, and in the case of reflection in Scala 3, using macros is really the only way to accomplish reflection. +This means you will be doing more re-compiling with macro-based code than you would without the macros. It's an unfortunate cost of inconvenience, but the payoff is a *dramatic* gain in speed at runtime, and in the case of reflection in Scala 3, using macros is the only way to accomplish reflection, so there really isn't an alternative. ## Features - * [Case Classes and Traits](doc/classesAndTraits.md) * [Non-Case Classes and Java Class Support](doc/noncase.md) * [Re-name Case Class Fields](doc/mapname.md) @@ -86,26 +81,14 @@ This means you will be doing more re-compiling with macro-based code than you wo * [Value Class Support](doc/valueClass.md) * [Parameterized Classes](doc/parameterized.md) * [Trait Type Hint Customization](doc/typeHint.md) -* [Custom Type Adapters (custom read/render)](doc/custom.md) -* [Try and Capture](doc/tryAndCapture.md) -* [ParseOrElse and Cascading Fallback Parsing](doc/parseOrElse.md) * [Null and None treatment](doc/nullAndNone.md) -* [Externalized Type Hints](doc/externalTypes.md) -* [View/SpliceInto](doc/viewSplice.md) -* [Filter](doc/filter.md) +* [NeoType Support](doc/neotype.md) * [Union type](doc/union.md) -* [Converters](doc/map.md) -* [ScalaJack Configuration](doc/config.md) -* [Gimme Speed!](doc/speed.md) - -Non-JSON Formats: -* [YAML](doc/yaml.md) -* [MongoDB](doc/mongo.md) -* [Delimited (e.g. CSV)](doc/delimited.md) -* [DynamoDB](doc/dynamo.md) -* [Json4s](doc/json4s.md) +* [Gimme Speed!](benchmark/README.md) ### Notes: -* 7.0.3 -- Rebuild on Scala 3.2.1 -* 7.0.1 -- GA release of ScalaJack 7 for Scala 3. -* 7.0.0-M2 -- Initial release for Scala 3 + +* 8.0.0 -- Rebuild on Scala 3.4.2 and deep refactor of ScalaJack 7.0 +* 7.0.3 -- Rebuild on Scala 3.2.1 +* 7.0.1 -- GA release of ScalaJack 7 for Scala 3. +* 7.0.0-M2 -- Initial release for Scala 3 \ No newline at end of file diff --git a/benchmark/README.md b/benchmark/README.md new file mode 100644 index 00000000..a0662989 --- /dev/null +++ b/benchmark/README.md @@ -0,0 +1,116 @@ +# Performance + +JSON serialization benchmarks I found in various project repos often measured (IMO) silly things like how fast a parser could handle a small list of Int. For this benchmark I used a small, but slightly more representative model. It has some nested objects and collections that make it a more interesting test. + +The test is run via jmh. The JVM is **stock**--not tuned to within an inch of its life, to be a more realistic +use case. + +Run benchmark from the ScalaJack/benchmark directory (not the main ScalaJack project directory): +``` +sbt "jmh:run -i 10 -wi 10 -f 2 -t 1 co.blocke.*" +``` + +## Reading Performance: + +![image info](./ReadingPerformance.png) + +| Benchmark | Mode | Count | Score | Error | Units | +|------------------|-------|-------:|----------------:|-------------:|-------| +| Jsoniter | thrpt | 20 | 1346388.345 | ± 17028.863 | ops/s | +| **ScalaJack 8** | thrpt | 20 | **986597.070**| ± 7473.148 | ops/s | +| ZIOJson | thrpt | 20 | 590995.917 | ± 817.817 | ops/s | +| Circe | thrpt | 20 | 210805.946 | ± 32488.564 | ops/s | +| Play | thrpt | 20 | 198747.067 | ± 7253.896 | ops/s | +| Argonaut | thrpt | 20 | 183670.032 | ± 8981.485 | ops/s | + +## Writing Performance: + +![image info](./WritingPerformance.png) + +| Benchmark | Mode | Count | Score | Error | Units | +|------------------|-------|-------:|----------------:|-------------:|-------| +|**ScalaJack 8 no escaped chars in String** | thrpt | 20 | **5200691.37** | ± 114219.728 | ops/s | +|**ScalaJack 8** | thrpt | 20 | **3039273.222** | ± 14952.932 | ops/s | +| Jsoniter | thrpt | 20 | 2843150.452 | ± 21478.503 | ops/s | +| Hand-Tooled | thrpt | 20 | 2732571.374 | ± 15129.007 | ops/s | +| Circe | thrpt | 20 | 1958244.437 | ± 23965.817 | ops/s | +| ZIO JSON | thrpt | 20 | 794352.301 | ± 32336.852 | ops/s | +| Argonaut | thrpt | 20 | 690269.697 | ± 6348.882 | ops/s | +| Play JSON | thrpt | 20 | 438650.022 | ± 23800.221 | ops/s | + +**Note:** Exact numbers aren't terribly important--they may vary widely depending on the platform +used. The important thing is the relative relationship between libraries given all tests +were performed on the same platform. + +**Note:** That extreme write speed for ScalaJack 8 is achieved by disabling escaped/special character +processing using +```scala +sj[Foo](JsonConfig.withSuppressEscapedStrings()) +``` +Its for when you need that absolute maximum performance and you're 100% sure there's 0 chance of any double-quotes, newlines, tabs, or +any other non-character/digit unicode special characters in your String values that require escaping. + +### Interpretation + +Performance for ScalaJack has been a journey. ScalaJack is a mature product--over 10 yrs old! +Long ago it was quite fast vs its competition, but as its peers improved its performance lagged badly, +to the point that it became one of the slower serialization libraries. ScalaJack 8 changes that! + +I was sampling and testing against a collection of popular serializers for Scala util +something quite unexpected happend: I discovered Jsoniter. Its performance was through +the roof! It far outpaced all competitors for raw speed. This was a shock. I had to +learn how this worked. + +So full credit where credit is due: ScalaJack 8's reading/writing codec architecture +is heavily informed from Jsoniter, so I'll post their licence here: + +[Jsoniter's License](https://github.com/plokhotnyuk/jsoniter-scala/blob/af23cf65a70d48834b8fecb792cc333b23409c6f/LICENSE) + +There are a number of optimizations and design choices I elected not to bring over from +Jsoniter, in many cases because ScalaJack doesn't need them for its intended feature set. +Of course ScalaJack utilizes our own scala-reflection library to great effect, which Jsoniter does not. + +Jsoniter achieves its neck-breaking speed by going deep--very deep--into macro code +generation. They also use a lot of low-level byte arrays and bitwise operators +to improve on the standard library functions everyone else uses. It works. + +### Technical Notes + +Achieving extreme speed for ScalaJack 8 was months of learning, trial, error, +and re-writes. I studied Jsoniter, Circe, ZIO Json, and others to learn optimizations. +The tough news for anyone wanting to duplicate this kind of performance in your own code +is that there isn't one magic trick to achieve maximum performance. It's a basket +of techniques, each achieving small marginal gains that add up, and you must decide when +enough is enough for you. Here's a partial list of learnings incorporated into ScalaJack 8: + +* Being careful when using .asInstanceOf[]. In fact, try to avoid it wherever possible + as it messes up CPU cache, harming performance. This means a lot of very careful type + management, and its why you see the RTypeRefs from scala-reflection are now all typed + in the latest version + +* Lots of specific typing. Don't make the compiler think--provide detailed types wherever + you can + +* In some cases (though not all) a series of if/else if/else statements is faster than + match/case statements + +* A carefully-crafted @tailrec function is often faster than a function driven by a + while loop. + +* For macro-based software like this--find every opportunity to do hard work at + compile-time + +* Be mindful of what code your macros generate! You can write a macro straight from + a blog example and get something working, but you may be disappointed if maximizing runtime + speed is your goal. The generated code may look kludgy and have extra cruft in it. Rework your + macros carefully until the generated code is as smooth as you might write by hand. + +After all the performance tunings and learnings, I was able to meet or beat Jsoniter for writing +speed. For reading I made a number of very substantial improvements, but there is still a +substantial performance gap between Jsoniter's reads and ScalaJack's, and for the life of me I can't +figure out what's driving that difference. The generated code is very similar. Json parsing should +be similar--in fact in some ways ScalaJack's should be faster. Although micro-benchmarks indicate +match/case is significantly slower in generated code than if/than/else, but in practice replacing +these with if/else didn't gain ScalaJack a thing. Jsoniter has some tricky ByteArrayAccess code +that looks very low-level and clever, but when I benchmarked it, the gains seemed nominal to none +in my use case. I dunno--If anyone has any ideas, please drop a comment in the Issues in repo! \ No newline at end of file diff --git a/benchmark/ReadingPerformance.png b/benchmark/ReadingPerformance.png new file mode 100644 index 0000000000000000000000000000000000000000..263a26c21e4be797c1bbf2ab265d61b90bcc2205 GIT binary patch literal 69457 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 K&t;ucLK6U_3b2a+ literal 0 HcmV?d00001 diff --git a/benchmark/WritingPerformance.png b/benchmark/WritingPerformance.png new file mode 100644 index 0000000000000000000000000000000000000000..34e685bc71f3ce991d8e5a1bde1c2e170bed09f6 GIT binary patch literal 86164 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%0A$Bbf_ziR5f6%ycCh}929grzQ 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 Z<`YKg>7*H&O@apeLFHBCN@NWD{}=25>IVP- literal 0 HcmV?d00001 diff --git a/benchmark/build.sbt b/benchmark/build.sbt new file mode 100644 index 00000000..548c1c1e --- /dev/null +++ b/benchmark/build.sbt @@ -0,0 +1,64 @@ +ThisBuild / organization := "co.blocke" + +val compilerOptions = Seq( + "-deprecation", + "-encoding", + "UTF-8", + "-explain", + "-feature", + "-language:existentials", + "-language:higherKinds", + "-unchecked" +) + +val circeVersion = "0.15.0-M1" +val scalaTestVersion = "3.2.11" +ThisBuild / scalaVersion := "3.4.2" + +def priorTo2_13(scalaVersion: String): Boolean = + CrossVersion.partialVersion(scalaVersion) match { + case Some((2, minor)) if minor < 13 => true + case _ => false + } + +val baseSettings = Seq( + scalacOptions ++= compilerOptions +) + +lazy val benchmark = project + .in(file(".")) + .settings(baseSettings ++ noPublishSettings) + .settings( + libraryDependencies ++= Seq( + "io.circe" %% "circe-core", + "io.circe" %% "circe-generic", + "io.circe" %% "circe-parser" + ).map(_ % circeVersion), + libraryDependencies ++= Seq( + "org.playframework" %% "play-json" % "3.0.1", + "io.argonaut" %% "argonaut" % "6.3.9", + "co.blocke" %% "scalajack" % "8.0.0", + "dev.zio" %% "zio-json" % "0.6.1", + "org.typelevel" %% "fabric-core" % "1.12.6", + "org.typelevel" %% "fabric-io" % "1.12.6", + "org.typelevel" %% "jawn-parser" % "1.3.2", + "org.typelevel" %% "jawn-ast" % "1.3.2", + "com.github.plokhotnyuk.jsoniter-scala" %% "jsoniter-scala-core" % "2.28.5", + "com.github.plokhotnyuk.jsoniter-scala" %% "jsoniter-scala-macros" % "2.28.5" % "compile-internal", + // "com.github.plokhotnyuk.jsoniter-scala" %% "jsoniter-scala-core" % "2.24.4", + // "com.github.plokhotnyuk.jsoniter-scala" %% "jsoniter-scala-macros" % "2.24.4" % "compile-internal", + // "io.circe" %% "circe-derivation" % "0.15.0-M1", + // "io.circe" %% "circe-jackson29" % "0.14.0", + // "org.json4s" %% "json4s-jackson" % "4.0.4", + // "com.github.plokhotnyuk.jsoniter-scala" %% "jsoniter-scala-core" % "2.13.17", + // "com.github.plokhotnyuk.jsoniter-scala" %% "jsoniter-scala-macros" % "2.13.17", + "org.scalatest" %% "scalatest" % scalaTestVersion % Test + ) + ) + .enablePlugins(JmhPlugin) + +lazy val noPublishSettings = Seq( + publish := {}, + publishLocal := {}, + publishArtifact := false +) diff --git a/benchmark/project/build.properties b/benchmark/project/build.properties new file mode 100644 index 00000000..27430827 --- /dev/null +++ b/benchmark/project/build.properties @@ -0,0 +1 @@ +sbt.version=1.9.6 diff --git a/benchmark/project/plugins.sbt b/benchmark/project/plugins.sbt new file mode 100644 index 00000000..514aeb2e --- /dev/null +++ b/benchmark/project/plugins.sbt @@ -0,0 +1 @@ +addSbtPlugin("pl.project13.scala" % "sbt-jmh" % "0.4.6") diff --git a/benchmark/src/main/scala/co.blocke/Argonaut.scala b/benchmark/src/main/scala/co.blocke/Argonaut.scala new file mode 100644 index 00000000..83ba6d17 --- /dev/null +++ b/benchmark/src/main/scala/co.blocke/Argonaut.scala @@ -0,0 +1,33 @@ +package co.blocke + +import org.openjdk.jmh.annotations._ + + +object ArgonautZ: + import argonaut._, Argonaut._ + + implicit val CodecPet: CodecJson[Pet2] = + casecodec3(Pet2.apply, (a: Pet2) => Option((a.name, a.species, a.age)))("name","species","age") + + implicit val CodecFriend: CodecJson[Friend2] = + casecodec3(Friend2.apply, (a: Friend2) => Option((a.name, a.age, a.email)))("name","age","email") + + implicit val CodecAddress: CodecJson[Address2] = + casecodec4(Address2.apply, (a: Address2) => Option((a.street, a.city, a.state, a.postal_code)))("street","city","state","postal_code") + + implicit val CodecPerson: CodecJson[Person2] = + casecodec6(Person2.apply, (a: Person2) => Option((a.name, a.age, a.address, a.email, a.phone_numbers, a.is_employed)))("name", "age","address","email","phone_numbers","is_employed") + + implicit val CodecRecord: CodecJson[Record2] = + casecodec4(Record2.apply, (a: Record2) => Option((a.person, a.hobbies, a.friends, a.pets)))("person", "hobbies", "friends", "pets") + + + trait ArgonautReadingBenchmark { + @Benchmark + def readRecordArgonaut = Parse.decodeEither[Record2](jsData2) + } + + trait ArgonautWritingBenchmark { + @Benchmark + def writeRecordArgonaut = record.asJson + } diff --git a/benchmark/src/main/scala/co.blocke/Benchmark.scala b/benchmark/src/main/scala/co.blocke/Benchmark.scala new file mode 100644 index 00000000..72e57d30 --- /dev/null +++ b/benchmark/src/main/scala/co.blocke/Benchmark.scala @@ -0,0 +1,63 @@ +package co.blocke + +import org.openjdk.jmh.annotations._ +import java.util.concurrent.TimeUnit + +import ZIOZ.* +import zio.json._ +val record = jsData2.fromJson[Record2] match + case Right(r) => r + case Left(_) => null.asInstanceOf[Record2] + + +trait HandTooledWritingBenchmark { + @Benchmark + def writeRecordHandTooled = + val sb = new StringBuilder() + sb.append("{") + sb.append("\"person\":{") + sb.append("\"name:\":\""+record.person.name+"\",") + sb.append("\"age:\":"+record.person.age+",") + sb.append("\"address:\":{\"street\":"+record.person.address.street+"\",") + sb.append("\"city\":\""+record.person.address.city+"\",") + sb.append("\"state\":\""+record.person.address.state+"\",") + sb.append("\"postal_code\":\""+record.person.address.postal_code+"\"},") + sb.append("\"email:\":\""+record.person.email+"\",") + sb.append("\"phone_numbers:\":[") + record.person.phone_numbers.map(p => sb.append("\""+p+"\",")) + sb.append("],") + sb.append("\"is_employed:\":"+record.person.is_employed+"},") + sb.append("\"hobbies:\":[") + record.hobbies.map(p => sb.append("\""+p+"\",")) + sb.append("],") + sb.append("\"friends:\":[") + record.friends.map(f=>sb.append(s"""{"name":"${f.name},"age":${f.age},"email":"${f.email}"},""")) + sb.append("],") + sb.append("\"pets:\":[") + record.pets.map(f=>sb.append(s"""{"name":"${f.name},"species":"${f.species}"","age":${f.age}},""")) + sb.append("]}") + sb.toString + } + +@State(Scope.Thread) +@BenchmarkMode(Array(Mode.Throughput)) +@OutputTimeUnit(TimeUnit.SECONDS) +class ReadingBenchmark + extends ScalaJackZ.ScalaJackReadingBenchmark + with CirceZ.CirceReadingBenchmark + with JsoniterZ.JsoniterReadingBenchmark + with ZIOZ.ZIOJsonReadingBenchmark + with PlayZ.PlayReadingBenchmark + with ArgonautZ.ArgonautReadingBenchmark + +@State(Scope.Thread) +@BenchmarkMode(Array(Mode.Throughput)) +@OutputTimeUnit(TimeUnit.SECONDS) +class WritingBenchmark + extends HandTooledWritingBenchmark + with CirceZ.CirceWritingBenchmark + with ScalaJackZ.ScalaJackWritingBenchmark + with JsoniterZ.JsoniterWritingBenchmark + with ZIOZ.ZIOJsonWritingBenchmark + with PlayZ.PlayWritingBenchmark + with ArgonautZ.ArgonautWritingBenchmark diff --git a/benchmark/src/main/scala/co.blocke/Circe.scala b/benchmark/src/main/scala/co.blocke/Circe.scala new file mode 100644 index 00000000..58ec31a5 --- /dev/null +++ b/benchmark/src/main/scala/co.blocke/Circe.scala @@ -0,0 +1,34 @@ +package co.blocke + +import org.openjdk.jmh.annotations._ + +object CirceZ: + import io.circe.syntax.* + import io.circe.* + import io.circe.generic.semiauto.* + import io.circe.parser.* + + implicit val recordDecoder: Decoder[Record2] = deriveDecoder[Record2] + implicit val recordEncoder: Encoder[Record2] = deriveEncoder[Record2] + + implicit val personDecoder: Decoder[Person2] = deriveDecoder[Person2] + implicit val personEncoder: Encoder[Person2] = deriveEncoder[Person2] + + implicit val addressDecoder: Decoder[Address2] = deriveDecoder[Address2] + implicit val addressEncoder: Encoder[Address2] = deriveEncoder[Address2] + + implicit val friendDecoder: Decoder[Friend2] = deriveDecoder[Friend2] + implicit val friendEncoder: Encoder[Friend2] = deriveEncoder[Friend2] + + implicit val petDecoder: Decoder[Pet2] = deriveDecoder[Pet2] + implicit val petEncoder: Encoder[Pet2] = deriveEncoder[Pet2] + + trait CirceReadingBenchmark{ + @Benchmark + def readRecordCirce = parse(jsData2).flatMap(_.as[Record2]) + } + + trait CirceWritingBenchmark { + @Benchmark + def writeRecordCirce = record.asJson + } diff --git a/benchmark/src/main/scala/co.blocke/Fabric.scalax b/benchmark/src/main/scala/co.blocke/Fabric.scalax new file mode 100644 index 00000000..06bf7e40 --- /dev/null +++ b/benchmark/src/main/scala/co.blocke/Fabric.scalax @@ -0,0 +1,24 @@ +package co.blocke + +import org.openjdk.jmh.annotations._ + +object FabricZ: + import fabric.* + import fabric.io.* + import fabric.rw.* + + implicit val rw: RW[Record] = RW.gen + + trait FabricReadingBenchmark{ + @Benchmark + def readRecordFabric = rw.write(JsonParser(jsData, Format.Json)) + //JsonParser(jsData, Format.Json) + } + + trait FabricWritingBenchmark{ + @Benchmark + def writeRecordFabric = rw.read(record) + } + + // No Fabric write test. Fabric has a different model... not simple serialization. + // Kinda like a query captured and compiled. diff --git a/benchmark/src/main/scala/co.blocke/Jawn.scalax b/benchmark/src/main/scala/co.blocke/Jawn.scalax new file mode 100644 index 00000000..ef82d38b --- /dev/null +++ b/benchmark/src/main/scala/co.blocke/Jawn.scalax @@ -0,0 +1,17 @@ +package co.blocke + +import org.openjdk.jmh.annotations._ + +object JawnZ: + + import org.typelevel.jawn.ast.* + + trait JawnReadingBenchmark{ + @Benchmark + def readRecordJawn = JParser.parseFromString(jsData) + } + + trait JawnWritingBenchmark { + @Benchmark + def writeRecordJawn = record.asJson + } diff --git a/benchmark/src/main/scala/co.blocke/Jsoniter.scala b/benchmark/src/main/scala/co.blocke/Jsoniter.scala new file mode 100644 index 00000000..e591e425 --- /dev/null +++ b/benchmark/src/main/scala/co.blocke/Jsoniter.scala @@ -0,0 +1,19 @@ +package co.blocke + +import org.openjdk.jmh.annotations._ + +object JsoniterZ: + + import com.github.plokhotnyuk.jsoniter_scala.core._ + import com.github.plokhotnyuk.jsoniter_scala.macros._ + + given codec: JsonValueCodec[Record2] = JsonCodecMaker.make + trait JsoniterReadingBenchmark{ + @Benchmark + def readRecordJsoniter = readFromString[Record2](jsData2) + } + + trait JsoniterWritingBenchmark{ + @Benchmark + def writeRecordJsoniter = writeToString(record) + } diff --git a/benchmark/src/main/scala/co.blocke/PlayJson.scala b/benchmark/src/main/scala/co.blocke/PlayJson.scala new file mode 100644 index 00000000..a3a84eb5 --- /dev/null +++ b/benchmark/src/main/scala/co.blocke/PlayJson.scala @@ -0,0 +1,60 @@ +package co.blocke + +import org.openjdk.jmh.annotations._ + +object PlayZ: + import play.api.libs.json._ + import play.api.libs.json.Reads._ + import play.api.libs.functional.syntax._ + + implicit val friendWrites: Writes[Friend2] = ( + (JsPath \ "name").write[String] and + (JsPath \ "age").write[Int] and + (JsPath \ "email").write[String] + )(unlift((a: Friend2) => Option((a.name, a.age, a.email)))) + + implicit val petWrites: Writes[Pet2] = ( + (JsPath \ "name").write[String] and + (JsPath \ "species").write[String] and + (JsPath \ "age").write[Int] + )(unlift((a: Pet2) => Option((a.name, a.species, a.age)))) + + implicit val addressWrites: Writes[Address2] = ( + (JsPath \ "street").write[String] and + (JsPath \ "city").write[String] and + (JsPath \ "state").write[String] and + (JsPath \ "postal_code").write[String] + )(unlift((a: Address2) => Option((a.street, a.city, a.state, a.postal_code)))) + + implicit val personWrites: Writes[Person2] = ( + (JsPath \ "name").write[String] and + (JsPath \ "age").write[Int] and + (JsPath \ "address").write[Address2] and + (JsPath \ "email").write[String] and + (JsPath \ "phone_numbers").write[List[String]] and + (JsPath \ "is_employed").write[Boolean] + )(unlift((a: Person2) => Option((a.name, a.age, a.address, a.email, a.phone_numbers, a.is_employed)))) + + implicit val recordWrites: Writes[Record2] = ( + (JsPath \ "person").write[Person2] and + (JsPath \ "hobbies").write[List[String]] and + (JsPath \ "friends").write[List[Friend2]] and + (JsPath \ "pets").write[List[Pet2]] + )(unlift((a: Record2) => Option((a.person, a.hobbies, a.friends, a.pets)))) + + implicit val friendReads: play.api.libs.json.Reads[co.blocke.Friend2] = Json.reads[Friend2] + implicit val petReads: play.api.libs.json.Reads[co.blocke.Pet2] = Json.reads[Pet2] + implicit val addressReads: play.api.libs.json.Reads[co.blocke.Address2] = Json.reads[Address2] + implicit val personReads: play.api.libs.json.Reads[co.blocke.Person2] = Json.reads[Person2] + implicit val recordReads: play.api.libs.json.Reads[co.blocke.Record2] = Json.reads[Record2] + + trait PlayWritingBenchmark { + @Benchmark + def writeRecordPlay = Json.toJson(record) + } + + // val playJS = Json.toJson(record) + trait PlayReadingBenchmark { + @Benchmark + def readRecordPlay = Json.fromJson[Record2](Json.parse(jsData2)) //Json.fromJson[Record](playJS) + } diff --git a/benchmark/src/main/scala/co.blocke/Record.scala b/benchmark/src/main/scala/co.blocke/Record.scala new file mode 100644 index 00000000..7dd85434 --- /dev/null +++ b/benchmark/src/main/scala/co.blocke/Record.scala @@ -0,0 +1,100 @@ +package co.blocke + +case class Person2( + name: String, + age: Int, + address: Address2, + email: String, + phone_numbers: List[String], + is_employed: Boolean +) + +case class Address2( + street: String, + city: String, + state: String, + postal_code: String +) + +case class Friend2( + name: String, + age: Int, + email: String +) + +case class Pet2( + name: String, + species: String, + age: Int +) + +case class Record2( + person: Person2, + hobbies: List[String], + friends: List[Friend2], + pets: List[Pet2] +) + +// import io.circe.{Codec,Decoder,Encoder,Json} +// import io.circe.generic.semiauto._ +// case class Who(id: String, name:String, `type`:Who.Type) +// object Who: +// implicit val codec: Codec[Who] = deriveCodec[Who] + +// sealed trait Type extends Product with Serializable +// object Type : +// val values: Set[Who.Type] = Set(Staff, Customer, Program) +// implicit val encoder: Encoder[Type] = ??? +// implicit val decoder: Decoder[Type] = ??? +// case object Staff extends Type +// case object Customer extends Type +// case object Program extends Type + +val jsData2 = + """{ + "person": { + "name": "John Doe", + "age": 30, + "address": { + "street": "123 Main Street", + "city": "Anytown", + "state": "CA", + "postal_code": "12345" + }, + "email": "john.doe@example.com", + "phone_numbers": [ + "555-555-5555", + "555-123-4567" + ], + "is_employed": true + }, + "hobbies": [ + "reading", + "swimming", + "traveling" + ], + "friends": [ + { + "name": "Jane Smith", + "age": 28, + "email": "jane.smith@example.com" + }, + { + "name": "Bob Johnson", + "age": 32, + "email": "bob.johnson@example.com" + } + ], + "pets": [ + { + "name": "Fido", + "species": "Dog", + "age": 5 + }, + { + "name": "Whiskers", + "species": "Cat", + "age": 3 + } + ] + }""" diff --git a/benchmark/src/main/scala/co.blocke/Run.scala b/benchmark/src/main/scala/co.blocke/Run.scala new file mode 100644 index 00000000..4ad296e3 --- /dev/null +++ b/benchmark/src/main/scala/co.blocke/Run.scala @@ -0,0 +1,17 @@ +package co.blocke + +import com.github.plokhotnyuk.jsoniter_scala.core._ +import com.github.plokhotnyuk.jsoniter_scala.macros._ + +object RunMe extends App: + + import co.blocke.scalajack.ScalaJack.* + import co.blocke.scalajack.* + + //case class MapHolder( m: Map[Pet2, String]) + // val mh = MapHolder( Map(Pet2("Mindy","Frenchie",4)->"a", Pet2("Rosie","Terrier",8)->"b")) + // given codec: JsonValueCodec[Pet2] = JsonCodecMaker.make + // given codec2: JsonValueCodec[MapHolder] = JsonCodecMaker.make + // println(writeToString(mh)) + + println("\nDone") \ No newline at end of file diff --git a/benchmark/src/main/scala/co.blocke/ScalaJack.scala b/benchmark/src/main/scala/co.blocke/ScalaJack.scala new file mode 100644 index 00000000..2c448de5 --- /dev/null +++ b/benchmark/src/main/scala/co.blocke/ScalaJack.scala @@ -0,0 +1,29 @@ +package co.blocke + +import org.openjdk.jmh.annotations._ + +object ScalaJackZ: + import co.blocke.scalajack.ScalaJack.* + import co.blocke.scalajack.* + + given sj: ScalaJack[co.blocke.Record2] = sjCodecOf[co.blocke.Record2] + + trait ScalaJackReadingBenchmark{ + @Benchmark + def readRecordScalaJack = sj.fromJson(jsData2) + } + + trait ScalaJackWritingBenchmark { + @Benchmark + def writeRecordScalaJack = sj.toJson(record) + } + + /* + This is the way: + + * Use implicit to define ScalaJack[...] = sj[...] + * Then use ScalaJack[...].___() to do json function + + implicit val blah: ScalaJack[co.blocke.Record2] = sj[co.blocke.Record2] + def writeRecordScalaJack = ScalaJack[co.blocke.Record2].toJson(record) + */ \ No newline at end of file diff --git a/benchmark/src/main/scala/co.blocke/ZIOJson.scala b/benchmark/src/main/scala/co.blocke/ZIOJson.scala new file mode 100644 index 00000000..9162ecdf --- /dev/null +++ b/benchmark/src/main/scala/co.blocke/ZIOJson.scala @@ -0,0 +1,27 @@ +package co.blocke + +import org.openjdk.jmh.annotations._ + +object ZIOZ: + import zio.json._ + + implicit val decoder1: JsonDecoder[Address2] = DeriveJsonDecoder.gen[Address2] + implicit val decoder2: JsonDecoder[Pet2] = DeriveJsonDecoder.gen[Pet2] + implicit val decoder3: JsonDecoder[Friend2] = DeriveJsonDecoder.gen[Friend2] + implicit val decoder4: JsonDecoder[Person2] = DeriveJsonDecoder.gen[Person2] + implicit val decoder5: JsonDecoder[Record2] = DeriveJsonDecoder.gen[Record2] + implicit val encoder1: JsonEncoder[Address2] = DeriveJsonEncoder.gen[Address2] + implicit val encoder2: JsonEncoder[Pet2] = DeriveJsonEncoder.gen[Pet2] + implicit val encoder3: JsonEncoder[Friend2] = DeriveJsonEncoder.gen[Friend2] + implicit val encoder4: JsonEncoder[Person2] = DeriveJsonEncoder.gen[Person2] + implicit val encoder5: JsonEncoder[Record2] = DeriveJsonEncoder.gen[Record2] + + trait ZIOJsonWritingBenchmark { + @Benchmark + def writeRecordZIOJson = record.toJson + } + + trait ZIOJsonReadingBenchmark { + @Benchmark + def readRecordZIOJson = jsData2.fromJson[Record2] + } \ No newline at end of file diff --git a/build.sbt b/build.sbt index 073c280c..aa197695 100644 --- a/build.sbt +++ b/build.sbt @@ -1,4 +1,7 @@ import org.typelevel.sbt.gha.JavaSpec.Distribution.Zulu +import scoverage.ScoverageKeys._ + +lazy val isCI = sys.env.get("CI").contains("true") inThisBuild(List( organization := "co.blocke", @@ -12,94 +15,79 @@ inThisBuild(List( url("http://www.blocke.co") ) ) + //coverageMinimumStmtTotal := 92, + //coverageFailOnMinimum := true )) name := "scalajack" -ThisBuild / githubWorkflowJavaVersions := Seq(JavaSpec(Zulu, "13")) ThisBuild / organization := "co.blocke" -val scala3 = "3.2.1" -val reflectionLibVersion = "1.1.11" +ThisBuild / scalaVersion := "3.4.2" +ThisBuild / githubWorkflowScalaVersions := Seq("3.4.2") -lazy val root = (project in file(".")) +lazy val root = project + .in(file(".")) .settings(settings) - .settings(publish / skip := true) .settings( - crossScalaVersions := Nil, - doc := null, // disable dottydoc for now - Compile / doc / sources := Seq() - ) - .aggregate(scalajack, scalajack_dynamo, scalajack_mongo) - -lazy val scalajack = (project in file("core")) - .settings(settings) - .settings( - name := "scalajack", + name := "serializer", + Compile / packageBin / mappings += { + (baseDirectory.value / "plugin.properties") -> "plugin.properties" + }, doc := null, // disable dottydoc for now Compile / doc / sources := Seq(), - libraryDependencies ++= commonDependencies, + //sources in (Compile, doc) := Seq(), Test / parallelExecution := false, - - // This messy stuff turns off reflection compiler plugin except for test case code - addCompilerPlugin("co.blocke" %% "scala-reflection" % reflectionLibVersion), - autoCompilerPlugins := false, - ivyConfigurations += Configurations.CompilerPlugin, - Test / scalacOptions ++= Classpaths.autoPlugins(update.value, Seq(), true) + scalafmtOnCompile := !isCI, + libraryDependencies ++= Seq( + "co.blocke" %% "scala-reflection" % "2.0.8", + "org.apache.commons" % "commons-text" % "1.11.0", + "io.github.kitlangton" %% "neotype" % "0.0.9", + "org.scalatest" %% "scalatest" % "3.2.17" % Test, + "org.json4s" %% "json4s-core" % "4.0.6" % Test, + "org.json4s" %% "json4s-native" % "4.0.6" % Test + ) ) -lazy val scalajack_dynamo = (project in file("dynamodb")) - .settings(settings) - .settings( - doc := null, // disable dottydoc for now - Compile / doc / sources := Seq(), - libraryDependencies ++= commonDependencies ++ Seq("com.amazonaws" % "aws-java-sdk-dynamodb" % "1.11.882" % Compile), - Test / parallelExecution := false, - - // This messy stuff turns off reflection compiler plugin except for test case code - addCompilerPlugin("co.blocke" %% "scala-reflection" % reflectionLibVersion), - autoCompilerPlugins := false, - ivyConfigurations += Configurations.CompilerPlugin, - Test / scalacOptions ++= Classpaths.autoPlugins(update.value, Seq(), true) - ).dependsOn(scalajack) - -lazy val scalajack_mongo = (project in file("mongo")) - .settings(settings) - .settings( - doc := null, // disable dottydoc for now - Compile / doc / sources := Seq(), - libraryDependencies ++= commonDependencies ++ Seq("org.mongodb" % "mongo-java-driver" % "3.12.7"), - Test / parallelExecution := false, - - // This messy stuff turns off reflection compiler plugin except for test case code - addCompilerPlugin("co.blocke" %% "scala-reflection" % reflectionLibVersion), - autoCompilerPlugins := false, - ivyConfigurations += Configurations.CompilerPlugin, - Test / scalacOptions ++= Classpaths.autoPlugins(update.value, Seq(), true) - ).dependsOn(scalajack) +ThisBuild / githubWorkflowJavaVersions := Seq(JavaSpec(Zulu, "21")) +ThisBuild / githubWorkflowOSes := Seq("ubuntu-latest") +ThisBuild / githubWorkflowPublishTargetBranches := Seq( + RefPredicate.Equals(Ref.Branch("main")), + RefPredicate.StartsWith(Ref.Tag("v")) +) -lazy val commonDependencies = Seq( - "co.blocke" %% "scala-reflection" % reflectionLibVersion, - "commons-codec" % "commons-codec" % "1.15", - "org.json4s" % "json4s-core_2.13" % "3.6.11", - "org.snakeyaml" % "snakeyaml-engine" % "2.3", - "org.json4s" % "json4s-native_2.13" % "3.6.11" % Test, - "org.scalameta" %% "munit" % "0.7.25" % Test +ThisBuild / githubWorkflowPublish := Seq( + WorkflowStep.Sbt( + List("ci-release"), + env = Map( + "PGP_PASSPHRASE" -> "${{ secrets.PGP_PASSPHRASE }}", + "PGP_SECRET" -> "${{ secrets.PGP_SECRET }}", + "SONATYPE_PASSWORD" -> "${{ secrets.SONATYPE_PASSWORD }}", + "SONATYPE_USERNAME" -> "${{ secrets.SONATYPE_USERNAME }}", + "CI_SNAPSHOT_RELEASE" -> "+publishSigned" + ) + ) ) //========================== // Settings //========================== +lazy val settings = Seq( + javacOptions ++= Seq("-source", "1.8", "-target", "1.8"), + scalacOptions ++= compilerOptions +) + lazy val compilerOptions = Seq( "-unchecked", "-feature", "-language:implicitConversions", "-deprecation", + // "-explain",' + "-coverage-exclude-files", + ".*SJConfig.*", + "-coverage-exclude-classlikes", + ".*internal.*", + "-coverage-exclude-classlikes", + ".*AnyWriter", "-encoding", "utf8" ) -lazy val settings = Seq( - scalacOptions ++= compilerOptions, - scalaVersion := scala3, - testFrameworks += new TestFramework("munit.Framework") -) - diff --git a/core/src/main/java/co/blocke/scalajack/DBKey.java b/core/src/main/java/co/blocke/scalajack/DBKey.java deleted file mode 100644 index 72377f60..00000000 --- a/core/src/main/java/co/blocke/scalajack/DBKey.java +++ /dev/null @@ -1,12 +0,0 @@ -package co.blocke.scalajack; - -import java.lang.annotation.*; - -@Inherited -@Target({ElementType.PARAMETER, ElementType.METHOD}) -@Retention(RetentionPolicy.RUNTIME) -public @interface DBKey { - // Some indexing schemes create unordered composite keys (eg Mongo) while others have primary/secondary distinctions (eg Dynamo). - // If the later, use the index parameter to "order" fields within a compound key. - int index() default 0; -} diff --git a/core/src/main/java/co/blocke/scalajack/Ignore.java b/core/src/main/java/co/blocke/scalajack/Ignore.java deleted file mode 100644 index ec479305..00000000 --- a/core/src/main/java/co/blocke/scalajack/Ignore.java +++ /dev/null @@ -1,13 +0,0 @@ -package co.blocke.scalajack; - -import java.lang.annotation.*; - -/** Annotation for Java getters or setters to tell reflector to ignore the decorated property for - * the purposes of reflection. - */ - -@Inherited -@Target({ElementType.METHOD, ElementType.FIELD}) -@Retention(RetentionPolicy.RUNTIME) -public @interface Ignore { -} \ No newline at end of file diff --git a/core/src/main/java/co/blocke/scalajack/JavaStuff.java b/core/src/main/java/co/blocke/scalajack/JavaStuff.java deleted file mode 100644 index d0111855..00000000 --- a/core/src/main/java/co/blocke/scalajack/JavaStuff.java +++ /dev/null @@ -1,11 +0,0 @@ -package co.blocke.scalajack; - -import java.math.*; - -public class JavaStuff { - - private BigInteger[][] multi; - - public BigInteger[][] getMulti() { return multi; } - public void setMulti(BigInteger[][] n) { multi = n; } -} \ No newline at end of file diff --git a/core/src/main/java/co/blocke/scalajack/Optional.java b/core/src/main/java/co/blocke/scalajack/Optional.java deleted file mode 100644 index e67af8c6..00000000 --- a/core/src/main/java/co/blocke/scalajack/Optional.java +++ /dev/null @@ -1,10 +0,0 @@ -package co.blocke.scalajack; - -import java.lang.annotation.*; - -@Inherited -@Target({ElementType.FIELD, ElementType.METHOD}) -@Retention(RetentionPolicy.RUNTIME) -public @interface Optional { -} - diff --git a/core/src/main/scala/co.blocke.scalajack/Converters.scala b/core/src/main/scala/co.blocke.scalajack/Converters.scala deleted file mode 100644 index 52a9cf4c..00000000 --- a/core/src/main/scala/co.blocke.scalajack/Converters.scala +++ /dev/null @@ -1,70 +0,0 @@ -package co.blocke.scalajack - -import co.blocke.scalajack.json.JsonFlavor -import model._ -import yaml.YamlFlavor -import delimited.DelimitedFlavor -import json4s.Json4sFlavor -import org.json4s.JValue -import json._ -import yaml._ -import delimited._ - -object Converters: - - //------ - // Note: Json, Json4s, and Yaml do NOT have a 'toDelimited' option because delimited format is *ordered*, while - // other 3 are intrinsically un-ordered, so there's no guarantee the delimited fields will be rendered in the - // 'correct' order. - //------ - - // JSON mappers - extension (j: JSON) - inline def mapJsonTo[T, S](toFlavor: JackFlavor[S])(fn: T => T)(implicit sjJ: JackFlavor[JSON]): S = toFlavor.render[T](fn(sjJ.read[T](j))) - - extension (j: JSON) - inline def jsonToYaml[T](implicit sjY: JackFlavor[YAML], sjJ: JackFlavor[JSON]): YAML = sjY.render( sjJ.read[T](j) ) - inline def jsonToJson4s[T](implicit sjV: JackFlavor[JValue], sjJ: JackFlavor[JSON]): JValue = sjV.render( sjJ.read[T](j) ) - inline def fromJson[T](implicit sjJ: JackFlavor[JSON]): T = sjJ.read[T](j) - inline def mapJson[T](fn: T => T)(implicit sjJ: JackFlavor[JSON]): JSON = sjJ.render[T](fn(sjJ.read[T](j))) - - - // YAML mappers - extension (y: YAML) - inline def mapYamlTo[T, S](toFlavor: JackFlavor[S])(fn: T => T)(implicit sjY: JackFlavor[YAML]): S = toFlavor.render[T](fn(sjY.read[T](y))) - - extension (y: YAML) - inline def yamlToJson[T](implicit sjJ: JackFlavor[JSON], sjY: JackFlavor[YAML]): JSON = sjJ.render( sjY.read[T](y) ) - inline def yamlToJson4s[T](implicit sjV: JackFlavor[JValue], sjY: JackFlavor[YAML]): JValue = sjV.render( sjY.read[T](y) ) - inline def fromYaml[T](implicit sjY: JackFlavor[YAML]): T = sjY.read[T](y) - inline def mapYaml[T](fn: T => T)(implicit sjY: JackFlavor[YAML]): YAML = sjY.render[T](fn(sjY.read[T](y))) - - - // DELIMITED mappers - extension (d: DELIMITED) - inline def mapDelimitedTo[T, S](toFlavor: JackFlavor[S])(fn: T => T)(implicit sjD: JackFlavor[DELIMITED]): S = toFlavor.render[T](fn(sjD.read[T](d))) - - extension (d: DELIMITED) - inline def delimitedToJson[T](implicit sjJ: JackFlavor[JSON], sjD: JackFlavor[DELIMITED]): JSON = sjJ.render( sjD.read[T](d) ) - inline def delimitedToJson4s[T](implicit sjV: JackFlavor[JValue], sjD: JackFlavor[DELIMITED]): JValue = sjV.render( sjD.read[T](d) ) - inline def delimitedToYaml[T](implicit sjY: JackFlavor[YAML], sjD: JackFlavor[DELIMITED]): YAML = sjY.render( sjD.read[T](d) ) - inline def fromDelimited[T](implicit sjD: JackFlavor[DELIMITED]): T = sjD.read[T](d) - inline def mapDelimited[T](fn: T => T)(implicit sjD: JackFlavor[DELIMITED]): DELIMITED = sjD.render[T](fn(sjD.read[T](d))) - - - // Json4s mappers - extension (j: JValue) - inline def mapJson4sTo[T, S](toFlavor: JackFlavor[S])(fn: T => T)(implicit sjV: JackFlavor[JValue]): S = toFlavor.render[T](fn(sjV.read[T](j))) - - extension (j: JValue) - inline def json4sToYaml[T](implicit sjY: JackFlavor[YAML], sjV: JackFlavor[JValue]): YAML = sjY.render( sjV.read[T](j) ) - inline def json4sToJson[T](implicit sjJ: JackFlavor[JSON], sjV: JackFlavor[JValue]): JSON = sjJ.render( sjV.read[T](j) ) - inline def fromJson4s[T](implicit sjV: JackFlavor[JValue]): T = sjV.read[T](j) - inline def mapJson4s[T](fn: T => T)(implicit sjV: JackFlavor[JValue]): JValue = sjV.render[T](fn(sjV.read[T](j))) - - - extension[T] (a: T) - inline def toJson(implicit sjJ: JackFlavor[JSON]): JSON = sjJ.render(a) - inline def toYaml(implicit sjY: JackFlavor[YAML]): YAML = sjY.render(a) - inline def toDelimited(implicit sjD: JackFlavor[DELIMITED]): DELIMITED = sjD.render(a) - inline def toJson4s(implicit sjV: JackFlavor[JValue]): JValue = sjV.render(a) diff --git a/core/src/main/scala/co.blocke.scalajack/Main.scalax b/core/src/main/scala/co.blocke.scalajack/Main.scalax deleted file mode 100644 index 33319130..00000000 --- a/core/src/main/scala/co.blocke.scalajack/Main.scalax +++ /dev/null @@ -1,37 +0,0 @@ -package co.blocke.scalajack - -import co.blocke.scala_reflection._ -import co.blocke.scala_reflection.info._ -import org.apache.commons.codec.binary.Base64 - -import scala.collection.mutable -import scala.jdk.CollectionConverters._ -import json._ -import yaml._ -import delimited._ -import model.JackFlavor -import co.blocke.scalajack.Converters._ - - -opaque type Foom = String - -object Main { - - def main(args: Array[String]): Unit = - - val f = "Foom".asInstanceOf[Foom] - if f == null then - println("Null!") - - - def constructors(clazz: Class[_]): String = - s"=== Constructors: ${clazz.getName} ===\n " + clazz.getConstructors.toList.mkString("\n ") - def methods(clazz: Class[_]): String = - s"=== Methods: ${clazz.getName} ===\n " + clazz.getMethods.toList.mkString("\n ") - def fields(clazz: Class[_]): String = - s"=== Fields: ${clazz.getName} ===\n " + clazz.getFields.toList.mkString("\n ") - def stack(clazz: Class[_]): String = - s"=== Superclass: ${clazz.getName} ===\n " + clazz.getSuperclass() + "\n" + - s"=== Interfaces: ${clazz.getName} ===\n " + clazz.getInterfaces.toList.mkString("\n ") - -} diff --git a/core/src/main/scala/co.blocke.scalajack/Performance.scalax b/core/src/main/scala/co.blocke.scalajack/Performance.scalax deleted file mode 100644 index dc894687..00000000 --- a/core/src/main/scala/co.blocke.scalajack/Performance.scalax +++ /dev/null @@ -1,106 +0,0 @@ -package co.blocke.scalajack - -import co.blocke.scala_reflection._ -import co.blocke.scala_reflection.info._ -import org.apache.commons.codec.binary.Base64 - -case class Person( - id: Int, - first_name: String, - last_name: String, - email: String, - gender: String, - ip_address: String) - - -/* -object Main { - - val people = - """[{"id":1,"first_name":"Kenneth","last_name":"Watson","email":"kwatson0@goo.ne.jp","gender":"Male","ip_address":"50.27.55.219"}, - {"id":2,"first_name":"Jason","last_name":"Peters","email":"jpeters1@tinypic.com","gender":"Male","ip_address":"152.156.120.235"}, - {"id":3,"first_name":"Beverly","last_name":"Stevens","email":"bstevens2@ustream.tv","gender":"Female","ip_address":"169.212.150.35"}, - {"id":4,"first_name":"Theresa","last_name":"Dixon","email":"tdixon3@hp.com","gender":"Female","ip_address":"137.214.192.32"}, - {"id":5,"first_name":"Michael","last_name":"Carr","email":"mcarr4@discovery.com","gender":"Male","ip_address":"244.152.168.54"}, - {"id":6,"first_name":"Carolyn","last_name":"Cruz","email":"ccruz5@nps.gov","gender":"Female","ip_address":"228.112.58.94"}, - {"id":7,"first_name":"Louis","last_name":"Alexander","email":"lalexander6@mapy.cz","gender":"Male","ip_address":"118.195.8.173"}, - {"id":8,"first_name":"Laura","last_name":"Campbell","email":"lcampbell7@google.ca","gender":"Female","ip_address":"125.91.1.1"}, - {"id":9,"first_name":"Judy","last_name":"Burke","email":"jburke8@furl.net","gender":"Female","ip_address":"153.45.26.242"}, - {"id":10,"first_name":"Earl","last_name":"Stevens","email":"estevens9@discovery.com","gender":"Male","ip_address":"172.161.173.238"}, - {"id":11,"first_name":"Rose","last_name":"Cooper","email":"rcoopera@lulu.com","gender":"Female","ip_address":"99.128.103.204"}, - {"id":12,"first_name":"Ashley","last_name":"Hawkins","email":"ahawkinsb@artisteer.com","gender":"Female","ip_address":"128.225.193.155"}, - {"id":13,"first_name":"Howard","last_name":"Harvey","email":"hharveyc@naver.com","gender":"Male","ip_address":"64.177.55.210"}, - {"id":14,"first_name":"Edward","last_name":"Ramos","email":"eramosd@is.gd","gender":"Male","ip_address":"208.65.154.100"}, - {"id":15,"first_name":"Jonathan","last_name":"Gonzalez","email":"jgonzaleze@walmart.com","gender":"Male","ip_address":"166.223.153.41"}, - {"id":16,"first_name":"Chris","last_name":"Reynolds","email":"creynoldsf@mail.ru","gender":"Male","ip_address":"183.239.230.178"}, - {"id":17,"first_name":"Helen","last_name":"Morales","email":"hmoralesg@vkontakte.ru","gender":"Female","ip_address":"19.89.226.60"}, - {"id":18,"first_name":"Tina","last_name":"Baker","email":"tbakerh@hubpages.com","gender":"Female","ip_address":"41.15.68.62"}, - {"id":19,"first_name":"Patricia","last_name":"Martin","email":"pmartini@booking.com","gender":"Female","ip_address":"98.67.244.69"}, - {"id":20,"first_name":"Rebecca","last_name":"Kelley","email":"rkelleyj@apple.com","gender":"Female","ip_address":"182.160.172.136"}, - {"id":21,"first_name":"Bonnie","last_name":"Carr","email":"bcarrk@jigsy.com","gender":"Female","ip_address":"73.181.196.21"}, - {"id":22,"first_name":"Harold","last_name":"Carter","email":"hcarterl@quantcast.com","gender":"Male","ip_address":"227.72.164.120"}, - {"id":23,"first_name":"Martha","last_name":"Barnes","email":"mbarnesm@skyrock.com","gender":"Female","ip_address":"46.162.4.230"}, - {"id":24,"first_name":"Martha","last_name":"Henderson","email":"mhendersonn@quantcast.com","gender":"Female","ip_address":"226.177.120.99"}, - {"id":25,"first_name":"Ashley","last_name":"Henderson","email":"ahendersono@buzzfeed.com","gender":"Female","ip_address":"159.212.195.202"}, - {"id":26,"first_name":"Sean","last_name":"Day","email":"sdayp@nationalgeographic.com","gender":"Male","ip_address":"32.29.74.112"}, - {"id":27,"first_name":"Mary","last_name":"Arnold","email":"marnoldq@sina.com.cn","gender":"Female","ip_address":"217.221.110.62"}, - {"id":28,"first_name":"Philip","last_name":"Pierce","email":"ppiercer@youtube.com","gender":"Male","ip_address":"170.222.96.245"}, - {"id":29,"first_name":"Johnny","last_name":"Gordon","email":"jgordons@themeforest.net","gender":"Male","ip_address":"229.207.75.169"}, - {"id":30,"first_name":"Julie","last_name":"Ruiz","email":"jruizt@jimdo.com","gender":"Female","ip_address":"209.193.34.42"}, - {"id":31,"first_name":"Benjamin","last_name":"Alvarez","email":"balvarezu@newsvine.com","gender":"Male","ip_address":"69.42.98.157"}, - {"id":32,"first_name":"Steve","last_name":"Marshall","email":"smarshallv@bizjournals.com","gender":"Male","ip_address":"135.55.106.6"}, - {"id":33,"first_name":"Aaron","last_name":"Diaz","email":"adiazw@friendfeed.com","gender":"Male","ip_address":"250.102.146.94"}, - {"id":34,"first_name":"Bonnie","last_name":"Fields","email":"bfieldsx@opera.com","gender":"Female","ip_address":"164.40.128.148"}, - {"id":35,"first_name":"Beverly","last_name":"Cunningham","email":"bcunninghamy@umn.edu","gender":"Female","ip_address":"4.128.182.77"}, - {"id":36,"first_name":"Juan","last_name":"Porter","email":"jporterz@nasa.gov","gender":"Male","ip_address":"171.157.112.131"}, - {"id":37,"first_name":"Donna","last_name":"Butler","email":"dbutler10@cdbaby.com","gender":"Female","ip_address":"126.95.247.209"}, - {"id":38,"first_name":"Richard","last_name":"Rivera","email":"rrivera11@irs.gov","gender":"Male","ip_address":"219.104.120.129"}, - {"id":39,"first_name":"Juan","last_name":"Hall","email":"jhall12@ftc.gov","gender":"Male","ip_address":"157.211.238.243"}, - {"id":40,"first_name":"Heather","last_name":"Lee","email":"hlee13@dailymail.co.uk","gender":"Female","ip_address":"10.153.241.206"}, - {"id":41,"first_name":"Rose","last_name":"Kennedy","email":"rkennedy14@bravesites.com","gender":"Female","ip_address":"200.54.196.76"}, - {"id":42,"first_name":"Russell","last_name":"Warren","email":"rwarren15@livejournal.com","gender":"Male","ip_address":"169.251.130.191"}, - {"id":43,"first_name":"Dennis","last_name":"Howell","email":"dhowell16@biglobe.ne.jp","gender":"Male","ip_address":"222.19.174.168"}, - {"id":44,"first_name":"Kimberly","last_name":"Wilson","email":"kwilson17@networksolutions.com","gender":"Female","ip_address":"13.139.193.159"}, - {"id":45,"first_name":"Sharon","last_name":"Jacobs","email":"sjacobs18@stanford.edu","gender":"Female","ip_address":"130.22.68.55"}, - {"id":46,"first_name":"Donald","last_name":"Nguyen","email":"dnguyen19@posterous.com","gender":"Male","ip_address":"0.115.100.139"}, - {"id":47,"first_name":"Brenda","last_name":"Stone","email":"bstone1a@senate.gov","gender":"Female","ip_address":"165.196.166.161"}, - {"id":48,"first_name":"Kelly","last_name":"Pierce","email":"kpierce1b@xrea.com","gender":"Female","ip_address":"73.180.74.227"}, - {"id":49,"first_name":"Sandra","last_name":"Murray","email":"smurray1c@princeton.edu","gender":"Female","ip_address":"211.149.35.132"}, - {"id":50,"first_name":"Alice","last_name":"Davis","email":"adavis1d@ow.ly","gender":"Female","ip_address":"4.124.35.181"}]""".stripMargin.asInstanceOf[co.blocke.scalajack.json.JSON] - - - val dj = ScalaJack() - - def main(args: Array[String]): Unit = { - - println(RType.of[SampleJNumber]) - - /* - - for(i <- 0 to 100) - dj.read[List[Person]](people) - - val n = 100000 - for (j <- 0 to 30) - println(s"Run $n iterations in ${run(n)} ms") - */ - - } - - def run(n:Int): Long = - val now = System.currentTimeMillis - for(i <- 0 to n) - dj.read[List[Person]](people) - val later = System.currentTimeMillis - later - now - - - -} -*/ - -// From reflection: -// ScalaCaseClassInfo(xxx.Thing,class xxx.Thing,List(),List(ScalaFieldInfo(0,t,ArrayInfo([[I,class [[I,ArrayInfo([I,class [I,Scala_Int)),Map(),public int[][] co.blocke.scala_reflection.Thing.t(),None)),List(),Map(),false) - -// From scalajack: -// ScalaCaseClassInfo(xxx.Thing,class xxx.Thing,List(),List(ScalaFieldInfo(0,t,ArrayInfo([[I,class scala.Array,ArrayInfo([I,class scala.Array,Scala_Int)),Map(),public int[][] co.blocke.scalajack.Thing.t(),None)),List(),Map(),false) diff --git a/core/src/main/scala/co.blocke.scalajack/SJCapture.scala b/core/src/main/scala/co.blocke.scalajack/SJCapture.scala deleted file mode 100644 index 963c241b..00000000 --- a/core/src/main/scala/co.blocke.scalajack/SJCapture.scala +++ /dev/null @@ -1,9 +0,0 @@ -package co.blocke.scalajack - -trait SJCapture { - var captured: java.util.HashMap[String,_] = new java.util.HashMap[String, Any]() - // var captured: java.util.HashMap[String,String] = new java.util.HashMap[String, String]() -} - -// Java classes should inherit this! -class SJCaptureJava extends SJCapture diff --git a/core/src/main/scala/co.blocke.scalajack/ScalaJack.scala b/core/src/main/scala/co.blocke.scalajack/ScalaJack.scala deleted file mode 100644 index ebcf3464..00000000 --- a/core/src/main/scala/co.blocke.scalajack/ScalaJack.scala +++ /dev/null @@ -1,20 +0,0 @@ -package co.blocke.scalajack - -import model._ -import json._ - -object ScalaJack: - def apply() = JsonFlavor() - def apply[S](kind: JackFlavor[S]): JackFlavor[S] = kind - - -class ScalaJackError(msg: String) extends Exception(msg) -class ScalaJackValueError(val value: Any, cause: Throwable) extends Exception(cause.getMessage) - -type HintBijective = util.BijectiveFunction[String, String] -val CHANGE_ANNO = "co.blocke.scalajack.Change" -val OPTIONAL_ANNO = "co.blocke.scalajack.Optional" -val IGNORE = "co.blocke.scalajack.Ignore" -val DB_KEY = "co.blocke.scalajack.DBKey" -val DB_COLLECTION = "co.blocke.scalajack.Collection" -val SJ_CAPTURE = "co.blocke.scalajack.SJCapture" \ No newline at end of file diff --git a/core/src/main/scala/co.blocke.scalajack/delimited/DelimitedEitherTypeAdapterFactory.scala b/core/src/main/scala/co.blocke.scalajack/delimited/DelimitedEitherTypeAdapterFactory.scala deleted file mode 100644 index 58312096..00000000 --- a/core/src/main/scala/co.blocke.scalajack/delimited/DelimitedEitherTypeAdapterFactory.scala +++ /dev/null @@ -1,46 +0,0 @@ -package co.blocke.scalajack -package delimited - -import model._ -import typeadapter.{EitherTypeAdapter, MaybeStringWrapTypeAdapter, StringWrapTypeAdapter} -import co.blocke.scala_reflection._ -import co.blocke.scala_reflection.info.EitherInfo -import co.blocke.scala_reflection.impl.Clazzes._ - -import scala.collection.mutable.Builder -import scala.util.{ Failure, Success, Try } - -/** - * The only reason this machinery exists for delimited either is that writes need to be string-wrapped, where they don't in the general/JSON case. - */ -object DelimitedEitherTypeAdapterFactory extends TypeAdapterFactory: - - def matches(concrete: RType): Boolean = - concrete match { - case _: EitherInfo => true - case _ => false - } - - def makeTypeAdapter(concrete: RType)(implicit taCache: TypeAdapterCache): TypeAdapter[_] = - val eitherInfo = concrete.asInstanceOf[EitherInfo] - val leftInfo = eitherInfo.leftType - val rightInfo = eitherInfo.rightType - - if( leftInfo.infoClass <:< rightInfo.infoClass || rightInfo.infoClass <:< leftInfo.infoClass) - throw new IllegalArgumentException( - s"Types ${leftInfo.name} and ${rightInfo.name} are not mutually exclusive" - ) - - val leftTypeAdapter = taCache.typeAdapterOf(leftInfo) match { - case ta if ta.isStringish => ta - case ta => MaybeStringWrapTypeAdapter(taCache.jackFlavor, ta) - } - val rightTypeAdapter = taCache.typeAdapterOf(rightInfo) match { - case ta if ta.isStringish => ta - case ta => MaybeStringWrapTypeAdapter(taCache.jackFlavor, ta) - } - - EitherTypeAdapter( - concrete, - leftTypeAdapter, - rightTypeAdapter) \ No newline at end of file diff --git a/core/src/main/scala/co.blocke.scalajack/delimited/DelimitedFlavor.scala b/core/src/main/scala/co.blocke.scalajack/delimited/DelimitedFlavor.scala deleted file mode 100644 index 9939d88f..00000000 --- a/core/src/main/scala/co.blocke.scalajack/delimited/DelimitedFlavor.scala +++ /dev/null @@ -1,72 +0,0 @@ -package co.blocke.scalajack -package delimited - -import model._ -import co.blocke.scala_reflection.RType -import typeadapter.{MaybeStringWrapTypeAdapter, StringWrapTypeAdapter} - -opaque type DELIMITED = String -val DELIM_PREFIX: Char = 2 - -case class DelimitedFlavor( - delimiter: Char = ',', - override val defaultHint: String = "_hint", - override val permissivesOk: Boolean = false, - override val customAdapters: List[TypeAdapterFactory] = List.empty[TypeAdapterFactory], - override val hintMap: Map[String, String] = Map.empty[String, String], - override val hintValueModifiers: Map[String, HintValueModifier] = Map.empty[String, HintValueModifier], - override val typeValueModifier: HintValueModifier = DefaultHintModifier, - override val parseOrElseMap: Map[Class[_], RType] = Map.empty[Class[_], RType], - override val enumsAsInt: Boolean = false -) extends JackFlavor[DELIMITED] { - - def _read[T](input: DELIMITED, typeAdapter: TypeAdapter[T]): T = - val parser = DelimitedParser(delimiter, s"$DELIM_PREFIX$input".asInstanceOf[DELIMITED], this) - typeAdapter.read(parser) - - def _render[T](t: T, typeAdapter: TypeAdapter[T]): DELIMITED = - val sb = StringBuilder[DELIMITED]() - typeAdapter.write(t, writer, sb) - sb.result().asInstanceOf[DELIMITED] - - def parse(input: DELIMITED): Parser = DelimitedParser(delimiter, input, this) - - private val writer = DelimitedWriter(delimiter) - - def allowPermissivePrimitives(): JackFlavor[DELIMITED] = - throw new ScalaJackError("Not available for delimited encoding") - def enumsAsInts(): JackFlavor[DELIMITED] = this.copy(enumsAsInt = true) - def parseOrElse(poe: (RType, RType)*): JackFlavor[DELIMITED] = - this.copy(parseOrElseMap = this.parseOrElseMap ++ poe.map{(p,oe) => p.infoClass->oe}) - def withAdapters(ta: TypeAdapterFactory*): JackFlavor[DELIMITED] = - throw new ScalaJackError("Not available for delimited encoding") - def withDefaultHint(hint: String): JackFlavor[DELIMITED] = - throw new ScalaJackError("Not available for delimited encoding") - def withHints(h: (RType, String)*): JackFlavor[DELIMITED] = - throw new ScalaJackError("Not available for delimited encoding") - def withHintModifiers(hm: (RType, HintValueModifier)*): JackFlavor[DELIMITED] = - throw new ScalaJackError("Not available for delimited encoding") - def withTypeValueModifier(tm: HintValueModifier): JackFlavor[DELIMITED] = - throw new ScalaJackError("Not available for delimited encoding") - - def stringWrapTypeAdapterFactory[T]( - wrappedTypeAdapter: TypeAdapter[T], - emptyStringOk: Boolean = true - ): TypeAdapter[T] = - StringWrapTypeAdapter(wrappedTypeAdapter) - // wrappedTypeAdapter // No-Op for delimited - - override def bakeCache(): TypeAdapterCache = { - val dads = super.bakeCache() - dads.copy( - factories = DelimitedEitherTypeAdapterFactory :: - DelimitedOptionTypeAdapterFactory :: dads.factories - ) - } - - def maybeStringWrapTypeAdapterFactory[T]( - wrappedTypeAdapter: TypeAdapter[T], - emptyStringOk: Boolean = true - ): TypeAdapter[T] = - MaybeStringWrapTypeAdapter(this, wrappedTypeAdapter, emptyStringOk) -} \ No newline at end of file diff --git a/core/src/main/scala/co.blocke.scalajack/delimited/DelimitedOptionTypeAdapterFactory.scala b/core/src/main/scala/co.blocke.scalajack/delimited/DelimitedOptionTypeAdapterFactory.scala deleted file mode 100644 index 87d3d30a..00000000 --- a/core/src/main/scala/co.blocke.scalajack/delimited/DelimitedOptionTypeAdapterFactory.scala +++ /dev/null @@ -1,31 +0,0 @@ -package co.blocke.scalajack -package delimited - -import typeadapter.{OptionTypeAdapter, JavaOptionalTypeAdapter} -import co.blocke.scala_reflection.RType -import co.blocke.scala_reflection.info.{OptionInfo, ScalaOptionInfo, JavaOptionalInfo, TypeSymbolInfo} - -import model._ - -/** - * Options are handled a little differently for Delimited. They should result in an empty field. - * Empty fields area always read in as None, so no null fields are possible for Delimited options. - */ -object DelimitedOptionTypeAdapterFactory extends TypeAdapterFactory: - - def matches(concrete: RType): Boolean = - concrete match { - case _: OptionInfo => true - case _ => false - } - - def makeTypeAdapter(concrete: RType)(implicit taCache: TypeAdapterCache): TypeAdapter[_] = - val optiBase = concrete.asInstanceOf[OptionInfo] - val wrapped = optiBase.optionParamType match { - case c: TypeSymbolInfo => throw new ScalaJackError(s"Unexpected non-concrete type in option: ${c.name}") - case c => taCache.typeAdapterOf(c) - } - concrete match { - case opti: ScalaOptionInfo => OptionTypeAdapter(concrete, wrapped, true) // Note nullAsNone = true here! - case jopti: JavaOptionalInfo => JavaOptionalTypeAdapter(concrete, wrapped, true) - } diff --git a/core/src/main/scala/co.blocke.scalajack/delimited/DelimitedParser.scala b/core/src/main/scala/co.blocke.scalajack/delimited/DelimitedParser.scala deleted file mode 100644 index 3b423360..00000000 --- a/core/src/main/scala/co.blocke.scalajack/delimited/DelimitedParser.scala +++ /dev/null @@ -1,221 +0,0 @@ -package co.blocke.scalajack -package delimited - -import model._ -import scala.collection.mutable -import typeadapter.classes.ClassTypeAdapterBase -import co.blocke.scala_reflection.info.TypeMemberInfo - -case class DelimitedParser( - delimChar: Char, - input: DELIMITED, - jackFlavor: JackFlavor[DELIMITED]) - extends Parser { - type WIRE = DELIMITED - - private var pos: Int = 0 - private val delimPrefixString = DELIM_PREFIX.toString - - val (tokens, indexes) = { - val dChars: Array[Char] = input.asInstanceOf[String].toCharArray - var i = 0 - val maxChars: Int = dChars.length - val tokenList = scala.collection.mutable.ListBuffer.empty[String] - val indexList = scala.collection.mutable.ListBuffer.empty[Int] - while (i < maxChars) { - var inQuotes = false - var done = false - val acc = new java.lang.StringBuilder() - indexList += i - while (i < maxChars && !done) { - dChars(i) match { - case DELIM_PREFIX => - acc.append(delimPrefixString) - done = true - case this.delimChar if !inQuotes => - done = true - case '"' if !inQuotes && i + 1 < maxChars && dChars(i + 1) != '"' => - inQuotes = true - i += 1 - case '"' if inQuotes && (i + 1 == maxChars || dChars(i + 1) != '"') => - inQuotes = false - i += 1 - done = true - case '"' if i + 1 < maxChars && dChars(i + 1) == '"' => - acc.append(dChars(i)) - i += 2 - case _ => // do nothing - acc.append(dChars(i)) - i += 1 - } - } - tokenList += acc.toString - if (i < maxChars) i += 1 // skip delimiter - } - if (i > 0 && dChars(i - 1) == delimChar) { - tokenList += "" - indexList += i - } - (tokenList.toList, indexList.toList) - } - - val max: Int = tokens.size - - @inline def isNumberChar(char: Char): Boolean = - ('0' <= char && char <= '9') || char == '.' || char == 'e' || char == 'E' || char == '-' || char == '+' - - def expectString(nullOK: Boolean = true): String = - if (pos < max) { - val ret = tokens(pos) - pos += 1 - ret - } else throw new ScalaJackError(showError("Attempt to read beyond input")) - - def expectList[K, TO]( - KtypeAdapter: TypeAdapter[K], - builder: mutable.Builder[K, TO]): TO = - expectString() match { - case "" => null.asInstanceOf[TO] - case listStr => - val subParser = DelimitedParser(delimChar, listStr.asInstanceOf[DELIMITED], jackFlavor) - while (subParser.pos < subParser.max) builder += KtypeAdapter.read( - subParser - ) - builder.result() - } - - def expectNumber(nullsOK: Boolean = false): String = - expectString() match { - // $COVERAGE-OFF$Never called--nulls caught in CollectionTypeAdapter before coming here. Left here as a safety - case "" if nullsOK => null - case "" => - backspace() - throw new ScalaJackError(showError("Expected a Number here")) - // $COVERAGE-ON$ - case candidate => - candidate.toCharArray.find(c => !isNumberChar(c)) match { - case None => candidate - case Some(_) => - backspace() - throw new ScalaJackError(showError("Expected a Number here")) - } - } - - def expectBoolean(): Boolean = - expectString() match { - case "true" => true - case "false" => false - case _ => - backspace() - throw new ScalaJackError(showError("Expected a Boolean here")) - } - - def expectTuple( - tupleFieldTypeAdapters: List[TypeAdapter[_]] - ): List[Object] = - expectString() match { - // $COVERAGE-OFF$Can't test--can't validly tell whether "" is escaped '"' or empty value in this context. Blame CSV - case "" => null.asInstanceOf[List[Object]] - // $COVERAGE-ON$ - case listStr => - val subParser = DelimitedParser(delimChar, listStr.asInstanceOf[DELIMITED], jackFlavor) - tupleFieldTypeAdapters.map { _ match { - case ccta: Classish => ccta.read( DelimitedParser(delimChar, subParser.expectString().asInstanceOf[DELIMITED], jackFlavor)) - case ta => ta.read(subParser) - }}.asInstanceOf[List[Object]] - } - - def expectMap[K, V, TO]( - keyTypeAdapter: TypeAdapter[K], - valueTypeAdapter: TypeAdapter[V], - builder: mutable.Builder[(K, V), TO]): TO = - throw new ScalaJackError(showError("No Map support for delimited data.")) // No map support in delimited format - - def expectObject( - classBase: ClassTypeAdapterBase[_], - hintLabel: String - ): (mutable.BitSet, List[Object], java.util.HashMap[String, _]) = { - if (!classBase.isCaseClass) - throw new ScalaJackError( - showError( - "Only case classes with non-empty constructors are supported for delimited data." - ) - ) - val fieldBits = mutable.BitSet() - - val args = classBase.argsTemplate.clone() - classBase.orderedFieldNames.foreach { name => - val oneField = classBase.fieldMembersByName(name) - val valueRead = oneField.valueTypeAdapter match { - case ccta: typeadapter.classes.CaseClassTypeAdapter[_] => - expectString() match { - case "" => null - case listStr => - val subParser = DelimitedParser(delimChar, listStr.asInstanceOf[DELIMITED], jackFlavor) - ccta.read(subParser) - } - case ta => ta.read(this) - } - valueRead match { - case None => - case null if oneField.info.defaultValue.isDefined => - case _ => - args(oneField.info.index) = valueRead.asInstanceOf[Object] - fieldBits += oneField.info.index - } - } - (fieldBits, args.toList, new java.util.HashMap[String, String]()) - } - - def showError(msg: String): String = { - val inputStr = input.asInstanceOf[String].drop(1) // Account for prefix char on input - val (clip, dashes) = indexes(pos) - 1 match { - case ep if ep <= 50 && max < 80 => (inputStr, ep) - case ep if ep <= 50 => (inputStr.substring(0, 77) + "...", ep) - case ep if ep > 50 && ep + 30 >= max => - ("..." + inputStr.substring(indexes(pos) - 50), 52) - case ep => ("..." + inputStr.substring(ep - 49, ep + 27) + "...", 52) - } - msg + "\n" + clip.replaceAll("[\n\t]", "~") + "\n" + ("-" * dashes) + "^" - } - - def peekForNull: Boolean = { - val isNull = pos == max || tokens(pos) == "" || (tokens(pos) == delimPrefixString && pos == max - 1) - if (pos < max && isNull || pos < max && tokens(pos) == delimPrefixString) - pos += 1 - isNull - } - - // $COVERAGE-OFF$No meaning for delimited input - def resolveTypeMembers( - typeMembersByName: Map[String, TypeMemberInfo], - converterFn: HintBijective - ): Map[String, TypeMemberInfo] = - throw new ScalaJackError( - showError("DelimitedFlavor does not support classes with type members") - ) - - def scanForHint(hint: String, converterFn: HintBijective): Class[_] = - throw new ScalaJackError( - showError("DelimitedFlavor does not support traits") - ) - - def nextIsObject: Boolean = false - def nextIsArray: Boolean = false - def nextIsBoolean: Boolean = false - def sourceAsString: String = input.asInstanceOf[String] - // $COVERAGE-ON$ - - def backspace(): Unit = pos -= 1 - def mark(): Int = pos - def revertToMark(mark: Int): Unit = pos = mark - def nextIsString: Boolean = true - def nextIsNumber: Boolean = { - val save = pos - val isValidNumber = expectString().toCharArray.forall(c => isNumberChar(c)) - pos = save - isValidNumber - } - def subParser(input: DELIMITED): Parser = - DelimitedParser(delimChar, input, jackFlavor) -} \ No newline at end of file diff --git a/core/src/main/scala/co.blocke.scalajack/delimited/DelimitedWriter.scala b/core/src/main/scala/co.blocke.scalajack/delimited/DelimitedWriter.scala deleted file mode 100644 index fe3513ae..00000000 --- a/core/src/main/scala/co.blocke.scalajack/delimited/DelimitedWriter.scala +++ /dev/null @@ -1,109 +0,0 @@ -package co.blocke.scalajack -package delimited - -import model._ -import co.blocke.scalajack.model - -import scala.collection.Map -import scala.collection.mutable - -case class DelimitedWriter(delimiter: Char) extends Writer[DELIMITED] { - - def writeArray[Elem](t: Iterable[Elem], elemTypeAdapter: TypeAdapter[Elem], out: mutable.Builder[DELIMITED, DELIMITED]): Unit = - if (t != null) { - val sb = model.StringBuilder[DELIMITED]() - val iter = t.iterator - while (iter.hasNext) { - elemTypeAdapter.write(iter.next, this, sb) - if (iter.hasNext) - sb += delimiter.toString.asInstanceOf[DELIMITED] - } - writeString(sb.result().asInstanceOf[String], out) - } - - def writeBigInt(t: BigInt, out: mutable.Builder[DELIMITED, DELIMITED]): Unit = - out += t.toString.asInstanceOf[DELIMITED] - def writeBoolean(t: Boolean, out: mutable.Builder[DELIMITED, DELIMITED]): Unit = - out += t.toString.asInstanceOf[DELIMITED] - def writeDecimal(t: BigDecimal, out: mutable.Builder[DELIMITED, DELIMITED]): Unit = - out += t.toString.asInstanceOf[DELIMITED] - def writeDouble(t: Double, out: mutable.Builder[DELIMITED, DELIMITED]): Unit = - out += t.toString.asInstanceOf[DELIMITED] - def writeInt(t: Int, out: mutable.Builder[DELIMITED, DELIMITED]): Unit = - out += t.toString.asInstanceOf[DELIMITED] - def writeLong(t: Long, out: mutable.Builder[DELIMITED, DELIMITED]): Unit = - out += t.toString.asInstanceOf[DELIMITED] - - def writeMap[Key, Value, To](t: Map[Key, Value], keyTypeAdapter: TypeAdapter[Key], valueTypeAdapter: TypeAdapter[Value], out: mutable.Builder[DELIMITED, DELIMITED]): Unit = - throw new ScalaJackError( - "Map-typed data is not supported for delimited output" - ) - - def writeNull(out: mutable.Builder[DELIMITED, DELIMITED]): Unit = {} // write empty field - - def writeObject[T]( - t: T, - orderedFieldNames: List[String], - fieldMembersByName: Map[String, ClassFieldMember[_,_]], - out: mutable.Builder[DELIMITED, DELIMITED], - extras: List[(String, ExtraFieldValue[_])] = List.empty[(String, ExtraFieldValue[_])] - ): Unit = - if (t != null) { - var first = true - orderedFieldNames.foreach { name => - if (first) - first = false - else - out += delimiter.toString.asInstanceOf[DELIMITED] - val f = fieldMembersByName(name) - f.valueTypeAdapter match { - case ta if ta.isInstanceOf[Classish] => - val sb = model.StringBuilder[DELIMITED]() - ta.castAndWrite(f.info.valueOf(t), this, sb) - writeString(sb.result().asInstanceOf[String], out) - case ta => - ta.castAndWrite(f.info.valueOf(t), this, out) - } - } - } - - def writeString(t: String, out: mutable.Builder[DELIMITED, DELIMITED]): Unit = - if (t != null) { - val t0 = t.replaceAll("\"", "\"\"") - val toWrite = - if (t0 != t || t0.contains(delimiter)) - "\"" + t0 + "\"" - else - t - out += toWrite.asInstanceOf[DELIMITED] - } - - // $COVERAGE-OFF$Never called for delimited output - def writeRaw(t: DELIMITED, out: mutable.Builder[DELIMITED, DELIMITED]): Unit = - writeString(t.asInstanceOf[String], out) - // $COVERAGE-ON$ - - def writeTuple[T]( - t: T, - writeFn: (Product) => List[(TypeAdapter[_], Any)], - out: mutable.Builder[DELIMITED, DELIMITED] - ): Unit = { - var first = true - val sb = model.StringBuilder[DELIMITED]() - writeFn(t.asInstanceOf[Product]).foreach { (fieldTA, fieldValue) => - if (first) - first = false - else - sb += delimiter.toString.asInstanceOf[DELIMITED] - fieldTA match { - case cta: Classish => - val sb2 = model.StringBuilder[DELIMITED]() - fieldTA.castAndWrite(fieldValue, this, sb2) - writeString(sb2.result().asInstanceOf[String], sb) - case ta => - fieldTA.castAndWrite(fieldValue, this, sb) - } - } - writeString(sb.result().asInstanceOf[String], out) - } -} diff --git a/core/src/main/scala/co.blocke.scalajack/json/JsonFlavor.scala b/core/src/main/scala/co.blocke.scalajack/json/JsonFlavor.scala deleted file mode 100644 index c98d3d43..00000000 --- a/core/src/main/scala/co.blocke.scalajack/json/JsonFlavor.scala +++ /dev/null @@ -1,63 +0,0 @@ -package co.blocke.scalajack -package json - -import model._ -import co.blocke.scala_reflection.RType -import typeadapter.{MaybeStringWrapTypeAdapter, StringWrapTypeAdapter} - -opaque type JSON = String - -case class JsonFlavor( - override val defaultHint: String = "_hint", - override val permissivesOk: Boolean = false, - override val customAdapters: List[TypeAdapterFactory] = List.empty[TypeAdapterFactory], - override val hintMap: Map[String, String] = Map.empty[String, String], - override val hintValueModifiers: Map[String, HintValueModifier] = Map.empty[String, HintValueModifier], - override val typeValueModifier: HintValueModifier = DefaultHintModifier, - override val parseOrElseMap: Map[Class[_], RType] = Map.empty[Class[_], RType], - override val enumsAsInt: Boolean = false -) extends JackFlavor[JSON] { - - def _read[T](input: JSON, typeAdapter: TypeAdapter[T]): T = - val parser = JsonParser(input, this) - typeAdapter.read(parser).asInstanceOf[T] - - def _render[T](t: T, typeAdapter: TypeAdapter[T]): JSON = - val sb = StringBuilder[JSON]() - typeAdapter.write(t, writer, sb) - sb.result() - - def parse(input: JSON): Parser = JsonParser(input, this) - - private val writer = JsonWriter() - - override val stringifyMapKeys: Boolean = true - - def allowPermissivePrimitives(): JackFlavor[JSON] = - this.copy(permissivesOk = true) - def enumsAsInts(): JackFlavor[JSON] = this.copy(enumsAsInt = true) - def parseOrElse(poe: (RType, RType)*): JackFlavor[JSON] = - this.copy(parseOrElseMap = this.parseOrElseMap ++ poe.map{(p,oe) => p.infoClass->oe}) - def withAdapters(ta: TypeAdapterFactory*): JackFlavor[JSON] = - this.copy(customAdapters = this.customAdapters ++ ta.toList) - def withDefaultHint(hint: String): JackFlavor[JSON] = - this.copy(defaultHint = hint) - def withHints(h: (RType, String)*): JackFlavor[JSON] = - this.copy(hintMap = this.hintMap ++ h.map{(rt,hint) => rt.name->hint}) - def withHintModifiers(hm: (RType, HintValueModifier)*): JackFlavor[JSON] = - this.copy(hintValueModifiers = this.hintValueModifiers ++ hm.map{(rt,hintM) => rt.name->hintM}) - def withTypeValueModifier(tm: HintValueModifier): JackFlavor[JSON] = - this.copy(typeValueModifier = tm) - - def stringWrapTypeAdapterFactory[T]( - wrappedTypeAdapter: TypeAdapter[T], - emptyStringOk: Boolean = true - ): TypeAdapter[T] = - StringWrapTypeAdapter(wrappedTypeAdapter, emptyStringOk) - - def maybeStringWrapTypeAdapterFactory[T]( - wrappedTypeAdapter: TypeAdapter[T], - emptyStringOk: Boolean = true - ): TypeAdapter[T] = - MaybeStringWrapTypeAdapter(this, wrappedTypeAdapter, emptyStringOk) -} diff --git a/core/src/main/scala/co.blocke.scalajack/json/JsonParser.scala b/core/src/main/scala/co.blocke.scalajack/json/JsonParser.scala deleted file mode 100644 index 6ade936e..00000000 --- a/core/src/main/scala/co.blocke.scalajack/json/JsonParser.scala +++ /dev/null @@ -1,428 +0,0 @@ -package co.blocke.scalajack -package json - -import model._ -import typeadapter.classes.ClassTypeAdapterBase -import co.blocke.scala_reflection._ -import co.blocke.scala_reflection.info.TypeMemberInfo - -import scala.collection.mutable -import scala.jdk.CollectionConverters._ - -case class JsonParser(jsRaw: JSON, jackFlavor: JackFlavor[JSON]) extends Parser { - - type WIRE = JSON - - private val js = jsRaw.asInstanceOf[String] - - private val jsChars: Array[Char] = js.toCharArray - private var i = 0 - private val max: Int = jsChars.length - - @inline def whitespace(): Unit = - while (i < max && jsChars(i).isWhitespace) i += 1 - @inline def nullCheck(): Boolean = - jsChars(i) == 'n' && i + 4 <= max && js.substring(i, i + 4) == "null" - - def backspace(): Unit = i -= 1 - - def expectString(nullOK: Boolean = true): String = { - if (nullOK && nullCheck()) { - i += 4 - null - } else if (jsChars(i) == '"') { - i += 1 - val mark = i - var captured: Option[String] = None - while (i < max && jsChars(i) != '"') { - if (jsChars(i) == '\\') { // Oops! Special char found. Reset and try again while capturing/translating special chars - i = mark - captured = Some(_expectString()) - } else - i += 1 - } - i += 1 - captured.getOrElse(js.substring(mark, i - 1)) - } else - throw new ScalaJackError(showError("Expected a String here")) - } - - // Slower "capture" version for when we discover embedded special chars that need translating, i.e. we - // can't do a simple substring within quotes. - private def _expectString(): String = { - val builder = new java.lang.StringBuilder() - while (i < max && jsChars(i) != '"') { - if (jsChars(i) == '\\') { - jsChars(i + 1) match { - case '"' => - builder.append('\"') - i += 2 - - case '\\' => - builder.append('\\') - i += 2 - - case 'b' => - builder.append('\b') - i += 2 - - case 'f' => - builder.append('\f') - i += 2 - - case 'n' => - builder.append('\n') - i += 2 - - case 'r' => - builder.append('\r') - i += 2 - - case 't' => - builder.append('\t') - i += 2 - - case 'u' => - val hexEncoded = js.substring(i + 2, i + 6) - val unicodeChar = Integer.parseInt(hexEncoded, 16).toChar - builder.append(unicodeChar.toString) - i += 6 - - case c => - builder.append(c) - i += 2 - } - } else { - builder.append(jsChars(i)) - i += 1 - } - } - builder.toString - } - - def expectBoolean(): Boolean = - if (i + 4 <= max && js.substring(i, i + 4) == "true") { - i += 4 - true - } else if (i + 5 <= max && js.substring(i, i + 5) == "false") { - i += 5 - false - } else - throw new ScalaJackError(showError("Expected a Boolean here")) - - @inline def isNumberChar(char: Char): Boolean = - ('0' <= char && char <= '9') || char == '.' || char == 'e' || char == 'E' || char == '-' || char == '+' - - def expectNumber(nullOK: Boolean = false): String = { - val mark = i - nullCheck() match { - case true if nullOK => - i += 4 - null - case true => - throw new ScalaJackError(showError("Expected a Number here")) - case false => - while (i < max && isNumberChar(jsChars(i))) i += 1 - if (mark == i) - throw new ScalaJackError(showError("Expected a Number here")) - else if (i == max || "\t\n ,}]".contains(jsChars(i))) - js.substring(mark, i) - else - throw new ScalaJackError(showError("Expected a Number here")) - } - } - - def expectList[E, TO](elemTypeAdapter: TypeAdapter[E], builder: mutable.Builder[E, TO]): TO = { - if (jsChars(i) != '[') - throw new ScalaJackError(showError("Expected start of list here")) - i += 1 - var first = true - while (i < max && jsChars(i) != ']') { - whitespace() - if (!first) { - if (jsChars(i) != ',') - throw new ScalaJackError(showError("Expected comma here")) - else - i += 1 // skip ',' - whitespace() - } else - first = false - builder += elemTypeAdapter.read(this) // Parse next item! - whitespace() - } - if (i == max || jsChars(i) != ']') - throw new ScalaJackError(showError("Expected end of list here")) - i += 1 - builder.result() - } - - def expectTuple( - tupleFieldTypeAdapters: List[TypeAdapter[_]] - ): List[Object] = { - if (i == max || jsChars(i) != '[') - throw new ScalaJackError(showError("Expected start of tuple here")) - i += 1 - var first = true - val result = tupleFieldTypeAdapters.map { fieldTypeAdapter => - whitespace() - if (!first) { - if (i == max || jsChars(i) != ',') - throw new ScalaJackError(showError("Expected comma here")) - else - i += 1 // skip ',' - whitespace() - } else - first = false - fieldTypeAdapter.read(this) - } - if (i == max || jsChars(i) != ']') - throw new ScalaJackError(showError("Expected end of tuple here")) - i += 1 - result.asInstanceOf[List[Object]] - } - - def expectMap[K, V, TO](keyTypeAdapter: TypeAdapter[K], valueTypeAdapter: TypeAdapter[V], builder: mutable.Builder[(K, V), TO]): TO = { - whitespace() - if (jsChars(i) != '{') - throw new ScalaJackError(showError("Expected start of object here")) - i += 1 - var first = true - while (i < max && jsChars(i) != '}') { - whitespace() - if (!first) { - if (i == max || jsChars(i) != ',') - throw new ScalaJackError(showError("Expected comma here")) - else - i += 1 // skip ',' - whitespace() - } else - first = false - val key = keyTypeAdapter.read(this) - if (key == null) - throw new ScalaJackError(showError("Map keys cannot be null")) - whitespace() - if (i == max || jsChars(i) != ':') - throw new ScalaJackError(showError("Expected colon here")) - i += 1 - whitespace() - val value = valueTypeAdapter.read(this) - whitespace() - builder += ((key, value)) - } - if (i == max || jsChars(i) != '}') - throw new ScalaJackError(showError("Expected end of object here")) - i += 1 - builder.result() - } - - def expectObject( - classBase: ClassTypeAdapterBase[_], - hintLabel: String - ): (mutable.BitSet, List[Object], java.util.HashMap[String, String]) = { - whitespace() - val args = classBase.argsTemplate.clone() - val fieldBits = mutable.BitSet() - val captured = - if (classBase.isSJCapture) - new java.util.HashMap[String, String]() - else - null - if (i == max || jsChars(i) != '{') - throw new ScalaJackError(showError("Expected start of object here")) - i += 1 - var first = true - while (i < max && jsChars(i) != '}') { - whitespace() - if (!first) { - if (i == max || jsChars(i) != ',') - throw new ScalaJackError(showError("Expected comma here")) - else - i += 1 // skip ',' - whitespace() - } else - first = false - val key = expectString(false) - whitespace() - if (i == max || jsChars(i) != ':') - throw new ScalaJackError(showError("Expected colon here")) - i += 1 - classBase.fieldMembersByName.get(key) match { - case Some(field) => - whitespace() - fieldBits += field.info.index - args(field.info.index) = field.valueTypeAdapter.read(this).asInstanceOf[Object] - case None => // found some input field not present in class - val mark = i - skipOverElement() - if (classBase.isSJCapture && key != hintLabel) - captured.put(key, js.substring(mark, i)) - } - whitespace() - } - if (i == max || jsChars(i) != '}') - throw new ScalaJackError(showError("Expected end of object here")) - i += 1 - (fieldBits, args.toList, captured) - } - - private def skipString(): Unit = { - i += 1 - while (i < max && jsChars(i) != '"') { - if (jsChars(i) == '\\') i += 1 - i += 1 - } - i += 1 - } - - private def skipOverElement(): Unit = { - whitespace() - jsChars(i) match { - case '[' => - var level = 0 - i += 1 - while (i < max && level >= 0) { - jsChars(i) match { - case '[' => - level += 1 - i += 1 - case '"' => skipString() - case ']' => - level -= 1 - i += 1 - case _ => - i += 1 - } - } - case '{' => - var level = 0 - i += 1 - while (i < max && level >= 0) jsChars(i) match { - case '{' => - level += 1 - i += 1 - case '"' => skipString() - case '}' => - level -= 1 - i += 1 - case _ => - i += 1 - } - case '"' => skipString() - case _ => // "naked" value: null, number, boolean - while (i < max && jsChars(i) != ',' && jsChars(i) != '}' && jsChars(i) != ']') i += 1 - } - } - - def peekForNull: Boolean = - if (nullCheck()) { - i += 4 - true - } else false - - // NOTE: Expectation here is we're sitting on beginning of object, '{'. This is called from TraitTypeAdapter - def scanForHint(hint: String, converterFn: HintBijective): Class[_] = { - val mark = i - whitespace() - if (i == max || jsChars(i) != '{') - throw new ScalaJackError(showError("Expected start of object here")) - i += 1 // skip over { - var done = false - while (!done) { - whitespace() - val key = expectString() - whitespace() - if (i == max || jsChars(i) != ':') - throw new ScalaJackError(showError("Expected ':' here")) - i += 1 // skip ':' - if (key == hint) { - whitespace() - done = true - } else { - skipOverElement() - whitespace() - jsChars(i) match { - case ',' => i += 1 // skip ',' - case '}' => - throw new ScalaJackError(showError(s"Type hint '$hint' not found")) - case _ => - throw new ScalaJackError(showError("Unexpected character found")) - } - } - } - val rawHintString = expectString() - val hintType = try { - converterFn.apply(rawHintString) - } catch { - case t: Throwable => - i -= 1 - throw new ScalaJackError( - showError(s"Couldn't marshal class for $rawHintString") - ) - } - i = mark // we found hint, but go back to parse object - Class.forName(hintType) - } - - def resolveTypeMembers( - typeMembersByName: Map[String, TypeMemberInfo], - converterFn: HintBijective - ): Map[String, TypeMemberInfo] = { - val mark = i - whitespace() - if (i == max || jsChars(i) != '{') - throw new ScalaJackError(showError("Expected start of object here")) - val collected = new java.util.HashMap[String, TypeMemberInfo]() - i += 1 // skip over { - var done = false - while (!done) { - whitespace() - val key = expectString() - whitespace() - if (i == max || jsChars(i) != ':') - throw new ScalaJackError(showError("Expected ':' here")) - i += 1 // skip ':' - if (typeMembersByName.contains(key)) { - whitespace() - collected.put( - key, - TypeMemberInfo(key, typeMembersByName(key).typeSymbol, RType.of(Class.forName(converterFn.apply(expectString())))) - ) - } else - skipOverElement() - whitespace() - jsChars(i) match { - case ',' => i += 1 // skip ',' - case '}' => done = true - case _ => - throw new ScalaJackError(showError("Unexpected character found")) - } - } - i = mark // go back to parse object - collected.asScala.toMap - } - - def showError(msg: String): String = { - val (clip, dashes) = i match { - case ep if ep <= 50 && max < 80 => (js, ep) - case ep if ep <= 50 => (js.substring(0, 77) + "...", ep) - case ep if ep > 50 && ep + 30 >= max => - ("..." + js.substring(i - 49), 52) - case ep => ("..." + js.substring(ep - 49, ep + 27) + "...", 52) - } - msg + "\n" + clip.replaceAll("[\n\t]", "~") + "\n" + ("-" * dashes) + "^" - } - - def mark(): Int = i - def revertToMark(mark: Int): Unit = i = mark - - def nextIsString: Boolean = nullCheck() || jsChars(i) == '"' - def nextIsNumber: Boolean = isNumberChar(jsChars(i)) - def nextIsObject: Boolean = nullCheck() || jsChars(i) == '{' - def nextIsArray: Boolean = nullCheck() || jsChars(i) == '[' - def nextIsBoolean: Boolean = - (i + 4 <= max && js.substring(i, i + 4) == "true") || (i + 5 <= max && js - .substring(i, i + 5) == "false") - def sourceAsString: String = js - - def subParser(input: JSON): Parser = JsonParser(input, jackFlavor) -} diff --git a/core/src/main/scala/co.blocke.scalajack/json/JsonWriter.scala b/core/src/main/scala/co.blocke.scalajack/json/JsonWriter.scala deleted file mode 100644 index 6629d719..00000000 --- a/core/src/main/scala/co.blocke.scalajack/json/JsonWriter.scala +++ /dev/null @@ -1,188 +0,0 @@ -package co.blocke.scalajack -package json - -import model._ -import scala.collection.mutable -import scala.collection.Map - -case class JsonWriter() extends Writer[JSON] { - - @inline def addString(s: String, out: mutable.Builder[JSON, JSON]): Unit = - out += s.asInstanceOf[JSON] - - def writeArray[Elem](t: Iterable[Elem], elemTypeAdapter: TypeAdapter[Elem], out: mutable.Builder[JSON, JSON]): Unit = t match { - case null => addString("null", out) - case a => - out += "[".asInstanceOf[JSON] - val iter = a.iterator - while (iter.hasNext) { - elemTypeAdapter.write(iter.next, this, out) - if (iter.hasNext) - out += ",".asInstanceOf[JSON] - } - out += "]".asInstanceOf[JSON] - } - - def writeBigInt(t: BigInt, out: mutable.Builder[JSON, JSON]): Unit = - addString(t.toString, out) - - def writeBoolean(t: Boolean, out: mutable.Builder[JSON, JSON]): Unit = - addString(t.toString, out) - - def writeDecimal(t: BigDecimal, out: mutable.Builder[JSON, JSON]): Unit = - t match { - case null => addString("null", out) - case s => addString(s.toString, out) - } - - def writeDouble(t: Double, out: mutable.Builder[JSON, JSON]): Unit = - addString(t.toString, out) - - def writeInt(t: Int, out: mutable.Builder[JSON, JSON]): Unit = - addString(t.toString, out) - - def writeLong(t: Long, out: mutable.Builder[JSON, JSON]): Unit = - addString(t.toString, out) - - def writeMap[Key, Value, To](t: Map[Key, Value], keyTypeAdapter: TypeAdapter[Key], valueTypeAdapter: TypeAdapter[Value], out: mutable.Builder[JSON, JSON]): Unit = - t match { - case null => addString("null", out) - case daMap => - out += "{".asInstanceOf[JSON] - var first = true - daMap.foreach { - case (key, value) => - if (first) - first = false - else - out += ",".asInstanceOf[JSON] - if (key == null) - throw new ScalaJackError("Map keys cannot be null.") - keyTypeAdapter.write(key, this, out) - out += ":".asInstanceOf[JSON] - valueTypeAdapter.write(value, this, out) - } - out += "}".asInstanceOf[JSON] - } - - def writeString(t: String, out: mutable.Builder[JSON, JSON]): Unit = - t match { - case null => addString("null", out) - case _: String => - out += "\"".asInstanceOf[JSON] - var i = 0 - val length = t.length - val chars = t.toCharArray - - while (i < length) { - chars(i) match { - case '"' => addString("""\"""", out) - case '\\' => addString("""\\""", out) - case '\b' => addString("""\b""", out) - case '\f' => addString("""\f""", out) - case '\n' => addString("""\n""", out) - case '\r' => addString("""\r""", out) - case '\t' => addString("""\t""", out) - case ch if ch < 32 || ch >= 128 => - addString("""\""" + "u" + "%04x".format(ch.toInt), out) - case c => out += c.toString.asInstanceOf[JSON] - } - - i += 1 - } - out += "\"".asInstanceOf[JSON] - } - - def writeRaw(t: JSON, out: mutable.Builder[JSON, JSON]): Unit = - addString(t.asInstanceOf[String], out) - - def writeNull(out: mutable.Builder[JSON, JSON]): Unit = - addString("null", out) - - @inline private def writeFields( - isFirst: Boolean, - fields: List[(String, Any, TypeAdapter[Any])], - out: mutable.Builder[JSON, JSON] - ): Boolean = { - var first = isFirst - for ((label, value, valueTypeAdapter) <- fields) - value match { - case None => // do nothing (skip this field) - case o: java.util.Optional[_] if !o.isPresent => // do nothing (skip this field) - case _ => - if (first) - first = false - else - out += ",".asInstanceOf[JSON] - writeString(label, out) - out += ":".asInstanceOf[JSON] - valueTypeAdapter.write(value, this, out) - } - first - } - - def writeObject[T]( - t: T, - orderedFieldNames: List[String], - fieldMembersByName: Map[String, ClassFieldMember[_,_]], - out: mutable.Builder[JSON, JSON], - extras: List[(String, ExtraFieldValue[_])] - ): Unit = { - if (t == null) { - addString("null", out) - } else { - out += "{".asInstanceOf[JSON] - val wasFirst = writeFields( - isFirst = true, - extras.map{ - e => - ( - e._1, - e._2.value, - e._2.valueTypeAdapter.asInstanceOf[TypeAdapter[Any]] - ) - }, - out - ) - val wasFirst2 = writeFields( - wasFirst, - orderedFieldNames - .map { fieldName => // Strictly-speaking JSON has no order, but it's clean to write out in constructor order. - val oneField = fieldMembersByName(fieldName) - (fieldName, oneField.info.valueOf(t), oneField.valueTypeAdapter.asInstanceOf[TypeAdapter[Any]]) - }, - out - ) - t match { - case sjc: SJCapture => - import scala.jdk.CollectionConverters._ - var first = wasFirst2 - sjc.captured.asScala.foreach { - case (field, fvalue) => - if (first) - first = false - else - out += ",".asInstanceOf[JSON] - writeString(field, out) - out += ":".asInstanceOf[JSON] - out += fvalue.asInstanceOf[JSON] // all json captured things are String - } - case _ => - } - out += "}".asInstanceOf[JSON] - } - } - - def writeTuple[T](t: T, writeFn: (Product) => List[(TypeAdapter[_], Any)], out: mutable.Builder[JSON, JSON]): Unit = { - out += "[".asInstanceOf[JSON] - var first = true - writeFn(t.asInstanceOf[Product]).foreach { (fieldTA, fieldValue) => - if (first) - first = false - else - out += ",".asInstanceOf[JSON] - fieldTA.castAndWrite( fieldValue, this, out ) - } - out += "]".asInstanceOf[JSON] - } -} \ No newline at end of file diff --git a/core/src/main/scala/co.blocke.scalajack/json4s/JValueBuilder.scala b/core/src/main/scala/co.blocke.scalajack/json4s/JValueBuilder.scala deleted file mode 100644 index fcc701a1..00000000 --- a/core/src/main/scala/co.blocke.scalajack/json4s/JValueBuilder.scala +++ /dev/null @@ -1,22 +0,0 @@ -package co.blocke.scalajack -package json4s - -import org.json4s.JValue - -import scala.collection.mutable - -case class JValueBuilder() extends mutable.Builder[JValue, JValue] { - private var internalValue: Option[JValue] = None - - def addOne(elem: JValue): this.type = { - internalValue = Some(elem) - this - } - - def clear(): Unit = internalValue = None - - def result(): JValue = - internalValue.getOrElse( - throw new ScalaJackError("No value set for internal json4s builder") - ) -} diff --git a/core/src/main/scala/co.blocke.scalajack/json4s/Json4sFlavor.scala b/core/src/main/scala/co.blocke.scalajack/json4s/Json4sFlavor.scala deleted file mode 100644 index f56f47d3..00000000 --- a/core/src/main/scala/co.blocke.scalajack/json4s/Json4sFlavor.scala +++ /dev/null @@ -1,68 +0,0 @@ -package co.blocke.scalajack -package json4s - -import model._ -import typeadapter.MaybeStringWrapTypeAdapter -import org.json4s._ - -import scala.collection.mutable -import co.blocke.scala_reflection.RType - - -case class Json4sFlavor( - override val defaultHint: String = "_hint", - override val permissivesOk: Boolean = false, - override val customAdapters: List[TypeAdapterFactory] = List.empty[TypeAdapterFactory], - override val hintMap: Map[String, String] = Map.empty[String, String], - override val hintValueModifiers: Map[String, HintValueModifier] = Map.empty[String, HintValueModifier], - override val typeValueModifier: HintValueModifier = DefaultHintModifier, - override val parseOrElseMap: Map[Class[_], RType] = Map.empty[Class[_], RType], - override val enumsAsInt: Boolean = false -) extends JackFlavor[JValue] { - - def _read[T](input: JValue, typeAdapter: TypeAdapter[T]): T = - val parser = Json4sParser(input, this) - typeAdapter.read(parser).asInstanceOf[T] - - def _render[T](t: T, typeAdapter: TypeAdapter[T]): JValue = - val sb = json4s.JValueBuilder() - typeAdapter.write(t, writer, sb) - sb.result() - - def parse(input: JValue): Parser = Json4sParser(input, this) - - private val writer = Json4sWriter() - - override val stringifyMapKeys: Boolean = true - - def allowPermissivePrimitives(): JackFlavor[JValue] = - throw new ScalaJackError("Permissive primitives not supported for Json4s") - def enumsAsInts(): JackFlavor[JValue] = this.copy(enumsAsInt = true) - def parseOrElse(poe: (RType, RType)*): JackFlavor[JValue] = - this.copy(parseOrElseMap = this.parseOrElseMap ++ poe.map{(p,oe) => p.infoClass->oe}) - def withAdapters(ta: TypeAdapterFactory*): JackFlavor[JValue] = - this.copy(customAdapters = this.customAdapters ++ ta.toList) - def withDefaultHint(hint: String): JackFlavor[JValue] = - this.copy(defaultHint = hint) - def withHints(h: (RType, String)*): JackFlavor[JValue] = - this.copy(hintMap = this.hintMap ++ h.map{(rt,hint) => rt.name->hint}) - def withHintModifiers(hm: (RType, HintValueModifier)*): JackFlavor[JValue] = - this.copy(hintValueModifiers = this.hintValueModifiers ++ hm.map{(rt,hintM) => rt.name->hintM}) - def withTypeValueModifier(tm: HintValueModifier): JackFlavor[JValue] = - this.copy(typeValueModifier = tm) - - def stringWrapTypeAdapterFactory[T]( - wrappedTypeAdapter: TypeAdapter[T], - emptyStringOk: Boolean = true - ): TypeAdapter[T] = - StringWrapTypeAdapter(wrappedTypeAdapter) - - override def getBuilder: mutable.Builder[JValue, JValue] = - json4s.JValueBuilder() - - def maybeStringWrapTypeAdapterFactory[T]( - wrappedTypeAdapter: TypeAdapter[T], - emptyStringOk: Boolean = true - ): TypeAdapter[T] = - MaybeStringWrapTypeAdapter(this, wrappedTypeAdapter, emptyStringOk) -} diff --git a/core/src/main/scala/co.blocke.scalajack/json4s/Json4sParser.scala b/core/src/main/scala/co.blocke.scalajack/json4s/Json4sParser.scala deleted file mode 100644 index 80a87574..00000000 --- a/core/src/main/scala/co.blocke.scalajack/json4s/Json4sParser.scala +++ /dev/null @@ -1,201 +0,0 @@ -package co.blocke.scalajack -package json4s - -import org.json4s._ -import model._ -import typeadapter.classes.ClassTypeAdapterBase -import scala.collection.mutable -import co.blocke.scala_reflection.RType -import co.blocke.scala_reflection.info.TypeMemberInfo - - -case class Json4sParser(input: JValue, jackFlavor: JackFlavor[JValue]) - extends Parser { - type WIRE = JValue - - def expectString(nullOK: Boolean = true): String = - input match { - case null | JNull => null - case JString(s) => s - case x => - throw new ScalaJackError(s"Expected string here, not '$x'") - } - - def expectList[K, TO]( - KtypeAdapter: TypeAdapter[K], - builder: mutable.Builder[K, TO]): TO = - input match { - // $COVERAGE-OFF$Null caught by TypeAdapter but this left here as a safety - case null | JNull => null.asInstanceOf[TO] - // $COVERAGE-ON$ - case JArray(arr) => - arr.foreach( - a => builder += KtypeAdapter.read(subParser(a)).asInstanceOf[K] - ) - builder.result() - case x => - throw new ScalaJackError(s"Expected list here, not '$x'") - } - - def expectTuple( - tupleFieldTypeAdapters: List[TypeAdapter[_]] - ): List[Object] = - input match { - // $COVERAGE-OFF$Null caught by TypeAdapter but this left here as a safety - case null | JNull => null - // $COVERAGE-ON$ - case JArray(arr) => - tupleFieldTypeAdapters.zip(arr).map { (fieldTypeAdapter, v) => - fieldTypeAdapter.read(subParser(v)).asInstanceOf[Object] - } - case x => - throw new ScalaJackError(s"Expected tuple (list) here, not '$x'") - } - - def expectMap[K, V, TO]( - keyTypeAdapter: TypeAdapter[K], - valueTypeAdapter: TypeAdapter[V], - builder: mutable.Builder[(K, V), TO]): TO = - input match { - // $COVERAGE-OFF$Null caught by TypeAdapter but this left here as a safety - case null | JNull => null.asInstanceOf[TO] - // $COVERAGE-ON$ - case JObject(obj) => - obj.foreach { - case (key, objVal) => - val mapKey = keyTypeAdapter.read(subParser(JString(key))) - val mapValue = valueTypeAdapter.read(subParser(objVal)) - val newElem: (K, V) = (mapKey, mapValue) - builder += newElem - } - builder.result - case x => - throw new ScalaJackError(s"Expected map here, not '$x'") - } - - def expectObject( - classBase: ClassTypeAdapterBase[_], - hintLabel: String - ): (mutable.BitSet, List[Object], java.util.HashMap[String, _]) = - input match { - case JObject(obj) => - val args = classBase.argsTemplate.clone() - val fieldBits = mutable.BitSet() - val captured = - if classBase.isSJCapture then - new java.util.HashMap[String, Any]() - else - null - obj.foreach { - case (key, objVal) => - classBase.fieldMembersByName - .get(key) - .map { field => - fieldBits += field.info.index - args(field.info.index) = - field.valueTypeAdapter.read(subParser(objVal)).asInstanceOf[Object] - } - .getOrElse { - if (captured != null) - captured.put(key, objVal) - } - } - (fieldBits, args.toList, captured) - case x => - throw new ScalaJackError(s"Expected object here, not '$x'") - } - - def expectBoolean(): Boolean = - input match { - case JBool(b) => b - case x => - throw new ScalaJackError(s"Expected boolean here, not '$x'") - } - - def expectNumber(nullOK: Boolean = false): String = - input match { - case null | JNull if nullOK => null - case null | JNull => - throw new ScalaJackError(s"Expected number here, not '$input'") - case JDecimal(num) => num.toString - case JDouble(num) => num.toString - case JInt(num) => num.toString - case JLong(num) => num.toString - case _ => throw new ScalaJackError(s"Expected number here, not '$input'") - } - - def peekForNull: Boolean = input match { - case null | JNull => true - case _ => false - } - - def scanForHint(hint: String, converterFn: HintBijective): Class[_] = - input match { - case JObject(obj) => - obj - .find(_._1 == hint) - .map { - case (label, hintValue) => - hintValue match { - case s: JString => - try { - Class.forName(converterFn.apply(s.s)) - } catch { - case t: Throwable => - throw new ScalaJackError( - s"Couldn't marshal class for ${s.s}" - ) - } - case _ => - throw new ScalaJackError( - s"Hint value $hint must be a string value" - ) - } - } - .getOrElse(throw new ScalaJackError(s"Type hint '$hint' not found")) - case x => - throw new ScalaJackError(s"Expected object here, not '$x'") - } - - // For embedded type members. Convert the type member into runtime "actual" type, e.g. T --> Foo - def resolveTypeMembers( - typeMembersByName: Map[String, TypeMemberInfo], - converterFn: HintBijective - ): Map[String, TypeMemberInfo] = // Returns Map[Type Signature Type (e.g. 'T'), Type] - input match { - case JObject(obj) => - val collected = obj.collect { - case (key, oneValue) if typeMembersByName.contains(key) && oneValue.isInstanceOf[JString] => - ( - key, - TypeMemberInfo(key, typeMembersByName(key).typeSymbol, RType.of(Class.forName(converterFn.apply(oneValue.asInstanceOf[JString].s)))) - ) - } - collected.toMap - case x => - throw new ScalaJackError(s"Expected object here, not '$x'") - } - - // $COVERAGE-OFF$Not needed for Json4s - def showError(msg: String): String = msg - def backspace(): Unit = {} - def mark(): Int = 0 - def revertToMark(mark: Int): Unit = {} - // $COVERAGE-ON$ - - def nextIsString: Boolean = input.isInstanceOf[JString] - def nextIsNumber: Boolean = - input match { - case JDecimal(_) => true - case JDouble(_) => true - case JInt(_) => true - case JLong(_) => true - case _ => false - } - def nextIsObject: Boolean = input.isInstanceOf[JObject] - def nextIsArray: Boolean = input.isInstanceOf[JArray] - def nextIsBoolean: Boolean = input.isInstanceOf[JBool] - - def subParser(input: JValue): Parser = Json4sParser(input, jackFlavor) - def sourceAsString: String = input.toString -} diff --git a/core/src/main/scala/co.blocke.scalajack/json4s/Json4sWriter.scala b/core/src/main/scala/co.blocke.scalajack/json4s/Json4sWriter.scala deleted file mode 100644 index ae1b2d44..00000000 --- a/core/src/main/scala/co.blocke.scalajack/json4s/Json4sWriter.scala +++ /dev/null @@ -1,138 +0,0 @@ -package co.blocke.scalajack -package json4s - -import model._ -import model.Writer - -import scala.collection.{Map, mutable} -import org.json4s._ - -case class Json4sWriter() extends Writer[JValue] { - - def writeArray[Elem](t: Iterable[Elem], elemTypeAdapter: TypeAdapter[Elem], out: collection.mutable.Builder[JValue, JValue]): Unit = - t match { - case null => out += JNull - case a => - var arr = JArray(List.empty[JValue]) - val outBuf = JValueBuilder() - a.iterator.foreach { item => - outBuf.clear() - elemTypeAdapter.write(item, this, outBuf) - arr = JArray(arr.arr :+ outBuf.result) - } - out += arr - } - - def writeRaw(t: JValue, out: mutable.Builder[JValue, JValue]): Unit = - out += t - - def writeBigInt(t: BigInt, out: collection.mutable.Builder[JValue, JValue]): Unit = - out += JInt(t) - - def writeBoolean(t: Boolean, out: collection.mutable.Builder[JValue, JValue]): Unit = - out += JBool(t) - - def writeDecimal(t: BigDecimal, out: collection.mutable.Builder[JValue, JValue]): Unit = - t match { - case null => out += JNull - case d => out += JDecimal(d) - } - - def writeDouble(t: Double, out: collection.mutable.Builder[JValue, JValue]): Unit = - out += JDouble(t) - - def writeInt(t: Int, out: collection.mutable.Builder[JValue, JValue]): Unit = - out += JInt(t) - - def writeLong(t: Long, out: collection.mutable.Builder[JValue, JValue]): Unit = - out += JLong(t) - - def writeMap[Key, Value, To](t: Map[Key, Value], keyTypeAdapter: TypeAdapter[Key], valueTypeAdapter: TypeAdapter[Value], out: mutable.Builder[JValue, JValue]): Unit = - t match { - case null => out += JNull - case daMap => - val outBuf = JValueBuilder() - val outMap = daMap.map { - case (key, value) => - if (key == null) - throw new ScalaJackError("Map keys cannot be null.") - outBuf.clear() - keyTypeAdapter.write(key.asInstanceOf[Key], this, outBuf) - val k = outBuf.result().values.toString - outBuf.clear() - valueTypeAdapter.write(value, this, outBuf) - k -> outBuf.result - }.toList - out += JObject(outMap) - } - - def writeString(t: String, out: collection.mutable.Builder[JValue, JValue]): Unit = - t match { - case null => out += JNull - case _: String => out += JString(t) - } - - def writeNull(out: collection.mutable.Builder[JValue, JValue]): Unit = - out += JNull - - @inline private def writeFields( - fields: List[(String, Any, TypeAdapter[Any])] - ): Map[String, JValue] = { - val outBuf = JValueBuilder() - fields.collect { - case (label, value, valueTypeAdapter) if value != None => - outBuf.clear() - valueTypeAdapter.write(value, this, outBuf) - label -> outBuf.result() - }.toMap - } - - def writeObject[T]( - t: T, - orderedFieldNames: List[String], - fieldMembersByName: Map[String, ClassFieldMember[_,_]], - out: mutable.Builder[JValue, JValue], - extras: List[(String, ExtraFieldValue[_])] = List.empty[(String, ExtraFieldValue[_])] - ): Unit = - t match { - case null => out += JNull - case _ => - val extraFields = writeFields( - extras.map( - e => - ( - e._1, - e._2.value, - e._2.valueTypeAdapter.asInstanceOf[TypeAdapter[Any]] - ) - ) - ) - val classFields = writeFields(orderedFieldNames.map { orn => - val oneField = fieldMembersByName(orn) - (orn, oneField.info.valueOf(t), oneField.valueTypeAdapter.asInstanceOf[TypeAdapter[Any]]) - }) - val captureFields = t match { - case sjc: SJCapture => - import scala.jdk.CollectionConverters._ - sjc.captured.asScala.asInstanceOf[Map[String, JValue]] - case _ => Map.empty[String, JValue] - } - - out += JObject((extraFields ++ classFields ++ captureFields).toList) - } - - def writeTuple[T]( - t: T, - writeFn: (Product) => List[(TypeAdapter[_], Any)], - out: mutable.Builder[JValue, JValue] - ): Unit = { - var arr = JArray(List.empty[JValue]) - val outBuf = JValueBuilder() - writeFn(t.asInstanceOf[Product]).foreach { (fieldTA, fieldValue) => - outBuf.clear() - fieldTA.castAndWrite(fieldValue, this, outBuf) - arr = JArray(arr.arr :+ outBuf.result) - } - out += arr - } -} diff --git a/core/src/main/scala/co.blocke.scalajack/json4s/StringWrapTypeAdapter.scala b/core/src/main/scala/co.blocke.scalajack/json4s/StringWrapTypeAdapter.scala deleted file mode 100644 index de99213c..00000000 --- a/core/src/main/scala/co.blocke.scalajack/json4s/StringWrapTypeAdapter.scala +++ /dev/null @@ -1,75 +0,0 @@ -package co.blocke.scalajack -package json4s - -import typeadapter.AnyTypeAdapter -import model._ -import org.json4s._ -import co.blocke.scala_reflection.RType -import co.blocke.scala_reflection.impl.PrimitiveType - -import scala.collection.mutable - -// A TypeAdapter for a type T, which is wrapped in a String, a.k.a. "stringified". -// This is used for JSON Map keys, which must be strings. -case class StringWrapTypeAdapter[T](wrappedTypeAdapter: TypeAdapter[T]) - extends TypeAdapter[T] { - - override def isStringish: Boolean = true - val info: RType = wrappedTypeAdapter.info - - def read(parser: Parser): T = { - // 1. Read String (JValue --> String) - val wrappedValueString = parser.expectString() - - wrappedTypeAdapter match { - case value: ScalarTypeAdapter[_] => - value.info match { - case PrimitiveType.Scala_Byte => wrappedValueString.toByte.asInstanceOf[T] - case PrimitiveType.Scala_Int => wrappedValueString.toInt.asInstanceOf[T] - case PrimitiveType.Scala_Long => wrappedValueString.toLong.asInstanceOf[T] - case PrimitiveType.Scala_Double => wrappedValueString.toDouble.asInstanceOf[T] - case PrimitiveType.Scala_Float => wrappedValueString.toFloat.asInstanceOf[T] - case PrimitiveType.Scala_Short => wrappedValueString.toShort.asInstanceOf[T] - case PrimitiveType.Scala_Boolean => wrappedValueString.toBoolean.asInstanceOf[T] - case r: RType if r.name == "scala.math.BigInt" => BigInt(wrappedValueString).asInstanceOf[T] - case r: RType if r.name == "scala.math.BigDecimal" => BigDecimal(wrappedValueString).asInstanceOf[T] - // $COVERAGE-OFF$Currently all scalars in ScalaJack are supported. Here just in case... - case _ => - throw new ScalaJackError( - "Only Scala scalar values are supported as JValue Map keys" - ) - // $COVERAGE-ON$ - } - case value: AnyTypeAdapter => - value.read(parser).asInstanceOf[T] - case _ => - throw new ScalaJackError( - "Only scalar values are supported as JValue Map keys" - ) - } - } - - def write[WIRE]( - t: T, - writer: model.Writer[WIRE], - out: mutable.Builder[WIRE, WIRE]): Unit = { - val keyValBuilder = - JValueBuilder().asInstanceOf[collection.mutable.Builder[Any, WIRE]] - wrappedTypeAdapter.write(t, writer, keyValBuilder) - val result = keyValBuilder.result() match { - case r: JBool => r.values.toString - case r: JDecimal => r.values.toString - case r: JDouble => r.values.toString - case r: JInt => r.values.toString - case r: JLong => r.values.toString - // $COVERAGE-OFF$All scalar/wrapped JValues supported as of this writing. Here just in case someone invents a new one. - case r: JString => r.values.toString // just in case someone wraps a String! - case r => - throw new ScalaJackError( - "Json4s type " + r.getClass.getName + " is not supported as a Map key" - ) - // $COVERAGE-ON$ - } - writer.writeString(result, out) - } -} diff --git a/core/src/main/scala/co.blocke.scalajack/model/ClassFieldMember.scala b/core/src/main/scala/co.blocke.scalajack/model/ClassFieldMember.scala deleted file mode 100644 index 73717585..00000000 --- a/core/src/main/scala/co.blocke.scalajack/model/ClassFieldMember.scala +++ /dev/null @@ -1,20 +0,0 @@ -package co.blocke.scalajack -package model - -import co.blocke.scala_reflection.info._ -import typeadapter._ - -case class ClassFieldMember[OWNER,T]( - info: FieldInfo, - valueTypeAdapter: TypeAdapter[T], - outerClass: java.lang.Class[OWNER], // class that "owns" this field - dbKeyIndex: Option[Int], - fieldMapName: Option[String] -): - def name: String = fieldMapName.getOrElse(info.name) - lazy val isOptional: Boolean = valueTypeAdapter match { - case _: OptionTypeAdapter[_] => true - case _: JavaOptionalTypeAdapter[_] => true - case _ if info.annotations.contains(OPTIONAL_ANNO) => true - case _ => false - } \ No newline at end of file diff --git a/core/src/main/scala/co.blocke.scalajack/model/HintValueModifier.scala b/core/src/main/scala/co.blocke.scalajack/model/HintValueModifier.scala deleted file mode 100644 index e1defe0e..00000000 --- a/core/src/main/scala/co.blocke.scalajack/model/HintValueModifier.scala +++ /dev/null @@ -1,38 +0,0 @@ -package co.blocke.scalajack -package model - -// import util.BijectiveFunctionHelpers._ - -trait HintValueModifier extends HintBijective - -/** - * Do-nothing modifier (default if none specified - */ -object DefaultHintModifier extends HintValueModifier { - def apply(rawHint: String): String = rawHint - def unapply(hintFieldType: String): String = hintFieldType -} - - -/** - * Convenience modifier that transforms a hint value string into a fully-qualified class name (and the reverse) - * using passed-in transformation functions. The appropriate Bijective is created under the covers. - */ -case class ClassNameHintModifier( - hintToClassname: (String) => String, - classNameToHint: (String) => String) - extends HintValueModifier { - def apply(rawHint: String): String = hintToClassname(rawHint) - def unapply(hintFieldType: String): String = classNameToHint(hintFieldType) // May explode -} - -/** - * Convenience modifier that transforms a map of string hint values to their respective types. - * Note there is a necessary assumption that the mapping is 1-to-1. If not you'll need to create the - * Bijective function yourself with whatever logic you need, and not use this class. - */ -case class StringMatchHintModifier(hintToType: Map[String, String]) extends HintValueModifier { - val typeToHint: Map[String, String] = hintToType.map(_.swap) - def apply(rawHint: String): String = hintToType(rawHint) - def unapply(hintFieldType: String): String = typeToHint(hintFieldType) -} diff --git a/core/src/main/scala/co.blocke.scalajack/model/JackFlavor.scala b/core/src/main/scala/co.blocke.scalajack/model/JackFlavor.scala deleted file mode 100644 index 43b7faf0..00000000 --- a/core/src/main/scala/co.blocke.scalajack/model/JackFlavor.scala +++ /dev/null @@ -1,115 +0,0 @@ -package co.blocke.scalajack -package model - -import typeadapter._ -import scala.collection.mutable -import co.blocke.scala_reflection._ - -trait JackFlavor[WIRE] extends ViewSplice: // extends Filterable[WIRE] with ViewSplice { - - type WIRE_TYPE = WIRE - - def parse(input: WIRE): Parser - - val defaultHint: String = "_hint" - val stringifyMapKeys: Boolean = false - val hintMap: Map[String, String] = Map.empty[String, String] - val hintValueModifiers: Map[String, HintValueModifier] = Map.empty[String, HintValueModifier] - val typeValueModifier: HintValueModifier = DefaultHintModifier - val enumsAsInt: Boolean = false - val customAdapters: List[TypeAdapterFactory] = List.empty[TypeAdapterFactory] - val parseOrElseMap: Map[Class[_], RType] = Map.empty[Class[_], RType] - val permissivesOk: Boolean = false - - lazy val taCache: TypeAdapterCache = bakeCache() - - def bakeCache(): TypeAdapterCache = - val permissives = - if (permissivesOk) - List( - PermissiveBigDecimalTypeAdapterFactory, - PermissiveBigIntTypeAdapterFactory, - PermissiveBooleanTypeAdapterFactory, - PermissiveByteTypeAdapterFactory, - PermissiveDoubleTypeAdapterFactory, - PermissiveFloatTypeAdapterFactory, - PermissiveIntTypeAdapterFactory, - PermissiveLongTypeAdapterFactory, - PermissiveShortTypeAdapterFactory, - PermissiveJavaBigDecimalTypeAdapterFactory, - PermissiveJavaBigIntegerTypeAdapterFactory, - PermissiveJavaBooleanTypeAdapterFactory, - PermissiveJavaByteTypeAdapterFactory, - PermissiveJavaDoubleTypeAdapterFactory, - PermissiveJavaFloatTypeAdapterFactory, - PermissiveJavaIntTypeAdapterFactory, - PermissiveJavaLongTypeAdapterFactory, - PermissiveJavaNumberTypeAdapterFactory, - PermissiveJavaShortTypeAdapterFactory - ) - else - List.empty[TypeAdapterFactory] - - // Need this (w/o parseOrElseFactories) because resolving the parse-or-else type adapters will cause an endless loop! - val stage1TC = TypeAdapterCache( - this, - permissives ::: customAdapters ::: TypeAdapterCache.StandardFactories - ) - - // ParseOrElse functionality - val parseOrElseFactories: List[TypeAdapterFactory] = parseOrElseMap.map { - case (attemptedTypeClass, fallbackType) => - new TypeAdapterFactory { - def matches(concrete: RType): Boolean = concrete.infoClass == attemptedTypeClass - - def makeTypeAdapter(concrete: RType)(implicit taCache: TypeAdapterCache): TypeAdapter[_] = - FallbackTypeAdapter( stage1TC.typeAdapterOf(concrete), stage1TC.typeAdapterOf(fallbackType) ) - } - }.toList - - TypeAdapterCache( - this, - parseOrElseFactories ::: stage1TC.factories - ) - - final inline def read[T](input: WIRE): T = - val typeAdapter = taCache.typeAdapterOf[T] - _read(input, typeAdapter) - - def _read[T](input: WIRE, typeAdapter: TypeAdapter[T]): T - - final inline def render[T](t: T): WIRE = - val typeAdapter = taCache.typeAdapterOf[T] - _render(t, typeAdapter) - - def _render[T](t: T, typeAdapter: TypeAdapter[T]): WIRE - - // These is so pervasively handy, let's just pre-stage it for easy access - lazy val stringTypeAdapter: TypeAdapter[String] = taCache.typeAdapterOf[String] - lazy val anyTypeAdapter: TypeAdapter[Any] = taCache.typeAdapterOf[Any] - - // Look up any custom hint label for given type, and if none then use default - def getHintLabelFor(tpe: RType): String = hintMap.getOrElse(tpe.name, defaultHint) - - def stringWrapTypeAdapterFactory[T]( - wrappedTypeAdapter: TypeAdapter[T], - emptyStringOk: Boolean = true - ): TypeAdapter[T] - def maybeStringWrapTypeAdapterFactory[T]( - wrappedTypeAdapter: TypeAdapter[T], - emptyStringOk: Boolean = true - ): TypeAdapter[T] - - def enumsAsInts(): JackFlavor[WIRE] - def allowPermissivePrimitives(): JackFlavor[WIRE] - def parseOrElse(poe: (RType, RType)*): JackFlavor[WIRE] - def withAdapters(ta: TypeAdapterFactory*): JackFlavor[WIRE] - def withDefaultHint(hint: String): JackFlavor[WIRE] - def withHints(h: (RType, String)*): JackFlavor[WIRE] - def withHintModifiers(hm: (RType, HintValueModifier)*): JackFlavor[WIRE] - def withTypeValueModifier(tm: HintValueModifier): JackFlavor[WIRE] - - // Need WIRE-specific Builder instance. By default this is StringBuilder. Mongo will overwrite this. - def getBuilder: mutable.Builder[WIRE, WIRE] = - StringBuilder() - .asInstanceOf[mutable.Builder[WIRE, WIRE]] diff --git a/core/src/main/scala/co.blocke.scalajack/model/LazyTypeAdapter.scala b/core/src/main/scala/co.blocke.scalajack/model/LazyTypeAdapter.scala deleted file mode 100644 index b932238a..00000000 --- a/core/src/main/scala/co.blocke.scalajack/model/LazyTypeAdapter.scala +++ /dev/null @@ -1,38 +0,0 @@ -package co.blocke.scalajack -package model - -import co.blocke.scala_reflection._ -import scala.collection.mutable - -/** This helps fix the concurrent/recursion error on maps. This lets the TypeAdapter resolve later (i.e. Lazy) - */ -case class LazyTypeAdapter[T](taCache: TypeAdapterCache, info: RType) - extends TypeAdapter[T] { - - var resolvedTypeAdapter: TypeAdapter[T] = null - - override def resolved: TypeAdapter[T] = { - var typeAdapter = resolvedTypeAdapter - - // $COVERAGE-OFF$Can't really test as this is triggered by race condition, if it can happen at all. - if (typeAdapter == null) { - typeAdapter = taCache.typeAdapterOf(info).resolved.asInstanceOf[TypeAdapter[T]] - if (typeAdapter.isInstanceOf[LazyTypeAdapter[_]]) { - throw new IllegalStateException( - s"Type adapter for ${info.name} is still being built" - ) - } - resolvedTypeAdapter = typeAdapter - } - // $COVERAGE-ON$ - - typeAdapter - } - - def read(parser: Parser): T = resolved.read(parser) - def write[WIRE]( - t: T, - writer: Writer[WIRE], - out: mutable.Builder[WIRE, WIRE]): Unit = - resolved.write(t, writer, out) -} diff --git a/core/src/main/scala/co.blocke.scalajack/model/Parser.scala b/core/src/main/scala/co.blocke.scalajack/model/Parser.scala deleted file mode 100644 index 38952574..00000000 --- a/core/src/main/scala/co.blocke.scalajack/model/Parser.scala +++ /dev/null @@ -1,50 +0,0 @@ -package co.blocke.scalajack -package model - -import typeadapter.classes.ClassTypeAdapterBase -import scala.collection.mutable -import co.blocke.scala_reflection.info.TypeMemberInfo - -trait Parser { - type WIRE - - val jackFlavor: JackFlavor[WIRE] // This is needed and used by permissive type adapters - - def expectString(nullOK: Boolean = true): String - def expectList[K, TO]( - KtypeAdapter: TypeAdapter[K], - builder: mutable.Builder[K, TO]): TO - def expectTuple( - tupleFieldTypeAdapters: List[TypeAdapter[_]] - ): List[Object] - def expectMap[K, V, TO]( - keyTypeAdapter: TypeAdapter[K], - valueTypeAdapter: TypeAdapter[V], - builder: mutable.Builder[(K, V), TO]): TO - def expectObject( - classBase: ClassTypeAdapterBase[_], - hintLabel: String - ): (mutable.BitSet, List[Object], java.util.HashMap[String, _]) - def expectBoolean(): Boolean - def expectNumber(nullOK: Boolean = false): String - def peekForNull: Boolean // peek-ahead to find null - def scanForHint(hint: String, converterFn: HintBijective): Class[_] - - // For embedded type members. Convert the type member into runtime "actual" type, e.g. T --> Foo - def resolveTypeMembers( - typeMembersByName: Map[String, TypeMemberInfo], - converterFn: HintBijective - ): Map[String, TypeMemberInfo] // Returns Map[Type Signature Type (e.g. 'T'), Type] - - def showError(msg: String): String - def backspace(): Unit - def mark(): Int - def revertToMark(mark: Int): Unit - def nextIsString: Boolean - def nextIsNumber: Boolean - def nextIsObject: Boolean - def nextIsArray: Boolean - def nextIsBoolean: Boolean - def subParser(input: WIRE): Parser - def sourceAsString: String -} \ No newline at end of file diff --git a/core/src/main/scala/co.blocke.scalajack/model/StringBuilder.scala b/core/src/main/scala/co.blocke.scalajack/model/StringBuilder.scala deleted file mode 100644 index 4943b8c0..00000000 --- a/core/src/main/scala/co.blocke.scalajack/model/StringBuilder.scala +++ /dev/null @@ -1,18 +0,0 @@ -package co.blocke.scalajack -package model - -import scala.collection.mutable - -case class StringBuilder[WIRE]() extends mutable.Builder[WIRE, WIRE] { - - private val buf = new StringBuffer() - - def addOne(elem: WIRE): StringBuilder.this.type = { - buf.append(elem) - this - } - - def clear() = buf.setLength(0) - - def result() = buf.toString.asInstanceOf[WIRE] -} diff --git a/core/src/main/scala/co.blocke.scalajack/model/TypeAdapaterCache.scala b/core/src/main/scala/co.blocke.scalajack/model/TypeAdapaterCache.scala deleted file mode 100644 index 2a000f11..00000000 --- a/core/src/main/scala/co.blocke.scalajack/model/TypeAdapaterCache.scala +++ /dev/null @@ -1,140 +0,0 @@ -package co.blocke.scalajack -package model - -import typeadapter._ -import typeadapter.classes._ -import scala.util.{ Success, Try } -import co.blocke.scala_reflection._ -import co.blocke.scala_reflection.impl.SelfRefRType -import co.blocke.scala_reflection.info._ - - -object TypeAdapterCache { - - val StandardFactories: List[TypeAdapterFactory] = - List( - BigDecimalTypeAdapterFactory, - BigIntTypeAdapterFactory, - BinaryTypeAdapterFactory, - BooleanTypeAdapterFactory, - ByteTypeAdapterFactory, - CharTypeAdapterFactory, - DoubleTypeAdapterFactory, - FloatTypeAdapterFactory, - IntTypeAdapterFactory, - LongTypeAdapterFactory, - ShortTypeAdapterFactory, - StringTypeAdapterFactory, - OptionTypeAdapterFactory, - TryTypeAdapterFactory, - TupleTypeAdapterFactory, - EitherTypeAdapterFactory, // Either must precede SealedTraitTypeAdapter - UnionTypeAdapterFactory, - IntersectionTypeAdapterFactory, - ArrayTypeAdapterFactory, - EnumTypeAdapterFactory, - UUIDTypeAdapterFactory, - CollectionTypeAdapterFactory, - - // WARNING: These two must precede CaseClassTypeAdapter in this list or all - // ValueClasses will be interpreted as case classes, and case objects - // will likewise be hidden (interpreted as regular classes). - SealedTraitTypeAdapterFactory, - ValueClassTypeAdapterFactory, - ScalaClassTypeAdapterFactory, - - TraitTypeAdapterFactory, - AnyTypeAdapterFactory, - JavaBigDecimalTypeAdapterFactory, - JavaBigIntegerTypeAdapterFactory, - JavaBooleanTypeAdapterFactory, - JavaByteTypeAdapterFactory, - JavaCharacterTypeAdapterFactory, - JavaDoubleTypeAdapterFactory, - JavaFloatTypeAdapterFactory, - JavaIntegerTypeAdapterFactory, - JavaLongTypeAdapterFactory, - JavaNumberTypeAdapterFactory, - JavaShortTypeAdapterFactory, - DurationTypeAdapterFactory, - InstantTypeAdapterFactory, - LocalDateTimeTypeAdapterFactory, - LocalDateTypeAdapterFactory, - LocalTimeTypeAdapterFactory, - OffsetDateTimeTypeAdapterFactory, - OffsetTimeTypeAdapterFactory, - PeriodTypeAdapterFactory, - ZonedDateTimeTypeAdapterFactory, - JavaClassTypeAdapterFactory - ) -} - -case class TypeAdapterCache( - jackFlavor: JackFlavor[_], - factories: List[TypeAdapterFactory]): - - sealed trait Phase - case object Uninitialized extends Phase - case object Initializing extends Phase - case class Initialized(typeAdapterAttempt: Try[TypeAdapter[_]]) extends Phase - - val selfCache = this - - class TypeEntry(tpe: RType): - @volatile - private var phase: Phase = Uninitialized - // println(s"--> TACache (${typeEntries.size}) add [${tpe.name}]") - - def typeAdapter: TypeAdapter[_] = - val attempt = - phase match { - case Initialized(a) => a - - case Uninitialized | Initializing => - synchronized { - phase match { - case Uninitialized => - phase = Initializing - val typeAdapterAttempt = Try { - val foundFactory = factories.find(_.matches(tpe)).get - foundFactory.makeTypeAdapter(tpe)(selfCache) - } - phase = Initialized(typeAdapterAttempt) - typeAdapterAttempt - - case Initializing => - Success(LazyTypeAdapter(TypeAdapterCache.this, tpe)) - - case Initialized(a) => - a - } - } - } - attempt.get - - - private val typeEntries = new java.util.concurrent.ConcurrentHashMap[RType, TypeEntry] - - def withFactory(factory: TypeAdapterFactory): TypeAdapterCache = - copy(factories = factories :+ factory) - - // def typeAdapterOf(tpe: TypeStructure): TypeAdapter[_] = - // typeAdapterOf(RType.ofType(tpe)) - - def typeAdapterOf(concreteType: RType): TypeAdapter[_] = - typeEntries.computeIfAbsent(concreteType, ConcreteTypeEntryFactory).typeAdapter - - inline def typeAdapterOf[T]: TypeAdapter[T] = - typeAdapterOf(RType.of[T]).asInstanceOf[TypeAdapter[T]] - - val self = this - - object ConcreteTypeEntryFactory extends java.util.function.Function[RType, TypeEntry]: - private val AnyRType = RType.of[Any] - private val AnySelfRef = SelfRefRType("scala.Any") - override def apply(concrete: RType): TypeEntry = - concrete match { - case AnySelfRef => new TypeEntry(AnyRType) - case s: SelfRefRType => new TypeEntry(RType.of(s.infoClass)) - case s => new TypeEntry(s) - } diff --git a/core/src/main/scala/co.blocke.scalajack/model/TypeAdapter.scala b/core/src/main/scala/co.blocke.scalajack/model/TypeAdapter.scala deleted file mode 100644 index c0bb1576..00000000 --- a/core/src/main/scala/co.blocke.scalajack/model/TypeAdapter.scala +++ /dev/null @@ -1,72 +0,0 @@ -package co.blocke.scalajack -package model - -import scala.collection.mutable -import scala.reflect.ClassTag -import co.blocke.scala_reflection._ -import co.blocke.scala_reflection.info._ -import co.blocke.scala_reflection.impl.Clazzes._ - - -/** - * TypeAdapter includes two matching patterns you can use when you extend trait TypeAdapter for your - * custom adapters. The two matching behaviors are '===' and '=:='. - * - * This difference is because =:= matches children. Consider: - * - * type Phone = String - * case class( name:String, phone:Phone ) - * - * With =:= both name and phone (String and Phone) will match a TypeAdapter derived from =:=. - * This is actually what you want if you haven't overridden Phone with its own TypeAdapter... it should default - * to the TypeAdapter of its base type. - * - * But... if you did provide an override PhoneTypeAdapter you want the matching to be strict, so we use === - * in this case. With strict matching String != Phone. - * - */ - trait TypeAdapter[T] { - self => - - type tpe = T - - val info: RType - def resolved: TypeAdapter[T] = this // Might be something else during Lazy construction - - def defaultValue: Option[T] = None - - // Used to determine if we need to wrap Map keys in quotes (no, if isStringish == true -- the quotes are automatic in this case) - def isStringish: Boolean = false - def maybeStringish: Boolean = false // means we don't know for sure if something is Stringish until read/render time (not in Factory), e.g. Option or Union - - def read(parser: Parser): T - def write[WIRE]( - t: T, - writer: Writer[WIRE], - out: mutable.Builder[WIRE, WIRE]): Unit - - def as[U <: TypeAdapter[_]: ClassTag]: U = { - val runtimeClass = implicitly[ClassTag[U]].runtimeClass - try { - runtimeClass.cast(self).asInstanceOf[U] - } catch { - case _: ClassCastException => - throw new RuntimeException( - s"$self is not an instance of ${implicitly[ClassTag[U]].runtimeClass}" - ) - } - } - - // Used to correctly-type tuple fields, which each have separate types that are unknown at write, but... - // each field's TypeAdapter *does* know its type so it can be correctly cast inside the TypeAdapter. - inline def castAndWrite[WIRE]( v: Any, writer: Writer[WIRE], out: mutable.Builder[WIRE, WIRE]): Unit = - write(v.asInstanceOf[tpe], writer, out) -} - -trait ScalarTypeAdapter[T] extends TypeAdapter[T] - -// Marker trait for collections -trait Collectionish - -// Marker trait for classes -trait Classish \ No newline at end of file diff --git a/core/src/main/scala/co.blocke.scalajack/model/TypeAdapterFactory.scala b/core/src/main/scala/co.blocke.scalajack/model/TypeAdapterFactory.scala deleted file mode 100644 index a5b345ac..00000000 --- a/core/src/main/scala/co.blocke.scalajack/model/TypeAdapterFactory.scala +++ /dev/null @@ -1,8 +0,0 @@ -package co.blocke.scalajack -package model - -import co.blocke.scala_reflection._ - -trait TypeAdapterFactory: - def matches(concrete: RType): Boolean - def makeTypeAdapter(concrete: RType)(implicit taCache: TypeAdapterCache): TypeAdapter[_] diff --git a/core/src/main/scala/co.blocke.scalajack/model/ViewSplice.scala b/core/src/main/scala/co.blocke.scalajack/model/ViewSplice.scala deleted file mode 100644 index 14a19f32..00000000 --- a/core/src/main/scala/co.blocke.scalajack/model/ViewSplice.scala +++ /dev/null @@ -1,84 +0,0 @@ -package co.blocke.scalajack -package model - -import typeadapter.classes.CaseClassTypeAdapter - -trait ViewSplice: - - me: JackFlavor[_] => - - /** - * Project fields from given master object to a view object of type T. Field names/types must match master - * precisely. - * @param master the master object from which the smaller object is projected - * @return an object of type T which is a "subset" of the master - */ - // WARNING: Assumes CaseClassTypeAdapter.members is in constructor-order. If not, sort on members.index. - inline def view[T](master: Any): T = { - val viewTarget = taCache.typeAdapterOf[T] match { - case ta: CaseClassTypeAdapter[T] => ta - case ta => - throw new ScalaJackError( - s"Output of view() must be a case class. ${ta.info.name} is not a case class." - ) - } - val masterData = master.getClass.getDeclaredFields - val args = viewTarget.fieldMembersByName.toList.flatMap { - case (fieldName: String, f: ClassFieldMember[_, _]) => - val gotOne = masterData - .find( - md => md.getName == f.name && md.getType == f.outerClass.getMethod(f.name).getReturnType - ) - .map(dataField => { - dataField.setAccessible(true) - dataField.get(master) - }) - if (gotOne.isEmpty && !f.isOptional) - throw new ScalaJackError( - "View master object " + master.getClass.getName + " is missing field " + fieldName + " required to build view object " + viewTarget.info.name - ) - gotOne - } - viewTarget.constructWith(args) - } - - /** - * Splice a view (subset) object's fields into a master object's fields. - * @param view the subset object - * @param master master object - * @return the master object with the view object's corresponding fields merged/overlayed - */ - inline def spliceInto[T, U](view: T, master: U): U = { - val masterTarget = taCache.typeAdapterOf[U] match { - case ta: CaseClassTypeAdapter[U] => ta - case ta => - throw new ScalaJackError( - s"Output of spliceInto() must be a case class. ${ta.info.name} is not a case class." - ) - } - val viewData = view.getClass.getDeclaredFields - val masterData = master.getClass.getDeclaredFields - val args = masterTarget.orderedFieldNames.map { fieldName => - val f = masterTarget.fieldMembersByName(fieldName) - viewData - .find( - vd => vd.getName == f.name && vd.getType == f.outerClass.getMethod(f.name).getReturnType - ) - .map(dataField => { - // Found matching master field in view object - dataField.setAccessible(true) - dataField.get(view) - }) - .getOrElse( - masterData - .find(_.getName == f.name) - .map { dataField => - // Didn't find matching master field in view object--just use original field from master object - dataField.setAccessible(true) - dataField.get(master) - } - .get - ) - } - masterTarget.constructWith(args) - } \ No newline at end of file diff --git a/core/src/main/scala/co.blocke.scalajack/model/Writer.scala b/core/src/main/scala/co.blocke.scalajack/model/Writer.scala deleted file mode 100644 index f1eb6ff4..00000000 --- a/core/src/main/scala/co.blocke.scalajack/model/Writer.scala +++ /dev/null @@ -1,34 +0,0 @@ -package co.blocke.scalajack -package model - -import scala.collection.Map -import scala.collection.mutable -import co.blocke.scala_reflection.info.{TupleInfo, FieldInfo} - -case class ExtraFieldValue[T](value: T, valueTypeAdapter: TypeAdapter[T]) - -trait Writer[WIRE] { - def writeArray[Elem](t: Iterable[Elem], elemTypeAdapter: TypeAdapter[Elem], out: mutable.Builder[WIRE, WIRE]): Unit - def writeBigInt(t: BigInt, out: mutable.Builder[WIRE, WIRE]): Unit - def writeBoolean(t: Boolean, out: mutable.Builder[WIRE, WIRE]): Unit - def writeDecimal(t: BigDecimal, out: mutable.Builder[WIRE, WIRE]): Unit - def writeDouble(t: Double, out: mutable.Builder[WIRE, WIRE]): Unit - def writeInt(t: Int, out: mutable.Builder[WIRE, WIRE]): Unit - def writeLong(t: Long, out: mutable.Builder[WIRE, WIRE]): Unit - def writeMap[Key, Value, To](t: Map[Key, Value], keyTypeAdapter: TypeAdapter[Key], valueTypeAdapter: TypeAdapter[Value], out: mutable.Builder[WIRE, WIRE]): Unit - def writeNull(out: mutable.Builder[WIRE, WIRE]): Unit - def writeObject[T]( - t: T, - orderedFieldNames: List[String], - fieldMembersByName: Map[String, ClassFieldMember[_,_]], - out: mutable.Builder[WIRE, WIRE], - extras: List[(String, ExtraFieldValue[_])] = List.empty[(String, ExtraFieldValue[_])] - ): Unit - def writeString(t: String, out: mutable.Builder[WIRE, WIRE]): Unit - def writeRaw(t: WIRE, out: mutable.Builder[WIRE, WIRE]): Unit // i.e. no quotes for JSON - def writeTuple[T]( - t: T, - writeFn: (Product) => List[(TypeAdapter[_], Any)], - out: mutable.Builder[WIRE, WIRE] - ): Unit -} \ No newline at end of file diff --git a/core/src/main/scala/co.blocke.scalajack/typeadapter/AnyTypeAdapter.scala b/core/src/main/scala/co.blocke.scalajack/typeadapter/AnyTypeAdapter.scala deleted file mode 100644 index b189d88c..00000000 --- a/core/src/main/scala/co.blocke.scalajack/typeadapter/AnyTypeAdapter.scala +++ /dev/null @@ -1,101 +0,0 @@ -package co.blocke.scalajack -package typeadapter - -import model._ -import classes._ -import co.blocke.scala_reflection._ -import collection._ - -import scala.collection.mutable -import scala.collection.mutable.ListBuffer -import scala.util.{Success, Try} - -object AnyTypeAdapterFactory extends TypeAdapterFactory: - def matches(concrete: RType): Boolean = concrete.infoClass == impl.PrimitiveType.Scala_Any.infoClass - def makeTypeAdapter(concrete: RType)(implicit taCache: TypeAdapterCache): TypeAdapter[_] = - AnyTypeAdapter(concrete, taCache) - - -case class AnyTypeAdapter(info: RType, taCache: TypeAdapterCache) extends TypeAdapter[Any] { - val jackFlavor = taCache.jackFlavor - - lazy val mapAnyTypeAdapter: TypeAdapter[Map[Any, Any]] = taCache.typeAdapterOf[Map[Any, Any]] - lazy val listAnyTypeAdapter: TypeAdapter[List[Any]] = taCache.typeAdapterOf[List[Any]] - lazy val optionAnyTypeAdapter: TypeAdapter[Option[Any]] = taCache.typeAdapterOf[Option[Any]] - - override def maybeStringish: Boolean = true - - def read(parser: Parser): Any = - parser match { - case p if p.peekForNull => null - case p if p.nextIsBoolean => p.expectBoolean() - case p if p.nextIsNumber => - BigDecimal(p.expectNumber()) match { - case i if i.isValidInt => i.toIntExact - case i if i.isValidLong => i.toLongExact - case d if d.isDecimalDouble => d.toDouble - case d if d.ulp == 1 => d.toBigInt - case d => d - } - case p if p.nextIsString && jackFlavor.permissivesOk => - jackFlavor.stringWrapTypeAdapterFactory(this).read(p) - case p if p.nextIsString => p.expectString() - case p if p.nextIsArray => - val listBuilder: ListBuffer[Any] = mutable.ListBuffer.empty[Any] - p.expectList(jackFlavor.anyTypeAdapter, listBuilder) - case p if p.nextIsObject => - val mapBuilder = mutable.Map - .empty[Any, Any] - .asInstanceOf[mutable.Builder[(Any, Any), mutable.Map[Any, Any]]] - val mark = parser.mark() - val foundMap = p.expectMap[Any, Any, mutable.Map[Any, Any]]( - jackFlavor.stringWrapTypeAdapterFactory(this), - this, - mapBuilder - ) - if (foundMap.contains(jackFlavor.defaultHint)) { - parser.revertToMark(mark) - Try( - Class.forName(foundMap(jackFlavor.defaultHint).toString) - ) match { - case Success(concreteTypeClass) => taCache.typeAdapterOf(RType.of(concreteTypeClass)).read(p) - case _ => foundMap - } - } else - foundMap.toMap - case p => p.sourceAsString - } - - // Need this little bit of gymnastics here to unpack the X type parameter so we can use it to case the TypeAdapter - private def unpack[X, WIRE](value: X, writer: Writer[WIRE], out: mutable.Builder[WIRE, WIRE]): Unit = - taCache - .typeAdapterOf(RType.of(value.getClass)) - .asInstanceOf[TypeAdapter[X]] match { - case ta: CaseClassTypeAdapter[X] => - val builder = jackFlavor.getBuilder.asInstanceOf[mutable.Builder[WIRE, WIRE]] - ta.writeWithHint[WIRE](jackFlavor.asInstanceOf[JackFlavor[WIRE]], value, writer, builder) - writer.writeRaw(builder.result(), out) - case ta: NonCaseClassTypeAdapter[X] => - val builder = jackFlavor.getBuilder.asInstanceOf[mutable.Builder[WIRE, WIRE]] - ta.writeWithHint[WIRE](jackFlavor.asInstanceOf[JackFlavor[WIRE]], value, writer, builder) - writer.writeRaw(builder.result(), out) - case ta => - ta.write(value, writer, out) - } - - - // WARNING: JSON output broken for Option[...] where value is None -- especially bad for Map keys! - def write[WIRE]( - t: Any, - writer: Writer[WIRE], - out: mutable.Builder[WIRE, WIRE]): Unit = - t match { - case null => writer.writeNull(out) - case e if e.getClass.getName == "scala.Enumeration$Val" => writer.writeString(t.toString, out) - case _: scala.reflect.Enum => writer.writeString(t.toString, out) - case _: Map[_, _] => mapAnyTypeAdapter.write(t.asInstanceOf[Map[Any, Any]], writer, out) - case _: Seq[_] => listAnyTypeAdapter.write(t.asInstanceOf[List[Any]], writer, out) - case _: Option[_] => optionAnyTypeAdapter.write(t.asInstanceOf[Option[Any]], writer, out) - case v => unpack[Any,WIRE](v, writer, out) - } -} diff --git a/core/src/main/scala/co.blocke.scalajack/typeadapter/ArrayTypeAdatper.scala b/core/src/main/scala/co.blocke.scalajack/typeadapter/ArrayTypeAdatper.scala deleted file mode 100644 index 3b427038..00000000 --- a/core/src/main/scala/co.blocke.scalajack/typeadapter/ArrayTypeAdatper.scala +++ /dev/null @@ -1,71 +0,0 @@ -package co.blocke.scalajack -package typeadapter - -import model._ - -import co.blocke.scala_reflection.impl.Clazzes._ -import co.blocke.scala_reflection.info._ -import co.blocke.scala_reflection._ - -import scala.collection.mutable -import scala.reflect.ClassTag -import scala.language.implicitConversions - - -object ArrayTypeAdapterFactory extends TypeAdapterFactory: - - def matches(concrete: RType): Boolean = - concrete match { - case _: ArrayInfo => true - case _: JavaArrayInfo => true - case _ => false - } - - def makeTypeAdapter(concrete: RType)(implicit taCache: TypeAdapterCache): TypeAdapter[_] = - concrete match { - case arrInfo: ArrayInfo => - val elementInfo = arrInfo.elementType - ArrayTypeAdapter( - concrete, - elementInfo.isInstanceOf[OptionInfo], - elementInfo, - taCache.typeAdapterOf(elementInfo)) - case javaInfo: JavaArrayInfo => - val elementInfo = javaInfo.elementType - ArrayTypeAdapter( - concrete, - false, // TODO: Support java.Optional ==> elementInfo.isInstanceOf[OptionInfo], - elementInfo, - taCache.typeAdapterOf(elementInfo)) - } - - -case class ArrayTypeAdapter[ELEM]( - info: RType, - elemIsOptional: Boolean, - elementType: RType, - elementTypeAdapter: TypeAdapter[ELEM] - ) extends TypeAdapter[Array[ELEM]] with ScalarTypeAdapter[Array[ELEM]]: - - def read(parser: Parser): Array[ELEM] = - parser.peekForNull match { - case true => null - case _ => - val classtag = ClassTag[ELEM](elementType.infoClass) - val builder: mutable.Builder[ELEM,Array[ELEM]] = Array.newBuilder[ELEM](classtag.asInstanceOf[ClassTag[ELEM]]).asInstanceOf[mutable.Builder[ELEM,Array[ELEM]]] - val values = parser.expectList(elementTypeAdapter, builder) - builder.result - } - - def write[WIRE](t: Array[ELEM], writer: Writer[WIRE], out: mutable.Builder[WIRE, WIRE]): Unit = - t match { - case null => writer.writeNull(out) - case _ if elemIsOptional => - writer.writeArray( - t.toList.filterNot(_ == None), - elementTypeAdapter, - out - ) - case _ => - writer.writeArray(t.toList, elementTypeAdapter, out) - } diff --git a/core/src/main/scala/co.blocke.scalajack/typeadapter/CollectionTypeAdapter.scala b/core/src/main/scala/co.blocke.scalajack/typeadapter/CollectionTypeAdapter.scala deleted file mode 100644 index 41c207bc..00000000 --- a/core/src/main/scala/co.blocke.scalajack/typeadapter/CollectionTypeAdapter.scala +++ /dev/null @@ -1,191 +0,0 @@ -package co.blocke.scalajack -package typeadapter - -import model._ -import collection._ - -import co.blocke.scala_reflection._ -import co.blocke.scala_reflection.impl.CollectionRType -import co.blocke.scala_reflection.info._ - - -object CollectionTypeAdapterFactory extends TypeAdapterFactory: - def matches(concrete: RType): Boolean = - concrete match { - case _: CollectionRType => true - case _ => false - } - - inline def isOptionalTA(ta: TypeAdapter[_]) = ta.isInstanceOf[OptionTypeAdapter[_]] || ta.isInstanceOf[JavaOptionalTypeAdapter[_]] - - def makeTypeAdapter(concrete: RType)(implicit taCache: TypeAdapterCache): TypeAdapter[_] = - concrete match { - case c: SeqLikeInfo => - val elementInfo = c.elementType - val companionClass = Class.forName(c.infoClass.getName+"$") - val companionInstance = companionClass.getField("MODULE$").get(companionClass) - val builderMethod = companionClass.getMethod("newBuilder") - SeqLikeTypeAdapter(concrete, elementInfo.isInstanceOf[OptionInfo], taCache.typeAdapterOf(elementInfo), companionInstance, builderMethod) - - case c: MapLikeInfo => - val companionClass = Class.forName(c.infoClass.getName+"$") - val companionInstance = companionClass.getField("MODULE$").get(companionClass) - val builderMethod = companionClass.getMethod("newBuilder") - - val jackFlavor = taCache.jackFlavor - val keyTypeAdapter = taCache.typeAdapterOf(c.elementType) - // Wrap Map keys in a StringWrapTypeAdapter? - val finalKeyTypeAdapter = keyTypeAdapter match { - case k if k.isStringish => k // ready-to-eat - case k if k.maybeStringish => jackFlavor.maybeStringWrapTypeAdapterFactory(k) - case k => jackFlavor.stringWrapTypeAdapterFactory(k) // wrap map keys in quotes - } - val valueTypeAdapter = taCache.typeAdapterOf(c.elementType2) match { - case ta: OptionTypeAdapter[_] => ta.convertNullToNone() - case ta: JavaOptionalTypeAdapter[_] => ta.convertNullToNone() - case ta => ta - } - - // Note: We include Any here because Any *could* be an Option, so we must include it as a possibility - val keyIsOptionalOrAny = - isOptionalTA(keyTypeAdapter) || - (keyTypeAdapter.isInstanceOf[StringWrapTypeAdapter[_]] && isOptionalTA(keyTypeAdapter - .asInstanceOf[StringWrapTypeAdapter[_]] - .wrappedTypeAdapter)) || - keyTypeAdapter.isInstanceOf[AnyTypeAdapter] - // keyTypeAdapter == taCache.jackFlavor.anyMapKeyTypeAdapter - - val valueIsOptionalOrAny = isOptionalTA(valueTypeAdapter) || valueTypeAdapter.isInstanceOf[AnyTypeAdapter] - - MapLikeTypeAdapter( - concrete, - keyIsOptionalOrAny, - valueIsOptionalOrAny, - finalKeyTypeAdapter, - valueTypeAdapter, - companionInstance, - builderMethod) - - - case c: JavaListInfo => - val elementInfo = c.elementType - // For List-like Java collections, use a ListBuilder then convert later to the Java collection - val companionClass = Class.forName("scala.collection.immutable.List$") - val companionInstance = companionClass.getField("MODULE$").get(companionClass) - val builderMethod = companionClass.getMethod("newBuilder") - val javaCollectionConstructor = c.infoClass.getConstructor(Class.forName("java.util.Collection")) - val toArrayMethod = c.infoClass.getMethod("toArray") - val elementTA = taCache.typeAdapterOf(elementInfo) - - JavaSeqLikeTypeAdapter( - concrete, - isOptionalTA(elementTA), - elementTA, - companionInstance, - builderMethod, - javaCollectionConstructor, - toArrayMethod - ) - - case c: JavaSetInfo => - val elementInfo = c.elementType - // For List-like Java collections, use a ListBuilder then convert later to the Java collection - val companionClass = Class.forName("scala.collection.immutable.List$") - val companionInstance = companionClass.getField("MODULE$").get(companionClass) - val builderMethod = companionClass.getMethod("newBuilder") - val javaCollectionConstructor = c.infoClass.getConstructor(Class.forName("java.util.Collection")) - val toArrayMethod = c.infoClass.getMethod("toArray") - val elementTA = taCache.typeAdapterOf(elementInfo) - - JavaSeqLikeTypeAdapter( - concrete, - isOptionalTA(elementTA), - elementTA, - companionInstance, - builderMethod, - javaCollectionConstructor, - toArrayMethod - ) - - case c: JavaQueueInfo => - val elementInfo = c.elementType - // For List-like Java collections, use a ListBuilder then convert later to the Java collection - val companionClass = Class.forName("scala.collection.immutable.List$") - val companionInstance = companionClass.getField("MODULE$").get(companionClass) - val builderMethod = companionClass.getMethod("newBuilder") - val javaCollectionConstructor = c.infoClass.getConstructor(Class.forName("java.util.Collection")) - val toArrayMethod = c.infoClass.getMethod("toArray") - val elementTA = taCache.typeAdapterOf(elementInfo) - - JavaSeqLikeTypeAdapter( - concrete, - isOptionalTA(elementTA), - elementTA, - companionInstance, - builderMethod, - javaCollectionConstructor, - toArrayMethod - ) - - case c: JavaStackInfo => - val elementInfo = c.elementType - // For List-like Java collections, use a ListBuilder then convert later to the Java collection - val companionClass = Class.forName("scala.collection.immutable.List$") - val companionInstance = companionClass.getField("MODULE$").get(companionClass) - val builderMethod = companionClass.getMethod("newBuilder") - val javaCollectionConstructor = c.infoClass.getConstructors.head - val toArrayMethod = c.infoClass.getMethod("toArray") - val elementTA = taCache.typeAdapterOf(elementInfo) - - JavaStackTypeAdapter( - concrete, - isOptionalTA(elementTA), - taCache.typeAdapterOf(elementInfo), - companionInstance, - builderMethod, - javaCollectionConstructor, - toArrayMethod - ) - - case c: JavaMapInfo => - val companionClass = Class.forName("scala.collection.immutable.Map$") - val companionInstance = companionClass.getField("MODULE$").get(companionClass) - val builderMethod = companionClass.getMethod("newBuilder") - val javaMapConstructor = c.infoClass.getConstructor(Class.forName("java.util.Map")) - - val jackFlavor = taCache.jackFlavor - val keyTypeAdapter = taCache.typeAdapterOf(c.elementType) - // Wrap Map keys in a StringWrapTypeAdapter? - val finalKeyTypeAdapter = keyTypeAdapter match { - case k if k.isStringish => k // ready-to-eat - case k if k.maybeStringish => jackFlavor.maybeStringWrapTypeAdapterFactory(k) - case k => jackFlavor.stringWrapTypeAdapterFactory(k) // wrap map keys in quotes - } - val valueTypeAdapter = taCache.typeAdapterOf(c.elementType2) match { - case ta: OptionTypeAdapter[_] => ta.convertNullToNone() - case ta: JavaOptionalTypeAdapter[_] => ta.convertNullToNone() - case ta => ta - } - - // Note: We include Any here because Any *could* be an Option, so we must include it as a possibility - val keyIsOptionalOrAny = - isOptionalTA(keyTypeAdapter) || - (keyTypeAdapter.isInstanceOf[StringWrapTypeAdapter[_]] && isOptionalTA(keyTypeAdapter - .asInstanceOf[StringWrapTypeAdapter[_]] - .wrappedTypeAdapter)) || - keyTypeAdapter.isInstanceOf[AnyTypeAdapter] - // keyTypeAdapter == taCache.jackFlavor.anyMapKeyTypeAdapter - - val valueIsOptionalOrAny = isOptionalTA(valueTypeAdapter) || valueTypeAdapter.isInstanceOf[AnyTypeAdapter] - - JavaMapLikeTypeAdapter( - concrete, - keyIsOptionalOrAny, - valueIsOptionalOrAny, - finalKeyTypeAdapter, - valueTypeAdapter, - companionInstance, - builderMethod, - javaMapConstructor) - - } diff --git a/core/src/main/scala/co.blocke.scalajack/typeadapter/EitherTypeAdapter.scala b/core/src/main/scala/co.blocke.scalajack/typeadapter/EitherTypeAdapter.scala deleted file mode 100644 index aedde23c..00000000 --- a/core/src/main/scala/co.blocke.scalajack/typeadapter/EitherTypeAdapter.scala +++ /dev/null @@ -1,78 +0,0 @@ -package co.blocke.scalajack -package typeadapter - -import model._ -import co.blocke.scala_reflection._ -import co.blocke.scala_reflection.info.EitherInfo -import co.blocke.scala_reflection.impl.Clazzes._ - -import scala.collection.mutable.Builder -import scala.util.{ Failure, Success, Try } - -object EitherTypeAdapterFactory extends TypeAdapterFactory: - - def matches(concrete: RType): Boolean = - concrete match { - case _: EitherInfo => true - case _ => false - } - - def makeTypeAdapter(concrete: RType)(implicit taCache: TypeAdapterCache): TypeAdapter[_] = - val eitherInfo = concrete.asInstanceOf[EitherInfo] - val leftInfo = eitherInfo.leftType - val rightInfo = eitherInfo.rightType - - if( leftInfo.infoClass <:< rightInfo.infoClass || rightInfo.infoClass <:< leftInfo.infoClass) - throw new IllegalArgumentException( - s"Types ${leftInfo.name} and ${rightInfo.name} are not mutually exclusive" - ) - val leftTypeAdapter = taCache.typeAdapterOf(leftInfo) - val rightTypeAdapter = taCache.typeAdapterOf(rightInfo) - - EitherTypeAdapter( - concrete, - leftTypeAdapter, - rightTypeAdapter) - - -case class EitherTypeAdapter[L, R]( - info: RType, - leftTypeAdapter: TypeAdapter[L], - rightTypeAdapter: TypeAdapter[R]) - extends TypeAdapter[Either[L, R]] { - - override def isStringish: Boolean = leftTypeAdapter.isStringish && rightTypeAdapter.isStringish - override def maybeStringish: Boolean = !isStringish - - def read(parser: Parser): Either[L, R] = { - val savedReader = parser.mark() - if (parser.peekForNull) - null - else - Try(rightTypeAdapter.read(parser)) match { - case Success(rightValue) => - Right(rightValue.asInstanceOf[R]) - case Failure(_) => // Right parse failed... try left - parser.revertToMark(savedReader) - Try(leftTypeAdapter.read(parser)) match { - case Success(leftValue) => - Left(leftValue.asInstanceOf[L]) - case Failure(x) => - parser.backspace() - throw new ScalaJackError( - parser.showError(s"Failed to read either side of Either") - ) - } - } - } - - def write[WIRE]( - t: Either[L, R], - writer: Writer[WIRE], - out: Builder[WIRE, WIRE]): Unit = - t match { - case null => writer.writeNull(out) - case Left(v) => leftTypeAdapter.write(v, writer, out) - case Right(v) => rightTypeAdapter.write(v, writer, out) - } -} diff --git a/core/src/main/scala/co.blocke.scalajack/typeadapter/EnumTypeAdapter.scala b/core/src/main/scala/co.blocke.scalajack/typeadapter/EnumTypeAdapter.scala deleted file mode 100644 index 00529bb5..00000000 --- a/core/src/main/scala/co.blocke.scalajack/typeadapter/EnumTypeAdapter.scala +++ /dev/null @@ -1,178 +0,0 @@ -package co.blocke.scalajack -package typeadapter - -import model._ - -import scala.collection.mutable -import scala.util.{Try, Success, Failure} -import java.lang.reflect.Method -import co.blocke.scala_reflection._ -import co.blocke.scala_reflection.info._ - -object EnumTypeAdapterFactory extends TypeAdapterFactory: - def matches(concrete: RType): Boolean = - concrete match { - case _: ScalaEnumInfo => true - case _: ScalaEnumerationInfo => true - case _: JavaEnumInfo => true - case _ => false - } - - def makeTypeAdapter(concrete: RType)(implicit taCache: TypeAdapterCache): TypeAdapter[_] = - val enumsAsInt = taCache.jackFlavor.enumsAsInt - concrete match { - // Scala 2.x Enumeration support - case scalaOld: ScalaEnumerationInfo => - val erasedEnumClassName = scalaOld.name + "$" - val enumInstance = Class - .forName(erasedEnumClassName) - .getField(scala.reflect.NameTransformer.MODULE_INSTANCE_NAME) - .get(null) - .asInstanceOf[Enumeration] - ScalaEnumerationTypeAdapter(enumInstance, concrete, enumsAsInt) - - // Scala 3.x Enum support - case scalaNew: ScalaEnumInfo => - ScalaEnumTypeAdapter(concrete, enumsAsInt) - - // Java Enum support - case javaEnum: JavaEnumInfo => - JavaEnumTypeAdapter(concrete, enumsAsInt) - } - - -case class ScalaEnumerationTypeAdapter[E <: Enumeration]( - e: E, - info: RType, - enumsAsInt: Boolean - ) extends TypeAdapter[e.Value]: - override def isStringish: Boolean = !enumsAsInt - - def read(parser: Parser): e.Value = - if (parser.nextIsNumber) { - val en = parser.expectNumber() - Try(e(en.toInt)) match { - case Success(u) => u - case Failure(u) => - parser.backspace() - throw new ScalaJackError( - parser.showError( - s"No value found in enumeration ${e.getClass.getName} for $en" - ) - ) - } - } else if (parser.nextIsString) { - val es = parser.expectString() - if (es == null) - null - else - Try(e.withName(es)) match { - case Success(u) => u - case Failure(u) => - parser.backspace() - throw new ScalaJackError( - parser.showError( - s"No value found in enumeration ${e.getClass.getName} for $es" - ) - ) - } - } else if (parser.peekForNull) - null - else - throw new ScalaJackError( - parser.showError(s"Expected a Number or String here") - ) - - def write[WIRE]( - t: e.Value, - writer: Writer[WIRE], - out: mutable.Builder[WIRE, WIRE]): Unit = - t match { - case null => writer.writeNull(out) - case v if enumsAsInt => writer.writeInt(v.id, out) - case v => writer.writeString(v.toString, out) - } - - -case class ScalaEnumTypeAdapter[E <: scala.reflect.Enum]( - info: RType, - enumsAsInt: Boolean - ) extends TypeAdapter[E]: - - val scalaEnum = info.asInstanceOf[ScalaEnumInfo] - override def isStringish: Boolean = !enumsAsInt - - def read(parser: Parser): E = - if (parser.nextIsNumber) { - val en = parser.expectNumber().toInt - Try(scalaEnum.valueOf(en)) match { - case Success(u) => u.asInstanceOf[E] - case Failure(u) => - parser.backspace() - throw new ScalaJackError( - parser.showError( - s"No value found in enumeration ${info.name} for $en" - ) - ) - } - } else if (parser.nextIsString) { - val es = parser.expectString() - if (es == null) - null.asInstanceOf[E] - else - Try(scalaEnum.valueOf(es).asInstanceOf[E]) match { - case Success(u) => u - case Failure(u) => - parser.backspace() - throw new ScalaJackError( - parser.showError( - s"No value found in enumeration ${info.name} for $es" - ) - ) - } - } else if (parser.peekForNull) - null.asInstanceOf[E] - else - throw new ScalaJackError( - parser.showError(s"Expected a Number or String here") - ) - - def write[WIRE]( - t: E, - writer: Writer[WIRE], - out: mutable.Builder[WIRE, WIRE]): Unit = - t match { - case null => writer.writeNull(out) - case _ if enumsAsInt => writer.writeInt(t.ordinal, out) - case _ => writer.writeString(t.toString, out) - } - - -case class JavaEnumTypeAdapter[E <: java.lang.Enum[_]]( - info: RType, - enumsAsInt: Boolean - ) extends TypeAdapter[E]: - - val javaEnum = info.asInstanceOf[JavaEnumInfo] - override def isStringish: Boolean = !enumsAsInt - - def read(parser: Parser): E = - if (parser.peekForNull) then - null.asInstanceOf[E] - else - val valueOf = info.infoClass.getDeclaredMethod("valueOf", classOf[String]) - try { - valueOf.invoke(info.infoClass, parser.expectString()).asInstanceOf[E] - } catch { - case ex: java.lang.reflect.InvocationTargetException => throw ex.getCause - case t: Throwable => throw t - } - - def write[WIRE]( - t: E, - writer: Writer[WIRE], - out: mutable.Builder[WIRE, WIRE]): Unit = - t match { - case null => writer.writeNull(out) - case _ => writer.writeString(t.toString, out) - } \ No newline at end of file diff --git a/core/src/main/scala/co.blocke.scalajack/typeadapter/FallbackTypeAdapter.scala b/core/src/main/scala/co.blocke.scalajack/typeadapter/FallbackTypeAdapter.scala deleted file mode 100644 index c914fcd3..00000000 --- a/core/src/main/scala/co.blocke.scalajack/typeadapter/FallbackTypeAdapter.scala +++ /dev/null @@ -1,30 +0,0 @@ -package co.blocke.scalajack -package typeadapter - -import model._ -import co.blocke.scala_reflection.RType - -import scala.collection.mutable -import scala.util.{ Failure, Success, Try } - -case class FallbackTypeAdapter[A, B]( - attemptedTypeAdapter: TypeAdapter[A], - orElseTypeAdapter: TypeAdapter[B] - ) extends TypeAdapter[A]: - - val info: RType = attemptedTypeAdapter.info - - def read(parser: Parser): A = - val mark = parser.mark() - Try(attemptedTypeAdapter.read(parser)) match { - case Success(a) => a - case Failure(_) => - parser.revertToMark(mark) - orElseTypeAdapter.read(parser).asInstanceOf[A] - } - - def write[WIRE]( - t: A, - writer: Writer[WIRE], - out: mutable.Builder[WIRE, WIRE]): Unit = - attemptedTypeAdapter.write(t, writer, out) diff --git a/core/src/main/scala/co.blocke.scalajack/typeadapter/IntersectionTypeAdapter.scala b/core/src/main/scala/co.blocke.scalajack/typeadapter/IntersectionTypeAdapter.scala deleted file mode 100644 index e756c96f..00000000 --- a/core/src/main/scala/co.blocke.scalajack/typeadapter/IntersectionTypeAdapter.scala +++ /dev/null @@ -1,55 +0,0 @@ -package co.blocke.scalajack -package typeadapter - -import model._ -import co.blocke.scala_reflection._ -import co.blocke.scala_reflection.info.IntersectionInfo -import co.blocke.scala_reflection.impl.Clazzes._ - -import scala.collection.mutable.Builder -import scala.util.{ Failure, Success, Try } - -object IntersectionTypeAdapterFactory extends TypeAdapterFactory: - - def matches(concrete: RType): Boolean = - concrete match { - case _: IntersectionInfo => true - case _ => false - } - - def makeTypeAdapter(concrete: RType)(implicit taCache: TypeAdapterCache): TypeAdapter[_] = - val intersectionInfo = concrete.asInstanceOf[IntersectionInfo] - val leftInfo = intersectionInfo.leftType - val rightInfo = intersectionInfo.rightType - - val leftTypeAdapter = taCache.typeAdapterOf(leftInfo) - val rightTypeAdapter = taCache.typeAdapterOf(rightInfo) - - IntersectionTypeAdapter( - concrete, - leftTypeAdapter, - rightTypeAdapter) - - -/** NOTE: This parsing "and" behavior only works for traits. The Dotty Intersection type is - * likely broader than that, but traits is the main use case here. It is unclear (from a - * serialization perspective) how you would combine "fields" from, say, a primitive type. - */ -case class IntersectionTypeAdapter[L, R]( - info: RType, - leftTypeAdapter: TypeAdapter[L], - rightTypeAdapter: TypeAdapter[R])(implicit taCache: TypeAdapterCache) - extends TypeAdapter[L & R]: - - val syntheticTA = taCache.typeAdapterOf[L] - override def isStringish: Boolean = leftTypeAdapter.isStringish && rightTypeAdapter.isStringish - override def maybeStringish: Boolean = !isStringish - - def read(parser: Parser): L & R = - syntheticTA.read(parser).asInstanceOf[L & R] - - def write[WIRE]( - t: L & R, - writer: Writer[WIRE], - out: Builder[WIRE, WIRE]): Unit = - syntheticTA.write(t.asInstanceOf[L], writer, out) diff --git a/core/src/main/scala/co.blocke.scalajack/typeadapter/JavaPrimitives.scala b/core/src/main/scala/co.blocke.scalajack/typeadapter/JavaPrimitives.scala deleted file mode 100644 index 96002309..00000000 --- a/core/src/main/scala/co.blocke.scalajack/typeadapter/JavaPrimitives.scala +++ /dev/null @@ -1,237 +0,0 @@ -package co.blocke.scalajack -package typeadapter - -import model._ -import co.blocke.scala_reflection._ -import co.blocke.scala_reflection.info.JavaClassInfo -import co.blocke.scala_reflection.impl.PrimitiveType - -import java.math.BigDecimal -import java.math.BigInteger - -import model._ -import scala.collection.mutable -import scala.language.implicitConversions - -object JavaBigDecimalTypeAdapterFactory extends TypeAdapterFactory with TypeAdapter[BigDecimal] with ScalarTypeAdapter[BigDecimal]: - def matches(concrete: RType): Boolean = - concrete match { - case j: JavaClassInfo if j.name == "java.math.BigDecimal" => true - case j => false - } - def makeTypeAdapter(concrete: RType)(implicit taCache: TypeAdapterCache): TypeAdapter[BigDecimal] = this - - val info = RType.of[java.math.BigDecimal] - def read(parser: Parser): BigDecimal = - parser.expectNumber(true) match { - case null => null - case bd => new BigDecimal(bd) - } - def write[WIRE](t: BigDecimal, writer: Writer[WIRE], out: mutable.Builder[WIRE, WIRE]): Unit = - t match { - case null => writer.writeNull(out) - case _ => writer.writeDecimal(t, out) - } - -object JavaBigIntegerTypeAdapterFactory extends TypeAdapterFactory with TypeAdapter[BigInteger] with ScalarTypeAdapter[BigInteger]: - def matches(concrete: RType): Boolean = - concrete match { - case j: JavaClassInfo if j.name == "java.math.BigInteger" => true - case _ => false - } - def makeTypeAdapter(concrete: RType)(implicit taCache: TypeAdapterCache): TypeAdapter[BigInteger] = this - - val info = RType.of[java.math.BigInteger] - def read(parser: Parser): BigInteger = - parser.expectNumber(true) match { - case null => null - case bd => new BigInteger(bd) - } - def write[WIRE](t: BigInteger, writer: Writer[WIRE], out: mutable.Builder[WIRE, WIRE]): Unit = t match { - case null => writer.writeNull(out) - case _ => writer.writeBigInt(t, out) - } - - -object JavaBooleanTypeAdapterFactory extends TypeAdapterFactory with TypeAdapter[java.lang.Boolean] with ScalarTypeAdapter[java.lang.Boolean]: - def matches(concrete: RType): Boolean = concrete.infoClass == PrimitiveType.Java_Boolean.infoClass - def makeTypeAdapter(concrete: RType)(implicit taCache: TypeAdapterCache): TypeAdapter[java.lang.Boolean] = this - - val info = RType.of[java.lang.Boolean] - def read(parser: Parser): java.lang.Boolean = - if (parser.peekForNull) - null // Booleans are nullable in Java, but not in Scala - else - java.lang.Boolean.valueOf(parser.expectBoolean()) - def write[WIRE](t: java.lang.Boolean, writer: Writer[WIRE], out: mutable.Builder[WIRE, WIRE]): Unit = - t match { - case null => writer.writeNull(out) - case _ => writer.writeBoolean(t, out) - } - - -object JavaByteTypeAdapterFactory extends TypeAdapterFactory with TypeAdapter[java.lang.Byte] with ScalarTypeAdapter[java.lang.Byte]: - def matches(concrete: RType): Boolean = concrete.infoClass == PrimitiveType.Java_Byte.infoClass - def makeTypeAdapter(concrete: RType)(implicit taCache: TypeAdapterCache): TypeAdapter[java.lang.Byte] = this - - val info = RType.of[java.lang.Byte] - def read(parser: Parser): java.lang.Byte = - if (parser.peekForNull) - null // Bytes are nullable in Java, but not Scala - else - java.lang.Byte.valueOf(parser.expectNumber().toInt.toByte) - def write[WIRE](t: java.lang.Byte, writer: Writer[WIRE], out: mutable.Builder[WIRE, WIRE]): Unit = - t match { - case null => writer.writeNull(out) - case _ => writer.writeInt(t.toByte, out) - } - - -object JavaCharacterTypeAdapterFactory extends TypeAdapterFactory with TypeAdapter[java.lang.Character] with ScalarTypeAdapter[java.lang.Character]: - def matches(concrete: RType): Boolean = concrete.infoClass == PrimitiveType.Java_Char.infoClass - def makeTypeAdapter(concrete: RType)(implicit taCache: TypeAdapterCache): TypeAdapter[java.lang.Character] = this - - override def isStringish: Boolean = true - val info = RType.of[java.lang.Character] - def read(parser: Parser): java.lang.Character = - if (parser.peekForNull) - null - else - parser.expectString() match { - case "" => - parser.backspace() - throw new ScalaJackError( - parser.showError("Tried to read a Character but empty string found") - ) - case c => java.lang.Character.valueOf(c.toCharArray()(0)) - } - def write[WIRE](t: java.lang.Character, writer: Writer[WIRE], out: mutable.Builder[WIRE, WIRE]): Unit = - t match { - case null => writer.writeNull(out) - case _ => writer.writeString(t.toString, out) - } - - -object JavaDoubleTypeAdapterFactory extends TypeAdapterFactory with TypeAdapter[java.lang.Double] with ScalarTypeAdapter[java.lang.Double]: - def matches(concrete: RType): Boolean = concrete.infoClass == PrimitiveType.Java_Double.infoClass - def makeTypeAdapter(concrete: RType)(implicit taCache: TypeAdapterCache): TypeAdapter[java.lang.Double] = this - - val info = RType.of[java.lang.Double] - def read(parser: Parser): java.lang.Double = - if (parser.peekForNull) - null - else - java.lang.Double.valueOf(parser.expectNumber()) - def write[WIRE](t: java.lang.Double, writer: Writer[WIRE], out: mutable.Builder[WIRE, WIRE]): Unit = - t match { - case null => writer.writeNull(out) - case _ => writer.writeDouble(t, out) - } - - -object JavaFloatTypeAdapterFactory extends TypeAdapterFactory with TypeAdapter[java.lang.Float] with ScalarTypeAdapter[java.lang.Float]: - def matches(concrete: RType): Boolean = concrete.infoClass == PrimitiveType.Java_Float.infoClass - def makeTypeAdapter(concrete: RType)(implicit taCache: TypeAdapterCache): TypeAdapter[java.lang.Float] = this - - val info = RType.of[java.lang.Float] - def read(parser: Parser): java.lang.Float = - if (parser.peekForNull) - null - else - java.lang.Float.valueOf(parser.expectNumber()) - def write[WIRE](t: java.lang.Float, writer: Writer[WIRE], out: mutable.Builder[WIRE, WIRE]): Unit = - t match { - case null => writer.writeNull(out) - case _ => writer.writeDouble(util.FixFloat.capFloat(t), out) - } - - -object JavaIntegerTypeAdapterFactory extends TypeAdapterFactory with TypeAdapter[java.lang.Integer] with ScalarTypeAdapter[java.lang.Integer]: - def matches(concrete: RType): Boolean = concrete.infoClass == PrimitiveType.Java_Int.infoClass - def makeTypeAdapter(concrete: RType)(implicit taCache: TypeAdapterCache): TypeAdapter[java.lang.Integer] = this - - val info = RType.of[java.lang.Integer] - def read(parser: Parser): java.lang.Integer = - if (parser.peekForNull) - null - else - java.lang.Integer.valueOf(parser.expectNumber()) - def write[WIRE](t: java.lang.Integer, writer: Writer[WIRE], out: mutable.Builder[WIRE, WIRE]): Unit = - t match { - case null => writer.writeNull(out) - case _ => writer.writeInt(t, out) - } - - -object JavaLongTypeAdapterFactory extends TypeAdapterFactory with TypeAdapter[java.lang.Long] with ScalarTypeAdapter[java.lang.Long]: - def matches(concrete: RType): Boolean = concrete.infoClass == PrimitiveType.Java_Long.infoClass - def makeTypeAdapter(concrete: RType)(implicit taCache: TypeAdapterCache): TypeAdapter[java.lang.Long] = this - - val info = RType.of[java.lang.Long] - def read(parser: Parser): java.lang.Long = - if (parser.peekForNull) - null - else - java.lang.Long.valueOf(parser.expectNumber()) - def write[WIRE](t: java.lang.Long, writer: Writer[WIRE], out: mutable.Builder[WIRE, WIRE]): Unit = - t match { - case null => writer.writeNull(out) - case _ => writer.writeLong(t, out) - } - - -object JavaNumberTypeAdapterFactory extends TypeAdapterFactory with TypeAdapter[java.lang.Number] with ScalarTypeAdapter[java.lang.Number]: - def matches(concrete: RType): Boolean = concrete.infoClass == PrimitiveType.Java_Number.infoClass - def makeTypeAdapter(concrete: RType)(implicit taCache: TypeAdapterCache): TypeAdapter[java.lang.Number] = this - - val info = RType.of[java.lang.Number] - def read(parser: Parser): java.lang.Number = - if (parser.peekForNull) - null - else - scala.BigDecimal(parser.expectNumber()) match { - case d if d.isValidByte => java.lang.Byte.valueOf(d.toByteExact) - case d if d.isValidShort => java.lang.Short.valueOf(d.toShortExact) - case d if d.isValidInt => java.lang.Integer.valueOf(d.toIntExact) - case d if d.isValidLong => java.lang.Long.valueOf(d.toLongExact) - case d if d.isDecimalFloat => java.lang.Float.valueOf(d.toFloat) - case d if d.isDecimalDouble => java.lang.Double.valueOf(d.toDouble) - case d => d.bigDecimal - } - def write[WIRE](t: java.lang.Number, writer: Writer[WIRE], out: mutable.Builder[WIRE, WIRE]): Unit = - t match { - case null => writer.writeNull(out) - case t if t.isInstanceOf[java.lang.Integer] => - writer.writeInt(t.intValue, out) - case t if t.isInstanceOf[java.lang.Long] => - writer.writeLong(t.longValue, out) - case t if t.isInstanceOf[java.lang.Byte] => - writer.writeInt(t.byteValue, out) - case t if t.isInstanceOf[java.lang.Short] => - writer.writeInt(t.shortValue, out) - case t if t.isInstanceOf[java.lang.Float] => - writer.writeDouble(util.FixFloat.capFloat(t.floatValue), out) - case t if t.isInstanceOf[java.lang.Double] => - writer.writeDouble(t.doubleValue, out) - case t if t.isInstanceOf[java.math.BigInteger] => - writer.writeBigInt(BigInt(t.asInstanceOf[BigInteger]), out) - case t if t.isInstanceOf[java.math.BigDecimal] => - writer.writeDecimal(scala.BigDecimal(t.asInstanceOf[BigDecimal]), out) - } - - -object JavaShortTypeAdapterFactory extends TypeAdapterFactory with TypeAdapter[java.lang.Short] with ScalarTypeAdapter[java.lang.Short]: - def matches(concrete: RType): Boolean = concrete.infoClass == PrimitiveType.Java_Short.infoClass - def makeTypeAdapter(concrete: RType)(implicit taCache: TypeAdapterCache): TypeAdapter[java.lang.Short] = this - - val info = RType.of[java.lang.Short] - def read(parser: Parser): java.lang.Short = - if (parser.peekForNull) - null - else - java.lang.Short.valueOf(parser.expectNumber()) - def write[WIRE](t: java.lang.Short, writer: Writer[WIRE], out: mutable.Builder[WIRE, WIRE]): Unit = - t match { - case null => writer.writeNull(out) - case _ => writer.writeInt(t.intValue, out) - } diff --git a/core/src/main/scala/co.blocke.scalajack/typeadapter/MaybeStringWrapTypeAadapter.scala b/core/src/main/scala/co.blocke.scalajack/typeadapter/MaybeStringWrapTypeAadapter.scala deleted file mode 100644 index 72a577b8..00000000 --- a/core/src/main/scala/co.blocke.scalajack/typeadapter/MaybeStringWrapTypeAadapter.scala +++ /dev/null @@ -1,49 +0,0 @@ -package co.blocke.scalajack -package typeadapter - -import model._ -import co.blocke.scala_reflection._ -import co.blocke.scala_reflection.info._ -import co.blocke.scala_reflection.impl.Clazzes._ - -import scala.collection.mutable - -// A TypeAdapter for a type T, which is wrapped in a String, a.k.a. "stringified". -// This is used for JSON Map keys, which must be strings. -case class MaybeStringWrapTypeAdapter[T]( - jackFlavor: JackFlavor[_], - wrappedTypeAdapter: TypeAdapter[T], - emptyStringOk: Boolean = true - ) extends TypeAdapter[T] { - - private val javaEnumClazz = Class.forName("java.util.Enumeration") - - override def isStringish: Boolean = true - val info: RType = wrappedTypeAdapter.info - - def read(parser: Parser): T = - parser.expectString() match { - case null => null.asInstanceOf[T] - case s if s.isEmpty && !emptyStringOk => - parser.backspace() - throw new ScalaJackError( - parser.showError(s"Expected a ${wrappedTypeAdapter.info.name} here") - ) - case s => - wrappedTypeAdapter.read(parser.subParser(s.asInstanceOf[parser.WIRE])) - } - - def write[WIRE]( - t: T, - writer: Writer[WIRE], - out: mutable.Builder[WIRE, WIRE]): Unit = - t match { - case null => writer.writeNull(out) - case t if t.getClass <:< classOf[String] => writer.writeString(t.toString, out) - case e if e.getClass.getName =="scala.Enumeration$Val" && !jackFlavor.enumsAsInt => writer.writeString(t.toString, out) - case _: scala.reflect.Enum if !jackFlavor.enumsAsInt => writer.writeString(t.toString, out) - case t if t.getClass <:< javaEnumClazz && !jackFlavor.enumsAsInt => writer.writeString(t.toString, out) - case _ => - jackFlavor.stringWrapTypeAdapterFactory(wrappedTypeAdapter).write(t, writer, out) - } -} diff --git a/core/src/main/scala/co.blocke.scalajack/typeadapter/OptionTypeAdapter.scala b/core/src/main/scala/co.blocke.scalajack/typeadapter/OptionTypeAdapter.scala deleted file mode 100644 index d4de70c0..00000000 --- a/core/src/main/scala/co.blocke.scalajack/typeadapter/OptionTypeAdapter.scala +++ /dev/null @@ -1,118 +0,0 @@ -package co.blocke.scalajack -package typeadapter - -import model._ - -import scala.collection.mutable -import co.blocke.scala_reflection._ -import co.blocke.scala_reflection.info._ -import java.util.Optional - -/* - O, the exquisite pain of mapping Option (None) to something in JSON! - Our assumptions (which may be different from earlier versions of ScalaJack: - - * Normal: None is just missing. Doesn't read or write anything at all. Classic example is a class field value--it's - just "missing" from the JSON and understood to be None. - - * Map key fields: Element is dropped from Map, like List element behavior - - * Map value fields: Element is dropped from Map, like List element behavior - - * List elements: Normal, i.e. read/write nothing. None elements just disappear. NOTE this does mean that a read/render - cycle may not yield the same object, which is generally breaking a ScalaJack core behavior goal - - * Tuple elements: Place needs to be preserved in a Tuple, so None becomes JSON null. Really hate this option, but JSON - doesn't leave many choices here. - */ - -object OptionTypeAdapterFactory extends TypeAdapterFactory: - def matches(concrete: RType): Boolean = - concrete match { - case _: OptionInfo => true - case _ => false - } - def makeTypeAdapter(concrete: RType)(implicit taCache: TypeAdapterCache): TypeAdapter[_] = - val optiBase = concrete.asInstanceOf[OptionInfo] - val wrapped = optiBase.optionParamType match { - case c: TypeSymbolInfo => throw new ScalaJackError(s"Unexpected non-concrete type in option: ${c.name}") - case c => taCache.typeAdapterOf(c) - } - concrete match { - case opti: ScalaOptionInfo => OptionTypeAdapter(concrete, wrapped) - case jopti: JavaOptionalInfo => JavaOptionalTypeAdapter(concrete, wrapped) - } - - -case class OptionTypeAdapter[E]( - info: RType, - valueTypeAdapter: TypeAdapter[E], - nullIsNone: Boolean = false - ) extends TypeAdapter[Option[E]]: - - override def defaultValue: Option[Option[E]] = Some(None) - - override def isStringish: Boolean = valueTypeAdapter.isStringish - override def maybeStringish: Boolean = !valueTypeAdapter.isStringish - - def read(parser: Parser): Option[E] = - // We have to do some voodoo here and peek ahead for Null. Some types, e.g. Int, aren't nullable, - // but Option[Int] is nullable, so we can't trust the valueTypeAdapter to catch and handle null in - // these cases. - parser.peekForNull match { - case true if nullIsNone => None - case true => null - case _ => Some(valueTypeAdapter.read(parser)) - } - - def write[WIRE]( - t: Option[E], - writer: Writer[WIRE], - out: mutable.Builder[WIRE, WIRE]): Unit = - t match { - case null => writer.writeNull(out) - case Some(e) => valueTypeAdapter.write(e, writer, out) - case None if nullIsNone => writer.writeNull(out) - case None => - } - - def convertNullToNone(): OptionTypeAdapter[E] = this.copy(nullIsNone = true) - - - -case class JavaOptionalTypeAdapter[E]( - info: RType, - valueTypeAdapter: TypeAdapter[E], - nullIsNone: Boolean = false - ) extends TypeAdapter[Optional[E]]: - - val empty = Optional.empty[E]() - override def defaultValue: Option[Optional[E]] = Some(empty) - - override def isStringish: Boolean = valueTypeAdapter.isStringish - override def maybeStringish: Boolean = !valueTypeAdapter.isStringish - - def read(parser: Parser): Optional[E] = - // We have to do some voodoo here and peek ahead for Null. Some types, e.g. Int, aren't nullable, - // but Option[Int] is nullable, so we can't trust the valueTypeAdapter to catch and handle null in - // these cases. - parser.peekForNull match { - case true if nullIsNone => empty - case true => null - case _ => Optional.of[E](valueTypeAdapter.read(parser)) - } - - def write[WIRE]( - t: Optional[E], - writer: Writer[WIRE], - out: mutable.Builder[WIRE, WIRE]): Unit = - if t == null then - writer.writeNull(out) - else - if t.isPresent then - valueTypeAdapter.write(t.get, writer, out) - else if nullIsNone then - writer.writeNull(out) - // else write nothing - - def convertNullToNone(): JavaOptionalTypeAdapter[E] = this.copy(nullIsNone = true) \ No newline at end of file diff --git a/core/src/main/scala/co.blocke.scalajack/typeadapter/PermissiveJavaPrimitives.scala b/core/src/main/scala/co.blocke.scalajack/typeadapter/PermissiveJavaPrimitives.scala deleted file mode 100644 index e768210c..00000000 --- a/core/src/main/scala/co.blocke.scalajack/typeadapter/PermissiveJavaPrimitives.scala +++ /dev/null @@ -1,191 +0,0 @@ -package co.blocke.scalajack -package typeadapter - -import model._ -import co.blocke.scala_reflection.impl.Clazzes._ -import co.blocke.scala_reflection.impl.PrimitiveType -import co.blocke.scala_reflection.info._ -import co.blocke.scala_reflection._ -import scala.collection.mutable - -import java.math.BigDecimal -import java.math.BigInteger - - -object PermissiveJavaBigDecimalTypeAdapterFactory extends TypeAdapterFactory with TypeAdapter[BigDecimal] with ScalarTypeAdapter[BigDecimal]: - def matches(concrete: RType): Boolean = - concrete match { - case u: JavaClassInfo if u.infoClass.getName == "java.math.BigDecimal" => true - case _ => false - } - def makeTypeAdapter(concrete: RType)(implicit taCache: TypeAdapterCache): TypeAdapter[BigDecimal] = this - - val info = RType.of[scala.math.BigDecimal] - def read(parser: Parser): BigDecimal = - if (parser.nextIsString) - parser.jackFlavor - .stringWrapTypeAdapterFactory(JavaBigDecimalTypeAdapterFactory, emptyStringOk = false) - .read(parser) - else - JavaBigDecimalTypeAdapterFactory.read(parser) - - def write[WIRE](t: BigDecimal, writer: Writer[WIRE], out: mutable.Builder[WIRE, WIRE]): Unit = - JavaBigDecimalTypeAdapterFactory.write(t, writer, out) - - -object PermissiveJavaBigIntegerTypeAdapterFactory extends TypeAdapterFactory with TypeAdapter[BigInteger] with ScalarTypeAdapter[BigInteger]: - def matches(concrete: RType): Boolean = - concrete match { - case u: JavaClassInfo if u.infoClass.getName == "java.math.BigInteger" => true - case _ => false - } - def makeTypeAdapter(concrete: RType)(implicit taCache: TypeAdapterCache): TypeAdapter[BigInteger] = this - - val info = RType.of[java.math.BigInteger] - def read(parser: Parser): BigInteger = - if (parser.nextIsString) - parser.jackFlavor - .stringWrapTypeAdapterFactory(JavaBigIntegerTypeAdapterFactory, emptyStringOk = false) - .read(parser) - else - JavaBigIntegerTypeAdapterFactory.read(parser) - - def write[WIRE](t: BigInteger, writer: Writer[WIRE], out: mutable.Builder[WIRE, WIRE]): Unit = - JavaBigIntegerTypeAdapterFactory.write(t, writer, out) - - -object PermissiveJavaBooleanTypeAdapterFactory extends TypeAdapterFactory with TypeAdapter[java.lang.Boolean] with ScalarTypeAdapter[java.lang.Boolean]: - def matches(concrete: RType): Boolean = concrete.infoClass == PrimitiveType.Java_Boolean.infoClass - def makeTypeAdapter(concrete: RType)(implicit taCache: TypeAdapterCache): TypeAdapter[java.lang.Boolean] = this - - val info = RType.of[java.lang.Boolean] - def read(parser: Parser): java.lang.Boolean = - if (parser.nextIsString) - parser.jackFlavor - .stringWrapTypeAdapterFactory(JavaBooleanTypeAdapterFactory, emptyStringOk = false) - .read(parser) - else - JavaBooleanTypeAdapterFactory.read(parser) - - def write[WIRE](t: java.lang.Boolean, writer: Writer[WIRE], out: mutable.Builder[WIRE, WIRE]): Unit = - JavaBooleanTypeAdapterFactory.write(t, writer, out) - - -object PermissiveJavaByteTypeAdapterFactory extends TypeAdapterFactory with TypeAdapter[java.lang.Byte] with ScalarTypeAdapter[java.lang.Byte]: - def matches(concrete: RType): Boolean = concrete.infoClass == PrimitiveType.Java_Byte.infoClass - def makeTypeAdapter(concrete: RType)(implicit taCache: TypeAdapterCache): TypeAdapter[java.lang.Byte] = this - - val info = RType.of[java.lang.Byte] - def read(parser: Parser): java.lang.Byte = - if (parser.nextIsString) - parser.jackFlavor - .stringWrapTypeAdapterFactory(JavaByteTypeAdapterFactory, emptyStringOk = false) - .read(parser) - else - JavaByteTypeAdapterFactory.read(parser) - - def write[WIRE](t: java.lang.Byte, writer: Writer[WIRE], out: mutable.Builder[WIRE, WIRE]): Unit = - JavaByteTypeAdapterFactory.write(t, writer, out) - - -object PermissiveJavaDoubleTypeAdapterFactory extends TypeAdapterFactory with TypeAdapter[java.lang.Double] with ScalarTypeAdapter[java.lang.Double]: - def matches(concrete: RType): Boolean = concrete.infoClass == PrimitiveType.Java_Double.infoClass - def makeTypeAdapter(concrete: RType)(implicit taCache: TypeAdapterCache): TypeAdapter[java.lang.Double] = this - - val info = RType.of[java.lang.Double] - def read(parser: Parser): java.lang.Double = - if (parser.nextIsString) - parser.jackFlavor - .stringWrapTypeAdapterFactory(JavaDoubleTypeAdapterFactory, emptyStringOk = false) - .read(parser) - else - JavaDoubleTypeAdapterFactory.read(parser) - - def write[WIRE](t: java.lang.Double, writer: Writer[WIRE], out: mutable.Builder[WIRE, WIRE]): Unit = - JavaDoubleTypeAdapterFactory.write(t, writer, out) - - -object PermissiveJavaFloatTypeAdapterFactory extends TypeAdapterFactory with TypeAdapter[java.lang.Float] with ScalarTypeAdapter[java.lang.Float]: - def matches(concrete: RType): Boolean = concrete.infoClass == PrimitiveType.Java_Float.infoClass - def makeTypeAdapter(concrete: RType)(implicit taCache: TypeAdapterCache): TypeAdapter[java.lang.Float] = this - - val info = RType.of[java.lang.Float] - def read(parser: Parser): java.lang.Float = - if (parser.nextIsString) - parser.jackFlavor - .stringWrapTypeAdapterFactory(JavaFloatTypeAdapterFactory, emptyStringOk = false) - .read(parser) - else - JavaFloatTypeAdapterFactory.read(parser) - - def write[WIRE](t: java.lang.Float, writer: Writer[WIRE], out: mutable.Builder[WIRE, WIRE]): Unit = - JavaFloatTypeAdapterFactory.write(t, writer, out) - - -object PermissiveJavaIntTypeAdapterFactory extends TypeAdapterFactory with TypeAdapter[java.lang.Integer] with ScalarTypeAdapter[java.lang.Integer]: - def matches(concrete: RType): Boolean = concrete.infoClass == PrimitiveType.Java_Int.infoClass - def makeTypeAdapter(concrete: RType)(implicit taCache: TypeAdapterCache): TypeAdapter[java.lang.Integer] = this - - val info = RType.of[java.lang.Integer] - def read(parser: Parser): java.lang.Integer = - if (parser.nextIsString) - parser.jackFlavor - .stringWrapTypeAdapterFactory(JavaIntegerTypeAdapterFactory, emptyStringOk = false) - .read(parser) - else - JavaIntegerTypeAdapterFactory.read(parser) - - def write[WIRE](t: java.lang.Integer, writer: Writer[WIRE], out: mutable.Builder[WIRE, WIRE]): Unit = - JavaIntegerTypeAdapterFactory.write(t, writer, out) - - -object PermissiveJavaLongTypeAdapterFactory extends TypeAdapterFactory with TypeAdapter[java.lang.Long] with ScalarTypeAdapter[java.lang.Long]: - def matches(concrete: RType): Boolean = concrete.infoClass == PrimitiveType.Java_Long.infoClass - def makeTypeAdapter(concrete: RType)(implicit taCache: TypeAdapterCache): TypeAdapter[java.lang.Long] = this - - val info = RType.of[java.lang.Long] - def read(parser: Parser): java.lang.Long = - if (parser.nextIsString) - parser.jackFlavor - .stringWrapTypeAdapterFactory(JavaLongTypeAdapterFactory, emptyStringOk = false) - .read(parser) - else - JavaLongTypeAdapterFactory.read(parser) - - def write[WIRE](t: java.lang.Long, writer: Writer[WIRE], out: mutable.Builder[WIRE, WIRE]): Unit = - JavaLongTypeAdapterFactory.write(t, writer, out) - - -object PermissiveJavaNumberTypeAdapterFactory extends TypeAdapterFactory with TypeAdapter[java.lang.Number] with ScalarTypeAdapter[java.lang.Number]: - def matches(concrete: RType): Boolean = concrete.infoClass == PrimitiveType.Java_Number.infoClass - def makeTypeAdapter(concrete: RType)(implicit taCache: TypeAdapterCache): TypeAdapter[java.lang.Number] = this - - val info = RType.of[java.lang.Number] - def read(parser: Parser): java.lang.Number = - if (parser.nextIsString) - parser.jackFlavor - .stringWrapTypeAdapterFactory(JavaNumberTypeAdapterFactory, emptyStringOk = false) - .read(parser) - else - JavaNumberTypeAdapterFactory.read(parser) - - def write[WIRE](t: java.lang.Number, writer: Writer[WIRE], out: mutable.Builder[WIRE, WIRE]): Unit = - JavaNumberTypeAdapterFactory.write(t, writer, out) - - -object PermissiveJavaShortTypeAdapterFactory extends TypeAdapterFactory with TypeAdapter[java.lang.Short] with ScalarTypeAdapter[java.lang.Short]: - def matches(concrete: RType): Boolean = concrete.infoClass == PrimitiveType.Java_Short.infoClass - def makeTypeAdapter(concrete: RType)(implicit taCache: TypeAdapterCache): TypeAdapter[java.lang.Short] = this - - val info = RType.of[java.lang.Short] - def read(parser: Parser): java.lang.Short = - if (parser.nextIsString) - parser.jackFlavor - .stringWrapTypeAdapterFactory(JavaShortTypeAdapterFactory, emptyStringOk = false) - .read(parser) - else - JavaShortTypeAdapterFactory.read(parser) - - def write[WIRE](t: java.lang.Short, writer: Writer[WIRE], out: mutable.Builder[WIRE, WIRE]): Unit = - JavaShortTypeAdapterFactory.write(t, writer, out) - \ No newline at end of file diff --git a/core/src/main/scala/co.blocke.scalajack/typeadapter/PermissiveScalaPrimitives.scala b/core/src/main/scala/co.blocke.scalajack/typeadapter/PermissiveScalaPrimitives.scala deleted file mode 100644 index c0e65efe..00000000 --- a/core/src/main/scala/co.blocke.scalajack/typeadapter/PermissiveScalaPrimitives.scala +++ /dev/null @@ -1,169 +0,0 @@ -package co.blocke.scalajack -package typeadapter - -import model._ -import co.blocke.scala_reflection.impl.Clazzes._ -import co.blocke.scala_reflection.info._ -import co.blocke.scala_reflection.impl.PrimitiveType -import co.blocke.scala_reflection._ -import scala.collection.mutable - -object PermissiveBigDecimalTypeAdapterFactory extends TypeAdapterFactory with TypeAdapter[BigDecimal] with ScalarTypeAdapter[BigDecimal]: - def matches(concrete: RType): Boolean = - concrete match { - case u: Scala2Info if u.infoClass.getName == "scala.math.BigDecimal" => true - case _ => false - } - def makeTypeAdapter(concrete: RType)(implicit taCache: TypeAdapterCache): TypeAdapter[BigDecimal] = this - - val info = RType.of[scala.math.BigDecimal] - def read(parser: Parser): BigDecimal = - if (parser.nextIsString) - parser.jackFlavor - .stringWrapTypeAdapterFactory(BigDecimalTypeAdapterFactory, emptyStringOk = false) - .read(parser) - else - BigDecimalTypeAdapterFactory.read(parser) - - def write[WIRE](t: BigDecimal, writer: Writer[WIRE], out: mutable.Builder[WIRE, WIRE]): Unit = - BigDecimalTypeAdapterFactory.write(t, writer, out) - - -object PermissiveBigIntTypeAdapterFactory extends TypeAdapterFactory with TypeAdapter[BigInt] with ScalarTypeAdapter[BigInt]: - def matches(concrete: RType): Boolean = - concrete match { - case u: Scala2Info if u.infoClass.getName == "scala.math.BigInt" => true - case _ => false - } - def makeTypeAdapter(concrete: RType)(implicit taCache: TypeAdapterCache): TypeAdapter[BigInt] = this - - val info = RType.of[scala.math.BigInt] - def read(parser: Parser): BigInt = - if (parser.nextIsString) - parser.jackFlavor - .stringWrapTypeAdapterFactory(BigIntTypeAdapterFactory, emptyStringOk = false) - .read(parser) - else - BigIntTypeAdapterFactory.read(parser) - - def write[WIRE](t: BigInt, writer: Writer[WIRE], out: mutable.Builder[WIRE, WIRE]): Unit = - BigIntTypeAdapterFactory.write(t, writer, out) - - -object PermissiveBooleanTypeAdapterFactory extends TypeAdapterFactory with TypeAdapter[Boolean] with ScalarTypeAdapter[Boolean]: - def matches(concrete: RType): Boolean = concrete.infoClass == PrimitiveType.Scala_Boolean.infoClass - def makeTypeAdapter(concrete: RType)(implicit taCache: TypeAdapterCache): TypeAdapter[Boolean] = this - - val info = RType.of[Boolean] - def read(parser: Parser): Boolean = - if (parser.nextIsString) - parser.jackFlavor - .stringWrapTypeAdapterFactory(BooleanTypeAdapterFactory, emptyStringOk = false) - .read(parser) - else - BooleanTypeAdapterFactory.read(parser) - - def write[WIRE](t: Boolean, writer: Writer[WIRE], out: mutable.Builder[WIRE, WIRE]): Unit = - BooleanTypeAdapterFactory.write(t, writer, out) - - -object PermissiveByteTypeAdapterFactory extends TypeAdapterFactory with TypeAdapter[Byte] with ScalarTypeAdapter[Byte]: - def matches(concrete: RType): Boolean = concrete.infoClass == PrimitiveType.Scala_Byte.infoClass - def makeTypeAdapter(concrete: RType)(implicit taCache: TypeAdapterCache): TypeAdapter[Byte] = this - - val info = RType.of[Byte] - def read(parser: Parser): Byte = - if (parser.nextIsString) - parser.jackFlavor - .stringWrapTypeAdapterFactory(ByteTypeAdapterFactory, emptyStringOk = false) - .read(parser) - else - ByteTypeAdapterFactory.read(parser) - - def write[WIRE](t: Byte, writer: Writer[WIRE], out: mutable.Builder[WIRE, WIRE]): Unit = - ByteTypeAdapterFactory.write(t, writer, out) - - -object PermissiveDoubleTypeAdapterFactory extends TypeAdapterFactory with TypeAdapter[Double] with ScalarTypeAdapter[Double]: - def matches(concrete: RType): Boolean = concrete.infoClass == PrimitiveType.Scala_Double.infoClass - def makeTypeAdapter(concrete: RType)(implicit taCache: TypeAdapterCache): TypeAdapter[Double] = this - - val info = RType.of[Double] - def read(parser: Parser): Double = - if (parser.nextIsString) - parser.jackFlavor - .stringWrapTypeAdapterFactory(DoubleTypeAdapterFactory, emptyStringOk = false) - .read(parser) - else - DoubleTypeAdapterFactory.read(parser) - - def write[WIRE](t: Double, writer: Writer[WIRE], out: mutable.Builder[WIRE, WIRE]): Unit = - DoubleTypeAdapterFactory.write(t, writer, out) - - -object PermissiveFloatTypeAdapterFactory extends TypeAdapterFactory with TypeAdapter[Float] with ScalarTypeAdapter[Float]: - def matches(concrete: RType): Boolean = concrete.infoClass == PrimitiveType.Scala_Float.infoClass - def makeTypeAdapter(concrete: RType)(implicit taCache: TypeAdapterCache): TypeAdapter[Float] = this - - val info = RType.of[Float] - def read(parser: Parser): Float = - if (parser.nextIsString) - parser.jackFlavor - .stringWrapTypeAdapterFactory(FloatTypeAdapterFactory, emptyStringOk = false) - .read(parser) - else - FloatTypeAdapterFactory.read(parser) - - def write[WIRE](t: Float, writer: Writer[WIRE], out: mutable.Builder[WIRE, WIRE]): Unit = - FloatTypeAdapterFactory.write(t, writer, out) - - -object PermissiveIntTypeAdapterFactory extends TypeAdapterFactory with TypeAdapter[Int] with ScalarTypeAdapter[Int]: - def matches(concrete: RType): Boolean = concrete.infoClass == PrimitiveType.Scala_Int.infoClass - def makeTypeAdapter(concrete: RType)(implicit taCache: TypeAdapterCache): TypeAdapter[Int] = this - - val info = RType.of[Int] - def read(parser: Parser): Int = - if (parser.nextIsString) - parser.jackFlavor - .stringWrapTypeAdapterFactory(IntTypeAdapterFactory, emptyStringOk = false) - .read(parser) - else - IntTypeAdapterFactory.read(parser) - - def write[WIRE](t: Int, writer: Writer[WIRE], out: mutable.Builder[WIRE, WIRE]): Unit = - IntTypeAdapterFactory.write(t, writer, out) - - -object PermissiveLongTypeAdapterFactory extends TypeAdapterFactory with TypeAdapter[Long] with ScalarTypeAdapter[Long]: - def matches(concrete: RType): Boolean = concrete.infoClass == PrimitiveType.Scala_Long.infoClass - def makeTypeAdapter(concrete: RType)(implicit taCache: TypeAdapterCache): TypeAdapter[Long] = this - - val info = RType.of[Long] - def read(parser: Parser): Long = - if (parser.nextIsString) - parser.jackFlavor - .stringWrapTypeAdapterFactory(LongTypeAdapterFactory, emptyStringOk = false) - .read(parser) - else - LongTypeAdapterFactory.read(parser) - - def write[WIRE](t: Long, writer: Writer[WIRE], out: mutable.Builder[WIRE, WIRE]): Unit = - LongTypeAdapterFactory.write(t, writer, out) - - -object PermissiveShortTypeAdapterFactory extends TypeAdapterFactory with TypeAdapter[Short] with ScalarTypeAdapter[Short]: - def matches(concrete: RType): Boolean = concrete.infoClass == PrimitiveType.Scala_Short.infoClass - def makeTypeAdapter(concrete: RType)(implicit taCache: TypeAdapterCache): TypeAdapter[Short] = this - - val info = RType.of[Short] - def read(parser: Parser): Short = - if (parser.nextIsString) - parser.jackFlavor - .stringWrapTypeAdapterFactory(ShortTypeAdapterFactory, emptyStringOk = false) - .read(parser) - else - ShortTypeAdapterFactory.read(parser) - - def write[WIRE](t: Short, writer: Writer[WIRE], out: mutable.Builder[WIRE, WIRE]): Unit = - ShortTypeAdapterFactory.write(t, writer, out) diff --git a/core/src/main/scala/co.blocke.scalajack/typeadapter/ScalaPrimitives.scala b/core/src/main/scala/co.blocke.scalajack/typeadapter/ScalaPrimitives.scala deleted file mode 100644 index 03817884..00000000 --- a/core/src/main/scala/co.blocke.scalajack/typeadapter/ScalaPrimitives.scala +++ /dev/null @@ -1,236 +0,0 @@ -package co.blocke.scalajack -package typeadapter - -import model._ - -import co.blocke.scala_reflection.impl.Clazzes._ -import co.blocke.scala_reflection.impl.PrimitiveType -import co.blocke.scala_reflection.info._ -import co.blocke.scala_reflection._ - -import org.apache.commons.codec.binary.Base64 -import scala.collection.mutable -import scala.language.implicitConversions - - -object BigDecimalTypeAdapterFactory extends TypeAdapterFactory with TypeAdapter[BigDecimal] with ScalarTypeAdapter[BigDecimal]: - def matches(concrete: RType): Boolean = - concrete match { - case u: Scala2Info if u.infoClass.getName == "scala.math.BigDecimal" => true - case _ => false - } - def makeTypeAdapter(concrete: RType)(implicit taCache: TypeAdapterCache): TypeAdapter[BigDecimal] = this - - val info = RType.of[scala.math.BigDecimal] - def read(parser: Parser): BigDecimal = - parser.expectNumber(true) match { - case null => null - case bd => BigDecimal(bd) - } - def write[WIRE](t: BigDecimal, writer: Writer[WIRE], out: mutable.Builder[WIRE, WIRE]): Unit = - writer.writeDecimal(t, out) - - -object BigIntTypeAdapterFactory extends TypeAdapterFactory with TypeAdapter[BigInt] with ScalarTypeAdapter[BigInt]: - def matches(concrete: RType): Boolean = - concrete match { - case u: Scala2Info if u.infoClass.getName == "scala.math.BigInt" => true - case _ => false - } - def makeTypeAdapter(concrete: RType)(implicit taCache: TypeAdapterCache): TypeAdapter[BigInt] = this - - val info = RType.of[scala.math.BigInt] - def read(parser: Parser): BigInt = - parser.expectNumber(true) match { - case null => null - case bd => BigInt(bd) - } - def write[WIRE](t: BigInt, writer: Writer[WIRE], out: mutable.Builder[WIRE, WIRE]): Unit = t match { - case null => writer.writeNull(out) - case _ => writer.writeBigInt(t, out) - } - - -object BinaryTypeAdapterFactory extends TypeAdapterFactory with TypeAdapter[Array[Byte]] with ScalarTypeAdapter[Array[Byte]]: - def matches(concrete: RType): Boolean = - concrete match { - case ArrayInfo("[B", ic) if ic.infoClass == PrimitiveType.Scala_Byte.infoClass => true - case c => false - } - def makeTypeAdapter(concrete: RType)(implicit taCache: TypeAdapterCache): TypeAdapter[Array[Byte]] = this - - val info = RType.of[Array[Byte]] - def read(parser: Parser): Array[Byte] = - parser.expectString() match { - case null => null - case s: String => Base64.decodeBase64(s) - } - - def write[WIRE](t: Array[Byte], writer: Writer[WIRE], out: mutable.Builder[WIRE, WIRE]): Unit = - t match { - case null => writer.writeNull(out) - case _ => writer.writeString(Base64.encodeBase64String(t), out) - } - - -object BooleanTypeAdapterFactory extends TypeAdapterFactory with TypeAdapter[Boolean] with ScalarTypeAdapter[Boolean]: - def matches(concrete: RType): Boolean = concrete.infoClass == PrimitiveType.Scala_Boolean.infoClass - def makeTypeAdapter(concrete: RType)(implicit taCache: TypeAdapterCache): TypeAdapter[Boolean] = this - - val info = RType.of[Boolean] - def read(parser: Parser): Boolean = parser.expectBoolean() - def write[WIRE](t: Boolean, writer: Writer[WIRE], out: mutable.Builder[WIRE, WIRE]): Unit = - writer.writeBoolean(t, out) - - -object ByteTypeAdapterFactory extends TypeAdapterFactory with TypeAdapter[Byte] with ScalarTypeAdapter[Byte]: - def matches(concrete: RType): Boolean = concrete.infoClass == PrimitiveType.Scala_Byte.infoClass - def makeTypeAdapter(concrete: RType)(implicit taCache: TypeAdapterCache): TypeAdapter[Byte] = this - - val info = RType.of[Byte] - def read(parser: Parser): Byte = - Option(parser.expectNumber()) - .flatMap(_.toByteOption) - .getOrElse { - parser.backspace() - throw new ScalaJackError( - parser.showError("Cannot parse an Byte from value") - ) - } - def write[WIRE](t: Byte, writer: Writer[WIRE], out: mutable.Builder[WIRE, WIRE]): Unit = - writer.writeInt(t, out) - - -object CharTypeAdapterFactory extends TypeAdapterFactory with TypeAdapter[Char] with ScalarTypeAdapter[Char]: - def matches(concrete: RType): Boolean = concrete.infoClass == PrimitiveType.Scala_Char.infoClass - def makeTypeAdapter(concrete: RType)(implicit taCache: TypeAdapterCache): TypeAdapter[Char] = this - - override def isStringish: Boolean = true - val info = RType.of[Char] - def read(parser: Parser): Char = - parser.expectString() match { - case null => - parser.backspace() - throw new ScalaJackError( - parser.showError("A Char typed value cannot be null") - ) - case "" => - parser.backspace() - throw new ScalaJackError( - parser.showError("Tried to read a Char but empty string found") - ) - case s => s.charAt(0) - } - def write[WIRE](t: Char, writer: Writer[WIRE], out: mutable.Builder[WIRE, WIRE]): Unit = - writer.writeString(t.toString, out) - - -object DoubleTypeAdapterFactory extends TypeAdapterFactory with TypeAdapter[Double] with ScalarTypeAdapter[Double]: - def matches(concrete: RType): Boolean = concrete.infoClass == PrimitiveType.Scala_Double.infoClass - def makeTypeAdapter(concrete: RType)(implicit taCache: TypeAdapterCache): TypeAdapter[Double] = this - - val info = RType.of[Double] - def read(parser: Parser): Double = - Option( - parser - .expectNumber()) - .flatMap(_.toDoubleOption) - .getOrElse { - parser.backspace() - throw new ScalaJackError( - parser.showError("Cannot parse an Double from value") - ) - } - def write[WIRE](t: Double, writer: Writer[WIRE], out: mutable.Builder[WIRE, WIRE]): Unit = - writer.writeDouble(t, out) - - -object FloatTypeAdapterFactory extends TypeAdapterFactory with TypeAdapter[Float] with ScalarTypeAdapter[Float]: - def matches(concrete: RType): Boolean = concrete.infoClass == PrimitiveType.Scala_Float.infoClass - def makeTypeAdapter(concrete: RType)(implicit taCache: TypeAdapterCache): TypeAdapter[Float] = this - - val info = RType.of[Float] - def read(parser: Parser): Float = - Option( - parser - .expectNumber()) - .flatMap(_.toFloatOption) - .getOrElse { - parser.backspace() - throw new ScalaJackError( - parser.showError("Cannot parse an Float from value") - ) - } - def write[WIRE](t: Float, writer: Writer[WIRE], out: mutable.Builder[WIRE, WIRE]): Unit = - writer.writeDouble(util.FixFloat.capFloat(t), out) - - -object IntTypeAdapterFactory extends TypeAdapterFactory with TypeAdapter[Int] with ScalarTypeAdapter[Int]: - def matches(concrete: RType): Boolean = concrete.infoClass == PrimitiveType.Scala_Int.infoClass - def makeTypeAdapter(concrete: RType)(implicit taCache: TypeAdapterCache): TypeAdapter[Int] = this - - val info = RType.of[Int] - def read(parser: Parser): Int = - Option( - parser - .expectNumber()) - .flatMap(_.toIntOption) - .getOrElse { - parser.backspace() - throw new ScalaJackError( - parser.showError("Cannot parse an Int from value") - ) - } - def write[WIRE](t: Int, writer: Writer[WIRE], out: mutable.Builder[WIRE, WIRE]): Unit = - writer.writeInt(t, out) - - -object LongTypeAdapterFactory extends TypeAdapterFactory with TypeAdapter[Long] with ScalarTypeAdapter[Long]: - def matches(concrete: RType): Boolean = concrete.infoClass == PrimitiveType.Scala_Long.infoClass - def makeTypeAdapter(concrete: RType)(implicit taCache: TypeAdapterCache): TypeAdapter[Long] = this - - val info = RType.of[Long] - def read(parser: Parser): Long = - Option( - parser - .expectNumber()) - .flatMap(_.toLongOption) - .getOrElse { - parser.backspace() - throw new ScalaJackError( - parser.showError("Cannot parse an Long from value") - ) - } - def write[WIRE](t: Long, writer: Writer[WIRE], out: mutable.Builder[WIRE, WIRE]): Unit = - writer.writeLong(t, out) - - -object ShortTypeAdapterFactory extends TypeAdapterFactory with TypeAdapter[Short] with ScalarTypeAdapter[Short]: - def matches(concrete: RType): Boolean = concrete.infoClass == PrimitiveType.Scala_Short.infoClass - def makeTypeAdapter(concrete: RType)(implicit taCache: TypeAdapterCache): TypeAdapter[Short] = this - - val info = RType.of[Short] - def read(parser: Parser): Short = - Option( - parser - .expectNumber()) - .flatMap(_.toShortOption) - .getOrElse { - parser.backspace() - throw new ScalaJackError( - parser.showError("Cannot parse an Short from value") - ) - } - def write[WIRE](t: Short, writer: Writer[WIRE], out: mutable.Builder[WIRE, WIRE]): Unit = - writer.writeInt(t, out) - - -object StringTypeAdapterFactory extends TypeAdapterFactory with TypeAdapter[String] with ScalarTypeAdapter[String]: - def matches(concrete: RType): Boolean = concrete.infoClass == PrimitiveType.Scala_String.infoClass - def makeTypeAdapter(concrete: RType)(implicit taCache: TypeAdapterCache): TypeAdapter[String] = this - - override def isStringish: Boolean = true - val info = RType.of[String] - def read(parser: Parser): String = parser.expectString() - def write[WIRE](t: String, writer: Writer[WIRE], out: mutable.Builder[WIRE, WIRE]): Unit = - writer.writeString(t, out) diff --git a/core/src/main/scala/co.blocke.scalajack/typeadapter/SealedTraitTypeAdapter.scala b/core/src/main/scala/co.blocke.scalajack/typeadapter/SealedTraitTypeAdapter.scala deleted file mode 100644 index ffaa78b4..00000000 --- a/core/src/main/scala/co.blocke.scalajack/typeadapter/SealedTraitTypeAdapter.scala +++ /dev/null @@ -1,118 +0,0 @@ -package co.blocke.scalajack -package typeadapter - -import model._ - -import scala.collection.mutable -import co.blocke.scala_reflection._ -import co.blocke.scala_reflection.impl.Clazzes._ -import co.blocke.scala_reflection.info._ - -object SealedTraitTypeAdapterFactory extends TypeAdapterFactory: - def matches(concrete: RType): Boolean = - concrete match { - case _: SealedTraitInfo => true - case _ => false - } - def makeTypeAdapter(concrete: RType)(implicit taCache: TypeAdapterCache): TypeAdapter[_] = - if concrete.asInstanceOf[SealedTraitInfo].children.head.isInstanceOf[ObjectInfo] then - CaseObjectTypeAdapter( - concrete, - concrete.asInstanceOf[SealedTraitInfo].children.map(_.asInstanceOf[ObjectInfo].infoClass.getSimpleName).toList) - else - val typeAdapters = concrete.asInstanceOf[SealedTraitInfo].children.map(c => c -> taCache.typeAdapterOf(c)).toMap - SealedTraitTypeAdapter(taCache.jackFlavor, concrete, typeAdapters) - - -case class SealedTraitTypeAdapter[T]( - jackFlavor: JackFlavor[_], - info: RType, - typeAdapters: Map[RType, TypeAdapter[_]] - ) extends TypeAdapter[T]: - - val sealedInfo = info.asInstanceOf[SealedTraitInfo] - - def read(parser: Parser): T = - val savedReader = parser.mark() - if (parser.peekForNull) - null.asInstanceOf[T] - else { - val readFieldNames = parser.expectMap[String, Any, Map[String, Any]]( - jackFlavor.stringTypeAdapter, - jackFlavor.anyTypeAdapter, - Map.newBuilder[String,Any] - ).keySet - sealedInfo.children.filter( - _.asInstanceOf[ScalaCaseClassInfo].fields.map(_.name).toSet.intersect(readFieldNames).size == readFieldNames.size - ) match { - case setOfOne if setOfOne.size == 1 => - parser.revertToMark(savedReader) - typeAdapters(setOfOne.head).read(parser).asInstanceOf[T] - - case emptySet if emptySet.isEmpty => - parser.backspace() - throw new ScalaJackError( - parser.showError( - s"No sub-classes of ${info.name} match field names $readFieldNames" - ) - ) - - case _ => - // $COVERAGE-OFF$Should be impossible--here for safety. Something to trigger this would be ambiguous and would then be detected as a WrappedSealedTraitTypeAdapter, not here. - parser.backspace() - throw new ScalaJackError( - parser.showError( - s"Multiple sub-classes of ${info.name} match field names $readFieldNames" - ) - ) - // $COVERAGE-ON$ - } - } - - def write[WIRE]( - t: T, - writer: Writer[WIRE], - out: mutable.Builder[WIRE, WIRE]): Unit = - t match { - case null => writer.writeString(null, out) - case _ => - sealedInfo.children.find(f => t.getClass <:< f.infoClass) match { - case Some(implementation) => typeAdapters(implementation).asInstanceOf[TypeAdapter[T]]write(t, writer, out) - // $COVERAGE-OFF$Should be impossible, but including here for safety. Can't think of how to actaully trigger this for testing. - case None => - throw new IllegalStateException( - s"Given object ($t) doesn't seem to be a sealed trait." - ) - // $COVERAGE-ON$ - } - } - -case class CaseObjectTypeAdapter[T]( - info: RType, - values: List[String] - ) extends TypeAdapter[T]: - - val sealedInfo = info.asInstanceOf[SealedTraitInfo] - - def read(parser: Parser): T = - parser.expectString() match { - case null => null.asInstanceOf[T] - case s: String if values.contains(s) => - val simpleNameLen = info.infoClass.getSimpleName.length+1 - val clazz = Class.forName(info.infoClass.getName.dropRight(simpleNameLen) + "." + s + "$") - val objInstance = clazz.getField("MODULE$").get(null).asInstanceOf[T] - objInstance - case x => - parser.backspace() - throw new ScalaJackError(parser.showError(s"Expected a valid subclass of ${info.name} but got $x") - ) - } - - def write[WIRE]( - t: T, - writer: Writer[WIRE], - out: mutable.Builder[WIRE, WIRE]): Unit = - t match { - case null => writer.writeString(null, out) - case _ => writer.writeString(t.toString, out) - } \ No newline at end of file diff --git a/core/src/main/scala/co.blocke.scalajack/typeadapter/StringWrapTypeAdapter.scala b/core/src/main/scala/co.blocke.scalajack/typeadapter/StringWrapTypeAdapter.scala deleted file mode 100644 index d8fceb1f..00000000 --- a/core/src/main/scala/co.blocke.scalajack/typeadapter/StringWrapTypeAdapter.scala +++ /dev/null @@ -1,44 +0,0 @@ -package co.blocke.scalajack -package typeadapter - -import model._ -import co.blocke.scala_reflection._ -import co.blocke.scala_reflection.info._ -import co.blocke.scala_reflection.impl.Clazzes._ - -import scala.collection.mutable - -// A TypeAdapter for a type T, which is wrapped in a String, a.k.a. "stringified". -// This is used for JSON Map keys, which must be strings. -case class StringWrapTypeAdapter[T]( - wrappedTypeAdapter: TypeAdapter[T], - emptyStringOk: Boolean = true - ) extends TypeAdapter[T] { - - override def isStringish: Boolean = true - val info: RType = wrappedTypeAdapter.info - - def read(parser: Parser): T = - parser.expectString() match { - case null => null.asInstanceOf[T] - case s if s.isEmpty && !emptyStringOk => - parser.backspace() - throw new ScalaJackError( - parser.showError(s"Expected a ${wrappedTypeAdapter.info.name} here") - ) - case s => - wrappedTypeAdapter.read(parser.subParser(s.asInstanceOf[parser.WIRE])) - } - - def write[WIRE]( - t: T, - writer: Writer[WIRE], - out: mutable.Builder[WIRE, WIRE]): Unit = - val stringBuilder = co.blocke.scalajack.model.StringBuilder() - wrappedTypeAdapter.write( - t, - writer, - stringBuilder.asInstanceOf[mutable.Builder[Any, WIRE]] - ) - writer.writeString(stringBuilder.result(), out) -} diff --git a/core/src/main/scala/co.blocke.scalajack/typeadapter/TimePrimitives.scala b/core/src/main/scala/co.blocke.scalajack/typeadapter/TimePrimitives.scala deleted file mode 100644 index 6d5cc430..00000000 --- a/core/src/main/scala/co.blocke.scalajack/typeadapter/TimePrimitives.scala +++ /dev/null @@ -1,323 +0,0 @@ -package co.blocke.scalajack -package typeadapter - -import co.blocke.scala_reflection.impl.Clazzes._ -import co.blocke.scala_reflection.info._ -import co.blocke.scala_reflection._ - -import scala.collection.mutable -import scala.util.{ Failure, Success, Try } -import java.time._ -import java.time.format.DateTimeFormatter._ - -import model._ - - object DurationTypeAdapterFactory extends TypeAdapterFactory with TypeAdapter[Duration]: - def matches(concrete: RType): Boolean = - concrete match { - case u: JavaClassInfo if u.infoClass.getName == "java.time.Duration" => true - case _ => false - } - def makeTypeAdapter(concrete: RType)(implicit taCache: TypeAdapterCache): TypeAdapter[Duration] = this - val info = RType.of[Duration] - override def isStringish: Boolean = true - - def read(parser: Parser): Duration = - parser.expectString() match { - case null => null - case s => - Try(Duration.parse(s)) match { - case Success(d) => d - case Failure(u) => - parser.backspace() - throw new ScalaJackError( - parser.showError(s"""Failed to parse Duration from input '$s'""") - ) - } - } - - def write[WIRE]( - t: Duration, - writer: Writer[WIRE], - out: mutable.Builder[WIRE, WIRE]): Unit = - t match { - case null => writer.writeNull(out) - case _ => writer.writeString(t.toString, out) - } - - -object InstantTypeAdapterFactory extends TypeAdapterFactory with TypeAdapter[Instant]: - def matches(concrete: RType): Boolean = - concrete match { - case u: JavaClassInfo if u.infoClass.getName == "java.time.Instant" => true - case _ => false - } - def makeTypeAdapter(concrete: RType)(implicit taCache: TypeAdapterCache): TypeAdapter[Instant] = this - val info = RType.of[Instant] - override def isStringish: Boolean = true - - def read(parser: Parser): Instant = - parser.expectString() match { - case null => null - case s => - Try(Instant.parse(s)) match { - case Success(d) => d - case Failure(u) => - parser.backspace() - throw new ScalaJackError( - parser.showError(s"""Failed to parse Instant from input '$s'""") - ) - } - } - - def write[WIRE]( - t: Instant, - writer: Writer[WIRE], - out: mutable.Builder[WIRE, WIRE]): Unit = - t match { - case null => writer.writeNull(out) - case _ => writer.writeString(t.toString, out) - } - - -object LocalDateTimeTypeAdapterFactory extends TypeAdapterFactory with TypeAdapter[LocalDateTime]: - def matches(concrete: RType): Boolean = - concrete match { - case u: JavaClassInfo if u.infoClass.getName == "java.time.LocalDateTime" => true - case _ => false - } - def makeTypeAdapter(concrete: RType)(implicit taCache: TypeAdapterCache): TypeAdapter[LocalDateTime] = this - val info = RType.of[LocalDateTime] - override def isStringish: Boolean = true - - def read(parser: Parser): LocalDateTime = - parser.expectString() match { - case null => null - case s => - Try(LocalDateTime.parse(s, ISO_LOCAL_DATE_TIME)) match { - case Success(d) => d - case Failure(u) => - parser.backspace() - throw new ScalaJackError( - parser - .showError(s"""Failed to parse LocalDateTime from input '$s'""") - ) - } - } - - def write[WIRE]( - t: LocalDateTime, - writer: Writer[WIRE], - out: mutable.Builder[WIRE, WIRE]): Unit = - t match { - case null => writer.writeNull(out) - case _ => writer.writeString(t.format(ISO_LOCAL_DATE_TIME), out) - } - - -object LocalDateTypeAdapterFactory extends TypeAdapterFactory with TypeAdapter[LocalDate]: - def matches(concrete: RType): Boolean = - concrete match { - case u: JavaClassInfo if u.infoClass.getName == "java.time.LocalDate" => true - case _ => false - } - def makeTypeAdapter(concrete: RType)(implicit taCache: TypeAdapterCache): TypeAdapter[LocalDate] = this - val info = RType.of[LocalDate] - override def isStringish: Boolean = true - - def read(parser: Parser): LocalDate = - parser.expectString() match { - case null => null - case s => - Try(LocalDate.parse(s, ISO_LOCAL_DATE)) match { - case Success(d) => d - case Failure(u) => - parser.backspace() - throw new ScalaJackError( - parser.showError(s"""Failed to parse LocalDate from input '$s'""") - ) - } - } - - def write[WIRE]( - t: LocalDate, - writer: Writer[WIRE], - out: mutable.Builder[WIRE, WIRE]): Unit = - t match { - case null => writer.writeNull(out) - case _ => writer.writeString(t.format(ISO_LOCAL_DATE), out) - } - - -object LocalTimeTypeAdapterFactory extends TypeAdapterFactory with TypeAdapter[LocalTime]: - def matches(concrete: RType): Boolean = - concrete match { - case u: JavaClassInfo if u.infoClass.getName == "java.time.LocalTime" => true - case _ => false - } - def makeTypeAdapter(concrete: RType)(implicit taCache: TypeAdapterCache): TypeAdapter[LocalTime] = this - val info = RType.of[LocalTime] - override def isStringish: Boolean = true - - def read(parser: Parser): LocalTime = - parser.expectString() match { - case null => null - case s => - Try(LocalTime.parse(s, ISO_LOCAL_TIME)) match { - case Success(d) => d - case Failure(u) => - parser.backspace() - throw new ScalaJackError( - parser.showError(s"""Failed to parse LocalTime from input '$s'""") - ) - } - } - - def write[WIRE]( - t: LocalTime, - writer: Writer[WIRE], - out: mutable.Builder[WIRE, WIRE]): Unit = - t match { - case null => writer.writeNull(out) - case _ => writer.writeString(t.format(ISO_LOCAL_TIME), out) - } - - -object OffsetDateTimeTypeAdapterFactory extends TypeAdapterFactory with TypeAdapter[OffsetDateTime]: - def matches(concrete: RType): Boolean = - concrete match { - case u: JavaClassInfo if u.infoClass.getName == "java.time.OffsetDateTime" => true - case _ => false - } - def makeTypeAdapter(concrete: RType)(implicit taCache: TypeAdapterCache): TypeAdapter[OffsetDateTime] = this - val info = RType.of[OffsetDateTime] - override def isStringish: Boolean = true - - def read(parser: Parser): OffsetDateTime = - parser.expectString() match { - case null => null - case s => - Try(OffsetDateTime.parse(s, ISO_OFFSET_DATE_TIME)) match { - case Success(d) => d - case Failure(u) => - parser.backspace() - throw new ScalaJackError( - parser.showError( - s"""Failed to parse OffsetDateTime from input '$s'""" - ) - ) - } - } - - def write[WIRE]( - t: OffsetDateTime, - writer: Writer[WIRE], - out: mutable.Builder[WIRE, WIRE]): Unit = - t match { - case null => writer.writeNull(out) - case _ => writer.writeString(t.format(ISO_OFFSET_DATE_TIME), out) - } - - -object OffsetTimeTypeAdapterFactory extends TypeAdapterFactory with TypeAdapter[OffsetTime]: - def matches(concrete: RType): Boolean = - concrete match { - case u: JavaClassInfo if u.infoClass.getName == "java.time.OffsetTime" => true - case _ => false - } - def makeTypeAdapter(concrete: RType)(implicit taCache: TypeAdapterCache): TypeAdapter[OffsetTime] = this - val info = RType.of[OffsetTime] - override def isStringish: Boolean = true - - def read(parser: Parser): OffsetTime = - parser.expectString() match { - case null => null - case s => - Try(OffsetTime.parse(s, ISO_OFFSET_TIME)) match { - case Success(d) => d - case Failure(u) => - parser.backspace() - throw new ScalaJackError( - parser - .showError(s"""Failed to parse OffsetTime from input '$s'""") - ) - } - } - - def write[WIRE]( - t: OffsetTime, - writer: Writer[WIRE], - out: mutable.Builder[WIRE, WIRE]): Unit = - t match { - case null => writer.writeNull(out) - case _ => writer.writeString(t.format(ISO_OFFSET_TIME), out) - } - - -object PeriodTypeAdapterFactory extends TypeAdapterFactory with TypeAdapter[Period]: - def matches(concrete: RType): Boolean = - concrete match { - case u: JavaClassInfo if u.infoClass.getName == "java.time.Period" => true - case _ => false - } - def makeTypeAdapter(concrete: RType)(implicit taCache: TypeAdapterCache): TypeAdapter[Period] = this - val info = RType.of[Period] - override def isStringish: Boolean = true - - def read(parser: Parser): Period = - parser.expectString() match { - case null => null - case s => - Try(Period.parse(s)) match { - case Success(d) => d - case Failure(u) => - parser.backspace() - throw new ScalaJackError( - parser.showError(s"""Failed to parse Period from input '$s'""") - ) - } - } - - def write[WIRE]( - t: Period, - writer: Writer[WIRE], - out: mutable.Builder[WIRE, WIRE]): Unit = - t match { - case null => writer.writeNull(out) - case _ => writer.writeString(t.toString, out) - } - - -object ZonedDateTimeTypeAdapterFactory extends TypeAdapterFactory with TypeAdapter[ZonedDateTime]: - def matches(concrete: RType): Boolean = - concrete match { - case u: JavaClassInfo if u.infoClass.getName == "java.time.ZonedDateTime" => true - case _ => false - } - def makeTypeAdapter(concrete: RType)(implicit taCache: TypeAdapterCache): TypeAdapter[ZonedDateTime] = this - val info = RType.of[ZonedDateTime] - override def isStringish: Boolean = true - - def read(parser: Parser): ZonedDateTime = - parser.expectString() match { - case null => null - case s => - Try(ZonedDateTime.parse(s, ISO_ZONED_DATE_TIME)) match { - case Success(d) => d - case Failure(u) => - parser.backspace() - throw new ScalaJackError( - parser - .showError(s"""Failed to parse ZonedDateTime from input '$s'""") - ) - } - } - - def write[WIRE]( - t: ZonedDateTime, - writer: Writer[WIRE], - out: mutable.Builder[WIRE, WIRE]): Unit = - t match { - case null => writer.writeNull(out) - case _ => writer.writeString(t.format(ISO_ZONED_DATE_TIME), out) - } diff --git a/core/src/main/scala/co.blocke.scalajack/typeadapter/TraitTypeAdapter.scala b/core/src/main/scala/co.blocke.scalajack/typeadapter/TraitTypeAdapter.scala deleted file mode 100644 index 03c7da1a..00000000 --- a/core/src/main/scala/co.blocke.scalajack/typeadapter/TraitTypeAdapter.scala +++ /dev/null @@ -1,75 +0,0 @@ -package co.blocke.scalajack -package typeadapter - -import model._ -import classes._ -import co.blocke.scala_reflection._ -import co.blocke.scala_reflection.info.TraitInfo - -import scala.collection.mutable - -// This should come *after* SealedTraitTypeAdapter in the Context factory list, as all sealed traits are -// also traits, and this factory would pick them all up, hiding the sealed ones. -// -object TraitTypeAdapterFactory extends TypeAdapterFactory: - - def matches(concrete: RType): Boolean = - concrete match { - case _: TraitInfo => true - case _ => false - } - - def makeTypeAdapter(concrete: RType)(implicit taCache: TypeAdapterCache): TypeAdapter[_] = - TraitTypeAdapter(concrete, taCache.jackFlavor.getHintLabelFor(concrete)) - - -case class TraitTypeAdapter[T]( - info: RType, - hintLabel: String -)(implicit taCache: TypeAdapterCache) extends TypeAdapter[T] with Classish: - - inline def calcTA(c: Class[_]): ClassTypeAdapterBase[T] = - taCache.typeAdapterOf(RType.inTermsOf(c, info.asInstanceOf[TraitInfo])).asInstanceOf[ClassTypeAdapterBase[T]] - - // The battle plan here is: Scan the keys of the object looking for type typeHintField. Perform any (optional) - // re-working of the hint value via hintModFn. Look up the correct concete TypeAdapter based on the now-known type - // and re-read the object as a case class. - def read(parser: Parser): T = - if (parser.peekForNull) - null.asInstanceOf[T] - else { - val concreteClass = parser.scanForHint( - hintLabel, - taCache.jackFlavor.hintValueModifiers.getOrElse(info.name, DefaultHintModifier) - ).asInstanceOf[Class[T]] - val ccta = calcTA(concreteClass) - ccta.read(parser).asInstanceOf[T] - } - - def write[WIRE]( - t: T, - writer: Writer[WIRE], - out: mutable.Builder[WIRE, WIRE]): Unit = - if (t == null) - writer.writeNull(out) - else { - val ccta = calcTA(t.getClass) - val hintValue = taCache.jackFlavor.hintValueModifiers - .getOrElse(info.name, DefaultHintModifier) - .unapply(t.getClass.getName) - writer.writeObject( - t, - ccta.orderedFieldNames, - ccta.fieldMembersByName, - out, - List( - ( - hintLabel, - ExtraFieldValue( - hintValue, - taCache.jackFlavor.stringTypeAdapter - ) - ) - ) - ) - } \ No newline at end of file diff --git a/core/src/main/scala/co.blocke.scalajack/typeadapter/TryTypeAdapter.scala b/core/src/main/scala/co.blocke.scalajack/typeadapter/TryTypeAdapter.scala deleted file mode 100644 index d06fb79c..00000000 --- a/core/src/main/scala/co.blocke.scalajack/typeadapter/TryTypeAdapter.scala +++ /dev/null @@ -1,48 +0,0 @@ -package co.blocke.scalajack -package typeadapter - -import model._ - -import scala.collection.mutable -import co.blocke.scala_reflection._ -import co.blocke.scala_reflection.info._ -import scala.util.{Try, Success, Failure} - - -object TryTypeAdapterFactory extends TypeAdapterFactory: - def matches(concrete: RType): Boolean = - concrete match { - case _: TryInfo => true - case _ => false - } - def makeTypeAdapter(concrete: RType)(implicit taCache: TypeAdapterCache): TypeAdapter[_] = - TryTypeAdapter(concrete, taCache.typeAdapterOf(concrete.asInstanceOf[TryInfo].tryType), taCache.jackFlavor) - - -case class TryTypeAdapter[T]( - info: RType, - valueTypeAdapter: TypeAdapter[T], - jackFlavor: JackFlavor[_] - ) extends TypeAdapter[Try[T]]: - - def read(parser: Parser): Try[T] = - val saved = parser.mark() - Try { valueTypeAdapter.read(parser) } match { - case self @ Success(_) => self - case Failure(cause) => - parser.revertToMark(saved) - val f = Failure( - new ScalaJackValueError(jackFlavor.anyTypeAdapter.read(parser), cause) - ) - f - } - - def write[WIRE]( - t: Try[T], - writer: Writer[WIRE], - out: mutable.Builder[WIRE, WIRE]): Unit = - t match { - case Success(v) => valueTypeAdapter.write(v, writer, out) - case Failure(e: ScalaJackValueError) => jackFlavor.anyTypeAdapter.write(e.value, writer, out) - case Failure(e) => throw e - } diff --git a/core/src/main/scala/co.blocke.scalajack/typeadapter/TupleTypeAdapter.scala b/core/src/main/scala/co.blocke.scalajack/typeadapter/TupleTypeAdapter.scala deleted file mode 100644 index 1cd2c13f..00000000 --- a/core/src/main/scala/co.blocke.scalajack/typeadapter/TupleTypeAdapter.scala +++ /dev/null @@ -1,54 +0,0 @@ -package co.blocke.scalajack -package typeadapter - -import model._ -import co.blocke.scala_reflection._ -import co.blocke.scala_reflection.info.TupleInfo -import java.lang.reflect.Method -import scala.collection.mutable -import scala.util.matching.Regex - -object TupleTypeAdapterFactory extends TypeAdapterFactory: - - private val tupleFullName: Regex = """scala.Tuple(\d+)""".r - - def matches(concrete: RType): Boolean = - concrete match { - case ti: TupleInfo => true - case _ => false - } - - def makeTypeAdapter(concrete: RType)(implicit taCache: TypeAdapterCache): TypeAdapter[_] = - val fieldTAs = concrete.asInstanceOf[TupleInfo].tupleTypes.map{ f => - taCache.typeAdapterOf(f) match { - case ota: OptionTypeAdapter[_] => ota.copy(nullIsNone = true) - case jota: JavaOptionalTypeAdapter[_] => jota.copy(nullIsNone = true) - case other => other - } - }.toList - val writeFn = (t: Product) => fieldTAs.zip(t.productIterator) - TupleTypeAdapter(concrete, writeFn, fieldTAs, concrete.infoClass.getConstructors.head) - - -case class TupleTypeAdapter[T]( - info: RType, - writeFn: (Product) => List[(TypeAdapter[_], Any)], - fieldTAs: List[TypeAdapter[_]], - constructor: java.lang.reflect.Constructor[T] - ) extends TypeAdapter[T] with Collectionish { - - def read(parser: Parser): T = - if (parser.peekForNull) then - null.asInstanceOf[T] - else - constructor.newInstance(parser.expectTuple(fieldTAs): _*).asInstanceOf[T] - - def write[WIRE]( - t: T, - writer: Writer[WIRE], - out: mutable.Builder[WIRE, WIRE]): Unit = - if (t == null) - writer.writeNull(out) - else - writer.writeTuple(t, writeFn, out) -} diff --git a/core/src/main/scala/co.blocke.scalajack/typeadapter/UUIDTypeAdapter.scala b/core/src/main/scala/co.blocke.scalajack/typeadapter/UUIDTypeAdapter.scala deleted file mode 100644 index 47817076..00000000 --- a/core/src/main/scala/co.blocke.scalajack/typeadapter/UUIDTypeAdapter.scala +++ /dev/null @@ -1,44 +0,0 @@ -package co.blocke.scalajack -package typeadapter - -import model._ -import co.blocke.scala_reflection._ -import co.blocke.scala_reflection.impl.Clazzes._ -import co.blocke.scala_reflection.info.JavaClassInfo -import java.util.UUID -import scala.collection.mutable -import scala.util.{ Failure, Success, Try } - -object UUIDTypeAdapterFactory extends TypeAdapterFactory with TypeAdapter[UUID]: - override def isStringish: Boolean = true - val uuidClass = classOf[UUID] - def matches(concrete: RType): Boolean = - concrete match { - case j: JavaClassInfo if j.infoClass <:< uuidClass => true - case _ => false - } - def makeTypeAdapter(concrete: RType)(implicit taCache: TypeAdapterCache): TypeAdapter[UUID] = this - - val info = RType.of[java.util.UUID] - def read(parser: Parser): UUID = - val u = parser.expectString() - if (u == null) - null - else { - Try(UUID.fromString(u)) match { - case Success(uuid) => uuid - case Failure(uuid) => - parser.backspace() - throw new ScalaJackError( - parser.showError(s"Failed to create UUID value from parsed text ${u}") - ) - } - } - - def write[WIRE]( - t: UUID, - writer: Writer[WIRE], - out: mutable.Builder[WIRE, WIRE]): Unit = t match { - case null => writer.writeNull(out) - case _ => writer.writeString(t.toString, out) - } diff --git a/core/src/main/scala/co.blocke.scalajack/typeadapter/UnionTypeAdapter.scala b/core/src/main/scala/co.blocke.scalajack/typeadapter/UnionTypeAdapter.scala deleted file mode 100644 index e11e35f1..00000000 --- a/core/src/main/scala/co.blocke.scalajack/typeadapter/UnionTypeAdapter.scala +++ /dev/null @@ -1,67 +0,0 @@ -package co.blocke.scalajack -package typeadapter - -import model._ -import co.blocke.scala_reflection._ -import co.blocke.scala_reflection.info.UnionInfo -import co.blocke.scala_reflection.impl.Clazzes._ - -import scala.collection.mutable.Builder -import scala.util.{ Failure, Success, Try } - -object UnionTypeAdapterFactory extends TypeAdapterFactory: - - def matches(concrete: RType): Boolean = - concrete match { - case _: UnionInfo => true - case _ => false - } - - def makeTypeAdapter(concrete: RType)(implicit taCache: TypeAdapterCache): TypeAdapter[_] = - val unionInfo = concrete.asInstanceOf[UnionInfo] - val leftInfo = unionInfo.leftType - val rightInfo = unionInfo.rightType - - val leftTypeAdapter = taCache.typeAdapterOf(leftInfo) - val rightTypeAdapter = taCache.typeAdapterOf(rightInfo) - - UnionTypeAdapter( - concrete, - leftTypeAdapter, - rightTypeAdapter) - - -case class UnionTypeAdapter[L, R]( - info: RType, - leftTypeAdapter: TypeAdapter[L], - rightTypeAdapter: TypeAdapter[R])(implicit taCache: TypeAdapterCache) - extends TypeAdapter[L | R] { - - override def isStringish: Boolean = leftTypeAdapter.isStringish && rightTypeAdapter.isStringish - override def maybeStringish: Boolean = !isStringish - - def read(parser: Parser): L | R = { - val savedReader = parser.mark() - Try(leftTypeAdapter.read(parser)) match { - case Success(leftValue) => leftValue.asInstanceOf[L] - case Failure(_) => // Left parse failed... try Right - parser.revertToMark(savedReader) - Try(rightTypeAdapter.read(parser)) match { - case Success(rightValue) => rightValue.asInstanceOf[R] - case Failure(x) => - parser.backspace() - throw new ScalaJackError( parser.showError(s"Failed to read any values for union type") ) - } - } - } - - def write[WIRE]( - t: L | R, - writer: Writer[WIRE], - out: Builder[WIRE, WIRE]): Unit = - val trialBuilder = taCache.jackFlavor.getBuilder.asInstanceOf[scala.collection.mutable.Builder[WIRE,WIRE]] - if Try(leftTypeAdapter.write(t.asInstanceOf[L], writer, trialBuilder)).isFailure then - rightTypeAdapter.write(t.asInstanceOf[R], writer, out) - else - out += trialBuilder.result -} diff --git a/core/src/main/scala/co.blocke.scalajack/typeadapter/ValueClassTypeAdapter.scala b/core/src/main/scala/co.blocke.scalajack/typeadapter/ValueClassTypeAdapter.scala deleted file mode 100644 index cde223f4..00000000 --- a/core/src/main/scala/co.blocke.scalajack/typeadapter/ValueClassTypeAdapter.scala +++ /dev/null @@ -1,43 +0,0 @@ -package co.blocke.scalajack -package typeadapter - -import model._ -import co.blocke.scala_reflection.info._ -import co.blocke.scala_reflection._ -import scala.collection.mutable - -object ValueClassTypeAdapterFactory extends TypeAdapterFactory: - def matches(concrete: RType): Boolean = concrete match { - case c: ScalaCaseClassInfo if c.isValueClass => true - case c: ScalaClassInfo if c.isValueClass => true - case _ => false - } - - def makeTypeAdapter(concrete: RType)(implicit taCache: TypeAdapterCache): TypeAdapter[_] = - val elementType = concrete.asInstanceOf[ClassInfo].fields(0).fieldType - val field0 = concrete match { - case c: ScalaCaseClassInfo => c.fields(0).asInstanceOf[ScalaFieldInfo] - case c: ScalaClassInfo => c.fields(0).asInstanceOf[ScalaFieldInfo] - } - ValueClassTypeAdapter(concrete, field0, taCache.typeAdapterOf(elementType)) - - -case class ValueClassTypeAdapter[VC, Value]( - info: RType, - field0: ScalaFieldInfo, - elementTypeAdapter: TypeAdapter[Value] -) extends TypeAdapter[VC] { - - // For wrapping map keys - override def isStringish: Boolean = elementTypeAdapter.isStringish - override def maybeStringish: Boolean = !elementTypeAdapter.isStringish - - def read(parser: Parser): VC = - info.asInstanceOf[ClassInfo].infoClass.getConstructors.head.newInstance(List(elementTypeAdapter.read(parser).asInstanceOf[Object]):_*).asInstanceOf[VC] - - def write[WIRE]( - t: VC, - writer: Writer[WIRE], - out: mutable.Builder[WIRE, WIRE]): Unit = - elementTypeAdapter.write(t.getClass.getMethod(field0.name).invoke(t).asInstanceOf[Value], writer, out) -} diff --git a/core/src/main/scala/co.blocke.scalajack/typeadapter/classes/CaseClassTypeAdapter.scala b/core/src/main/scala/co.blocke.scalajack/typeadapter/classes/CaseClassTypeAdapter.scala deleted file mode 100644 index fd22927e..00000000 --- a/core/src/main/scala/co.blocke.scalajack/typeadapter/classes/CaseClassTypeAdapter.scala +++ /dev/null @@ -1,41 +0,0 @@ -package co.blocke.scalajack -package typeadapter -package classes - -import model._ -import co.blocke.scala_reflection.info._ -import co.blocke.scala_reflection._ - -import scala.collection.mutable - - -case class CaseClassTypeAdapter[T]( - info: RType, - fieldMembersByName: Map[String, ClassFieldMember[_,_]], - argsTemplate: Array[Object], - fieldBitsTemplate: mutable.BitSet, - typeMembersByName: Map[String, TypeMemberInfo], - orderedFieldNames: List[String], - dbCollectionName: Option[String] = None -)(implicit taCache: TypeAdapterCache) extends ScalaClassTypeAdapter[T]: - - override val isCaseClass = true; - - private val classInfo = info.asInstanceOf[ScalaCaseClassInfo] - - inline def constructWith(args: List[Object]): T = - val const = classInfo.infoClass.getConstructors.head // <-- NOTE: head here isn't bullet-proof, but a generally safe assumption for case classes. (Req because of arg typing mess.) - if (classInfo.typeMembers.nonEmpty) then - val originalArgTypes = classInfo.fields.map(_.fieldType.infoClass) - const.newInstance(args:_*).asInstanceOf[T] - else - const.newInstance(args:_*).asInstanceOf[T] - - def _read_createInstance(args: List[Object], foundBits: mutable.BitSet, captured: java.util.HashMap[String, _]): T = - val asBuilt = constructWith(args) - if isSJCapture then - asBuilt.asInstanceOf[SJCapture].captured = captured - asBuilt - - def _read_updateFieldMembers( fmbn: Map[String, ClassFieldMember[_,_]]): ScalaClassTypeAdapter[T] = - this.copy(fieldMembersByName = fmbn) diff --git a/core/src/main/scala/co.blocke.scalajack/typeadapter/classes/ClassTypeAdapterBase.scala b/core/src/main/scala/co.blocke.scalajack/typeadapter/classes/ClassTypeAdapterBase.scala deleted file mode 100644 index 457b2d8f..00000000 --- a/core/src/main/scala/co.blocke.scalajack/typeadapter/classes/ClassTypeAdapterBase.scala +++ /dev/null @@ -1,25 +0,0 @@ -package co.blocke.scalajack -package typeadapter -package classes - -import model._ -import co.blocke.scala_reflection._ -import co.blocke.scala_reflection.info._ - -import scala.collection.mutable - -// For case classes and Java/Scala plain classes, but not traits -trait ClassTypeAdapterBase[T] extends TypeAdapter[T] with Classish: - val info: RType - val argsTemplate: Array[Object] - val fieldBitsTemplate: mutable.BitSet - val isSJCapture: Boolean - val fieldMembersByName: Map[String, ClassFieldMember[_,_]] - val isCaseClass: Boolean = false - val orderedFieldNames: List[String] - val dbCollectionName: Option[String] - - def dbKeys: List[ClassFieldMember[_,_]] = - fieldMembersByName.values.toList - .filter(_.dbKeyIndex.isDefined) - .sortBy(_.dbKeyIndex.get) \ No newline at end of file diff --git a/core/src/main/scala/co.blocke.scalajack/typeadapter/classes/JavaClassTypeAdapter.scala b/core/src/main/scala/co.blocke.scalajack/typeadapter/classes/JavaClassTypeAdapter.scala deleted file mode 100644 index bd2b48bb..00000000 --- a/core/src/main/scala/co.blocke.scalajack/typeadapter/classes/JavaClassTypeAdapter.scala +++ /dev/null @@ -1,116 +0,0 @@ -package co.blocke.scalajack -package typeadapter -package classes - -import model._ - -import scala.collection.mutable -import co.blocke.scala_reflection._ -import co.blocke.scala_reflection.info._ -import scala.util.Try - -object JavaClassTypeAdapterFactory extends TypeAdapterFactory: - def matches(concrete: RType): Boolean = - concrete match { - case _: JavaClassInfo => true - case _ => false - } - - def makeTypeAdapter(concrete: RType)(implicit taCache: TypeAdapterCache): TypeAdapter[_] = - val classInfo = concrete.asInstanceOf[ClassInfo] - - // Filter out any ignored fields and re-index them all - val fieldsWeCareAbout = classInfo.fields.filterNot(_.annotations.contains(IGNORE)).zipWithIndex.map{ (f,idx) => f.reIndex(idx) } - - val bits = mutable.BitSet() - val args = new Array[Object](fieldsWeCareAbout.size) - - Try(classInfo.asInstanceOf[JavaClassInfo].infoClass.getConstructor()).toOption.orElse( - throw new ScalaJackError("ScalaJack does not support Java classes with a non-empty constructor.") - ) - - val const = classInfo.asInstanceOf[JavaClassInfo].infoClass.getConstructors.head - val phantomInstance = const.newInstance() // used to get default/initially-set values - - val fieldMembersByName = - fieldsWeCareAbout.map { f => - f.fieldType match { - case c: TypeSymbolInfo => throw new ScalaJackError(s"Concrete type expected for class ${concrete.name} field ${f.name}. ${c.getClass.getName} was found.") - case c => - bits += f.index - val fieldMapName = f.annotations.get(CHANGE_ANNO).map(_("name")) - val classFieldMember = ClassFieldMember( - f, - taCache.typeAdapterOf(c), - classInfo.infoClass, - f.annotations.get(DB_KEY).map(_.getOrElse("index","0").toInt), - fieldMapName - ) - if classFieldMember.isOptional then // filter out @Optional annotated fields - bits -= f.index - args(f.index) = f.asInstanceOf[JavaFieldInfo].valueAccessor.invoke(phantomInstance) - fieldMapName.getOrElse(f.name) -> classFieldMember - } - }.toMap - - // Exctract Collection name annotation if present (for plain classes) - val dbCollectionAnnotation = classInfo.annotations.get(DB_COLLECTION).map(_("name")) - - JavaClassTypeAdapter( - concrete, - args, - bits, - fieldMembersByName, - fieldsWeCareAbout.map( f => f.annotations.get(CHANGE_ANNO).map(_("name")).getOrElse(f.name)).toList, - dbCollectionAnnotation - ) - - -case class JavaClassTypeAdapter[J]( - info: RType, - argsTemplate: Array[Object], - fieldBitsTemplate: mutable.BitSet, - fieldMembersByName: Map[String, ClassFieldMember[_,_]], - orderedFieldNames: List[String], - dbCollectionName: Option[String] - )(implicit taCache: TypeAdapterCache) extends ClassTypeAdapterBase[J]: - - val javaClassInfo = info.asInstanceOf[JavaClassInfo] - val isSJCapture = javaClassInfo.hasMixin(SJ_CAPTURE) - - def read(parser: Parser): J = - val (foundBits, args, captured) = parser.expectObject( - this, - taCache.jackFlavor.defaultHint - ) - val testBits = fieldBitsTemplate.collect{ - case b if !foundBits.contains(b) => b - } - if (testBits.isEmpty) then - val const = javaClassInfo.infoClass.getConstructors.head - val asBuilt = const.newInstance().asInstanceOf[J] - if isSJCapture then - asBuilt.asInstanceOf[SJCapture].captured = captured - fieldMembersByName.values.map( f => f.info.asInstanceOf[JavaFieldInfo].valueSetter.invoke(asBuilt, args(f.info.index)) ) - asBuilt - else - parser.backspace() - throw new ScalaJackError( - parser.showError( - s"Class ${info.name} missing required fields: " + testBits - .map(b => orderedFieldNames(b)) - .mkString(", ") - ) - ) - - def write[WIRE]( - t: J, - writer: Writer[WIRE], - out: mutable.Builder[WIRE, WIRE]): Unit = - val extras = scala.collection.mutable.ListBuffer.empty[(String, ExtraFieldValue[_])] - writer.writeObject( - t, - orderedFieldNames, - fieldMembersByName, - out, - extras.toList) \ No newline at end of file diff --git a/core/src/main/scala/co.blocke.scalajack/typeadapter/classes/NonCaseClassTypeAdapter.scala b/core/src/main/scala/co.blocke.scalajack/typeadapter/classes/NonCaseClassTypeAdapter.scala deleted file mode 100644 index e87096e1..00000000 --- a/core/src/main/scala/co.blocke.scalajack/typeadapter/classes/NonCaseClassTypeAdapter.scala +++ /dev/null @@ -1,52 +0,0 @@ -package co.blocke.scalajack -package typeadapter -package classes - -import model._ -import co.blocke.scala_reflection.info._ -import co.blocke.scala_reflection._ -import scala.collection.mutable - - -case class NonCaseClassTypeAdapter[T]( - info: RType, - fieldMembersByName: Map[String, ClassFieldMember[_,_]], - argsTemplate: Array[Object], - fieldBitsTemplate: mutable.BitSet, - typeMembersByName: Map[String, TypeMemberInfo], - orderedFieldNames: List[String], - nonConstructorFields: List[ClassFieldMember[_,_]], - dbCollectionName: Option[String] -)(implicit taCache: TypeAdapterCache) extends ScalaClassTypeAdapter[T]: - - private val classInfo = info.asInstanceOf[ScalaClassInfo] - - inline def fieldName(f: FieldInfo): String = f.annotations.get(CHANGE_ANNO).map(_("name")).getOrElse(f.name) - - def _read_createInstance(args: List[Object], foundBits: mutable.BitSet, captured: java.util.HashMap[String, _]): T = - // Build base object - val asBuilt = - val const = classInfo.infoClass.getConstructors.head - const.newInstance(args.take(classInfo.fields.size):_*).asInstanceOf[T] - // Now call all the non-constructor setters for fields we populate - nonConstructorFields.collect{ - // make sure f is known--in one special case it will not be: "captured" field for SJCapture should be ignored - case f if fieldMembersByName.contains( fieldName(f.info) ) && foundBits.contains(f.info.index) => - // If field is a parameter 'T', the field type for the method is Object - val isTrait = fieldMembersByName(fieldName(f.info)).valueTypeAdapter.isInstanceOf[TraitTypeAdapter[_]] - val fieldMethodType = f.info.originalSymbol.match { - case Some(s) if !isTrait => classOf[Object] - case _ => fieldMembersByName(fieldName(f.info)).valueTypeAdapter.info.infoClass - } - val setter = classInfo.infoClass.getMethod(f.info.name+"_$eq", fieldMethodType ) - args(f.info.index) match { - case m: java.lang.reflect.Method => setter.invoke(asBuilt, m.invoke(asBuilt)) - case thing => setter.invoke(asBuilt, thing) - } - } - if isSJCapture then - asBuilt.asInstanceOf[SJCapture].captured = captured - asBuilt - - def _read_updateFieldMembers( fmbn: Map[String, ClassFieldMember[_,_]]): ScalaClassTypeAdapter[T] = - this.copy(fieldMembersByName = fmbn) diff --git a/core/src/main/scala/co.blocke.scalajack/typeadapter/classes/ScalaClassTypeAdapter.scala b/core/src/main/scala/co.blocke.scalajack/typeadapter/classes/ScalaClassTypeAdapter.scala deleted file mode 100644 index 7a5a4958..00000000 --- a/core/src/main/scala/co.blocke.scalajack/typeadapter/classes/ScalaClassTypeAdapter.scala +++ /dev/null @@ -1,161 +0,0 @@ -package co.blocke.scalajack -package typeadapter -package classes - -import model._ -import co.blocke.scala_reflection.info._ -import co.blocke.scala_reflection._ - -import scala.collection.mutable - - -trait ScalaClassTypeAdapter[T](implicit taCache: TypeAdapterCache) extends ClassTypeAdapterBase[T]: - val typeMembersByName: Map[String, TypeMemberInfo] - // dbCollectionName: Option[String] - - private val classInfo = info.asInstanceOf[ClassInfo] - - val isSJCapture = classInfo.hasMixin(SJ_CAPTURE) - - def _read_createInstance(args: List[Object], foundBits: mutable.BitSet, captured: java.util.HashMap[String, _]): T - def _read_updateFieldMembers( fmbn: Map[String, ClassFieldMember[_,_]]): ScalaClassTypeAdapter[T] - - def read(parser: Parser): T = - if (parser.peekForNull) then - null.asInstanceOf[T] - else - // External type hint --> Substitute type field's type into the placeholder (i.e.'T') in the class' fields - val (foundBits, args, captured) = { - if (classInfo.typeMembers.nonEmpty) then - val fixedFields = findActualTypeMemberTypes(parser) // Resolve actual type of type member (should be a class) and substitute any fields having that type with the actual - val substitutedClassInfo = _read_updateFieldMembers(fixedFields) - parser.expectObject(substitutedClassInfo, taCache.jackFlavor.defaultHint) - else - parser.expectObject(this, taCache.jackFlavor.defaultHint) - } - - val testBits = fieldBitsTemplate.collect{ - case b if !foundBits.contains(b) => b - } - if (testBits.isEmpty) then - _read_createInstance(args, foundBits, captured) - else - parser.backspace() - throw new ScalaJackError( - parser.showError( - s"Class ${classInfo.name} missing required fields: " + testBits - .map(b => orderedFieldNames(b)) - .mkString(", ") - ) - ) - - - def write[WIRE]( - t: T, - writer: Writer[WIRE], - out: mutable.Builder[WIRE, WIRE]): Unit = - - // Resolve actual types (in t) of any type members - val (allFields, filteredTypeMembers) = classInfo match { - case c: ScalaCaseClassInfo => (classInfo.fields.toList, c.filterTraitTypeParams.typeMembers.toList) - case c: ScalaClassInfo => (classInfo.fields.toList ++ c.nonConstructorFields.toList, c.filterTraitTypeParams.typeMembers.toList) - } - val (extras, resolvedFieldMembersByName) = - if filteredTypeMembers.nonEmpty then - val xtras: List[(String, ExtraFieldValue[_])] = filteredTypeMembers.map{ tm => - val foundActualField = allFields.find( _.asInstanceOf[ScalaFieldInfo].originalSymbol == Some(tm.typeSymbol) ) - val resolvedTypeMember = foundActualField.map{ a => - val actualRtype = RType.of(a.valueOf(t).getClass) - tm.copy(memberType = actualRtype) - }.getOrElse(tm) - ( - resolvedTypeMember.name, - ExtraFieldValue( - taCache.jackFlavor.typeValueModifier.unapply(resolvedTypeMember.asInstanceOf[TypeMemberInfo].memberType.name), - taCache.jackFlavor.stringTypeAdapter - ) - ) - } - - val filteredTypeMemberSymbols = filteredTypeMembers.map(_.typeSymbol) - val resolvedFields = fieldMembersByName.map{ case (fname, aField) => - val aScalaField = aField.info.asInstanceOf[ScalaFieldInfo] - if aScalaField.originalSymbol.isDefined && filteredTypeMemberSymbols.contains(aScalaField.originalSymbol.get) then - val actualRtype = RType.of(aScalaField.valueOf(t).getClass) - fname -> aField.copy( info = aScalaField.copy( fieldType = actualRtype ), valueTypeAdapter = taCache.typeAdapterOf(actualRtype) ) - else - fname -> aField - } - - (xtras, resolvedFields) - else - (Nil, fieldMembersByName) - - writer.writeObject( - t, - orderedFieldNames, - resolvedFieldMembersByName, - out, - extras - ) - - // Used by AnyTypeAdapter to insert type hint (not normally needed) into output so object - // may be reconstituted on read - def writeWithHint[WIRE]( - jackFlavor: JackFlavor[WIRE], - t: T, - writer: Writer[WIRE], - out: mutable.Builder[WIRE, WIRE]): Unit = - val hintValue = t.getClass.getName - val hintLabel = jackFlavor.getHintLabelFor(info) - val extra = List( - ( - hintLabel, - ExtraFieldValue(hintValue, jackFlavor.stringTypeAdapter) - ) - ) - writer.writeObject(t, orderedFieldNames, fieldMembersByName, out, extra) - - // Use parser to scan JSON for type member name and materialize the TypeMemberInfo with RType of actual/found type. - // (The original TypeMember's RType is likely a trait. The parser-found type should be a concrete class (ScalaCaseClassInfo).) - private def findActualTypeMemberTypes( - parser: Parser - ): Map[String, ClassFieldMember[_,_]] = - val foundByParser: Map[String, TypeMemberInfo] = parser.resolveTypeMembers( - typeMembersByName, - taCache.jackFlavor.typeValueModifier - ) - // Filter any non-trait/class type members... we ignore these so they don't mess up type hint modifiers - val filtered = typeMembersByName.collect { - case (k,tm) if tm.memberType.isInstanceOf[TraitInfo] || tm.memberType.isInstanceOf[ScalaCaseClassInfo] => (k,tm) - } - if (filtered.size != foundByParser.size) - throw new ScalaJackError( - parser.showError( - "Did not find required type member(s): " + typeMembersByName.keySet - .diff(foundByParser.keySet.map(_.toString)) - .mkString(",") - ) - ) - - // Map[TypeSymbol,TypeSymbolInfo] - val invertedBySymbol = foundByParser.map( (name, tpeInfo) => (tpeInfo.typeSymbol, tpeInfo)) - - // NOTE: This is sub-optimal and not "deep". Need a better solution to sew past Level-1 for a general n-deep solution - // As it stands, reflection doesn't provide a way to "sew" types deeply (dynamically). - fieldMembersByName.map { - case (name, fm) if fm.info.originalSymbol.flatMap(s => invertedBySymbol.get(s)).isDefined => - val actualTypeAdapter = taCache.typeAdapterOf(invertedBySymbol(fm.info.originalSymbol.get).memberType) // get TypeAdapter for actual type - val fixedTypeAdapter = fm.valueTypeAdapter match { - case fallback: FallbackTypeAdapter[_, _] => - FallbackTypeAdapter( - actualTypeAdapter.asInstanceOf[TypeAdapter[Any]], - fallback.orElseTypeAdapter.asInstanceOf[TypeAdapter[Any]] - ) - case _ => actualTypeAdapter - } - (name, fm.copy(info = fm.info.asInstanceOf[ScalaFieldInfo].copy(fieldType = invertedBySymbol(fm.info.originalSymbol.get)), valueTypeAdapter = fixedTypeAdapter)) - case (name, fm) => - (name, fm) - } - diff --git a/core/src/main/scala/co.blocke.scalajack/typeadapter/classes/ScalaClassTypeAdapterFactory.scala b/core/src/main/scala/co.blocke.scalajack/typeadapter/classes/ScalaClassTypeAdapterFactory.scala deleted file mode 100644 index 78626a6e..00000000 --- a/core/src/main/scala/co.blocke.scalajack/typeadapter/classes/ScalaClassTypeAdapterFactory.scala +++ /dev/null @@ -1,116 +0,0 @@ -package co.blocke.scalajack -package typeadapter -package classes - -import co.blocke.scala_reflection.info._ -import co.blocke.scala_reflection._ -import model._ -import scala.collection.mutable -import java.lang.reflect.Constructor - -object ScalaClassTypeAdapterFactory extends TypeAdapterFactory: - - def matches(concrete: RType): Boolean = - concrete match { - case c: ScalaCaseClassInfo if !c.isValueClass => true - case c: ScalaClassInfo => true - case _ => false - } - - final val CLASSCLASS = Class.forName("java.lang.Class") - - inline def bakeFieldMembersByName( - fields: List[FieldInfo], - constructor: Constructor[_], - infoClass: Class[_] )(implicit taCache: TypeAdapterCache - ): (Map[String,ClassFieldMember[_,_]], mutable.BitSet, Array[Object], List[String]) = - - // Filter out any ignored fields and re-index them all - val fieldsWeCareAbout = fields.filterNot(_.annotations.contains(IGNORE)).zipWithIndex.map{ (f,idx) => f.reIndex(idx) } - - val bits = mutable.BitSet() - val args = new Array[Object](fieldsWeCareAbout.size) - - val fieldsByName = fieldsWeCareAbout.map { f => - val fieldTypeAdapter = f.fieldType match { - case _: TypeSymbolInfo => taCache.typeAdapterOf(impl.PrimitiveType.Scala_Any) // Any unresolved type symbols must be considered Any - case t => - taCache.typeAdapterOf(t) match { - // In certain situations, value classes need to be unwrapped, i.e. use the type adapter of their member. - case vta: ValueClassTypeAdapter[_,_] if f.index < constructor.getParameterTypes().size => // value class in constructor - val constructorParamClass = constructor.getParameterTypes()(f.index).getClass - if constructorParamClass == vta.info.infoClass || constructorParamClass == CLASSCLASS then - vta - else - vta.elementTypeAdapter - case vta: ValueClassTypeAdapter[_,_] => // value class as body member - val returnTypeClass = infoClass.getMethod(f.name).getReturnType - if returnTypeClass == vta.info.infoClass || returnTypeClass == CLASSCLASS then - vta - else - vta.elementTypeAdapter - case other => - other - } - } - - // See if there's a default value set and blip bits/args accordingly to "pre-set" these values - if f.defaultValue.isDefined then - args(f.index) = f.defaultValue.get - else if fieldTypeAdapter.defaultValue.isDefined then - args(f.index) = fieldTypeAdapter.defaultValue.get.asInstanceOf[Object] - else - bits += f.index - - val fieldMapName = f.annotations.get(CHANGE_ANNO).map(_("name")) - val typeAdapter = fieldMapName.getOrElse(f.name) -> ClassFieldMember( - f, - fieldTypeAdapter, - infoClass, - f.annotations.get(DB_KEY).map(_.getOrElse("index","0").toInt), - fieldMapName - ) - // If this field is optional, we need to set the bits accordingly - if typeAdapter._2.isOptional then - bits -= f.index - typeAdapter - }.toMap - (fieldsByName, bits, args, fieldsWeCareAbout.map( f => f.annotations.get(CHANGE_ANNO).map(_("name")).getOrElse(f.name) )) - - def makeTypeAdapter(concrete: RType)(implicit taCache: TypeAdapterCache): TypeAdapter[_] = - concrete match { - case classInfo: ScalaCaseClassInfo => - val (fieldMembersByName, bits, args, orderedFieldNames) = bakeFieldMembersByName(classInfo.fields.toList, classInfo.constructor, classInfo.infoClass) - CaseClassTypeAdapter( - concrete, - fieldMembersByName, - args, - bits, - classInfo.typeMembers.map( tmem => (tmem.name, tmem.asInstanceOf[TypeMemberInfo]) ).toMap, - orderedFieldNames, - classInfo.annotations.get(DB_COLLECTION).map(_("name")) // Exctract Collection name annotation if present (for plain classes) - ) - - case classInfo: ScalaClassInfo => - // Nullify the defaultValue getter for non-constructor fields if there's no @Optional annotation - val nonConstructorFields = classInfo.nonConstructorFields.filterNot(_.name == "captured").map{ _ match { - case f if f.annotations.contains(OPTIONAL_ANNO) => f - case f: ScalaFieldInfo if f.fieldType.isInstanceOf[OptionInfo] => f - case f: ScalaFieldInfo => f.copy(defaultValueAccessorName = None) - } - } - - val (fieldMembersByName, bits, args, orderedFieldNames) = - bakeFieldMembersByName(classInfo.fields.toList ++ nonConstructorFields, classInfo.constructor, classInfo.infoClass) - val paramSize = classInfo.constructor.getParameterTypes().size - NonCaseClassTypeAdapter( - concrete, - fieldMembersByName, - args, - bits, - classInfo.typeMembers.map( tmem => (tmem.name, tmem.asInstanceOf[TypeMemberInfo]) ).toMap, - orderedFieldNames, - fieldMembersByName.values.filter(_.info.asInstanceOf[ScalaFieldInfo].isNonConstructorField).toList, - classInfo.annotations.get(DB_COLLECTION).map(_("name")) // Exctract Collection name annotation if present (for plain classes) - ) - } diff --git a/core/src/main/scala/co.blocke.scalajack/typeadapter/collection/JavaMapLikeTypeAdapter.scala b/core/src/main/scala/co.blocke.scalajack/typeadapter/collection/JavaMapLikeTypeAdapter.scala deleted file mode 100644 index 78172a8a..00000000 --- a/core/src/main/scala/co.blocke.scalajack/typeadapter/collection/JavaMapLikeTypeAdapter.scala +++ /dev/null @@ -1,73 +0,0 @@ -package co.blocke.scalajack -package typeadapter -package collection - -import model._ -import co.blocke.scala_reflection._ - -import scala.collection.mutable -import java.lang.reflect.{Method, Constructor} -import scala.jdk.CollectionConverters._ - - -case class JavaMapLikeTypeAdapter[KEY, VALUE, TO <: java.util.Map[KEY, VALUE]]( - info: RType, - keyIsOptionalOrAny: Boolean, - valueIsOptionalOrAny: Boolean, - keyTypeAdapter: TypeAdapter[KEY], - valueTypeAdapter: TypeAdapter[VALUE], - companionInstance: Object, - builderMethod: Method, - javaMapConstructor: Constructor[_], - ) extends TypeAdapter[TO]: - - def read(parser: Parser): TO = - // We have to do some voodoo here and peek ahead for Null. Some types, e.g. Int, aren't nullable, - // but Maps are nullable, so we can't trust the valueTypeAdapter to catch and handle null in - // these cases. - parser.peekForNull match { - case true => null.asInstanceOf[TO] - case _ => - val builder = builderMethod.invoke(companionInstance).asInstanceOf[mutable.Builder[(KEY, VALUE),Map[KEY,VALUE]]] - parser.expectMap( - keyTypeAdapter, - valueTypeAdapter, - builder) - javaMapConstructor.newInstance(builder.result.asJava).asInstanceOf[TO] - } - - def write[WIRE]( - t: TO, - writer: Writer[WIRE], - out: mutable.Builder[WIRE, WIRE]): Unit = - t match { - case null => writer.writeNull(out) - case _ => - val tScala = t.asScala - val filterKey = - if (keyIsOptionalOrAny && tScala != null) - tScala.asInstanceOf[Map[KEY, VALUE]].filterNot { case (k, v) => k match { - case None => true - case k: java.util.Optional[_] if !k.isPresent => true - case _ => false - }} - else - tScala - - val filterValue = - if valueIsOptionalOrAny && tScala != null then - filterKey.filterNot { case (k, v) => v match { - case None => true - case k: java.util.Optional[_] if !k.isPresent => true - case _ => false - }} - else - filterKey - - writer.writeMap( - filterValue, - keyTypeAdapter, - valueTypeAdapter, - out - ) - } diff --git a/core/src/main/scala/co.blocke.scalajack/typeadapter/collection/JavaSeqLikeTypeAdapter.scala b/core/src/main/scala/co.blocke.scalajack/typeadapter/collection/JavaSeqLikeTypeAdapter.scala deleted file mode 100644 index 26ae257e..00000000 --- a/core/src/main/scala/co.blocke.scalajack/typeadapter/collection/JavaSeqLikeTypeAdapter.scala +++ /dev/null @@ -1,57 +0,0 @@ -package co.blocke.scalajack -package typeadapter -package collection - -import model._ -import co.blocke.scala_reflection._ - -import scala.collection.mutable -import java.lang.reflect.{Method, Constructor} -import scala.jdk.CollectionConverters._ - - -case class JavaSeqLikeTypeAdapter[ELEM, TO]( - info: RType, - elemIsOptional: Boolean, - elementTypeAdapter: TypeAdapter[ELEM], - companionInstance: Object, - builderMethod: Method, - javaCollConstructor: Constructor[_], - toArrayMethod: Method - ) extends TypeAdapter[TO]: - - def read(parser: Parser): TO = - // We have to do some voodoo here and peek ahead for Null. Some types, e.g. Int, aren't nullable, - // but Seqs are nullable, so we can't trust the valueTypeAdapter to catch and handle null in - // these cases. - parser.peekForNull match { - case true => null.asInstanceOf[TO] - case _ => - val builder = builderMethod.invoke(companionInstance).asInstanceOf[mutable.Builder[ELEM,List[ELEM]]] - parser.expectList( - elementTypeAdapter, - builder - ) - val res: List[ELEM] = builder.result - javaCollConstructor.newInstance(res.asJava).asInstanceOf[TO] - } - - def write[WIRE]( - t: TO, - writer: Writer[WIRE], - out: mutable.Builder[WIRE, WIRE]): Unit = - t match { - case null => writer.writeNull(out) - case _ if elemIsOptional => - writer.writeArray( - t.asInstanceOf[Iterable[ELEM]].filterNot{ case v => v match { - case None => true - case v: java.util.Optional[_] if !v.isPresent => true - case _ => false - }}, - elementTypeAdapter, - out - ) - case _ => - writer.writeArray(toArrayMethod.invoke(t).asInstanceOf[Array[ELEM]], elementTypeAdapter, out) - } diff --git a/core/src/main/scala/co.blocke.scalajack/typeadapter/collection/JavaStackTypeAdapter.scala b/core/src/main/scala/co.blocke.scalajack/typeadapter/collection/JavaStackTypeAdapter.scala deleted file mode 100644 index bdf3d862..00000000 --- a/core/src/main/scala/co.blocke.scalajack/typeadapter/collection/JavaStackTypeAdapter.scala +++ /dev/null @@ -1,59 +0,0 @@ -package co.blocke.scalajack -package typeadapter -package collection - -import model._ -import co.blocke.scala_reflection._ - -import scala.collection.mutable -import java.lang.reflect.{Method, Constructor} -import scala.jdk.CollectionConverters._ - - -case class JavaStackTypeAdapter[ELEM, TO]( - info: RType, - elemIsOptional: Boolean, - elementTypeAdapter: TypeAdapter[ELEM], - companionInstance: Object, - builderMethod: Method, - javaCollConstructor: Constructor[_], - toArrayMethod: Method - ) extends TypeAdapter[TO]: - - def read(parser: Parser): TO = - // We have to do some voodoo here and peek ahead for Null. Some types, e.g. Int, aren't nullable, - // but Seqs are nullable, so we can't trust the valueTypeAdapter to catch and handle null in - // these cases. - parser.peekForNull match { - case true => null.asInstanceOf[TO] - case _ => - val builder = builderMethod.invoke(companionInstance).asInstanceOf[mutable.Builder[ELEM,List[ELEM]]] - parser.expectList( - elementTypeAdapter, - builder - ) - val res: List[ELEM] = builder.result - val stackInstance = javaCollConstructor.newInstance().asInstanceOf[java.util.Stack[ELEM]] - res.map( e => stackInstance.push(e) ) - stackInstance.asInstanceOf[TO] - } - - def write[WIRE]( - t: TO, - writer: Writer[WIRE], - out: mutable.Builder[WIRE, WIRE]): Unit = - t match { - case null => writer.writeNull(out) - case _ if elemIsOptional => - writer.writeArray( - t.asInstanceOf[Iterable[ELEM]].filterNot{ case v => v match { - case None => true - case v: java.util.Optional[_] if !v.isPresent => true - case _ => false - }}, - elementTypeAdapter, - out - ) - case _ => - writer.writeArray(toArrayMethod.invoke(t).asInstanceOf[Array[ELEM]], elementTypeAdapter, out) - } diff --git a/core/src/main/scala/co.blocke.scalajack/typeadapter/collection/MapLikeTypeAdapter.scala b/core/src/main/scala/co.blocke.scalajack/typeadapter/collection/MapLikeTypeAdapter.scala deleted file mode 100644 index 9ef620cb..00000000 --- a/core/src/main/scala/co.blocke.scalajack/typeadapter/collection/MapLikeTypeAdapter.scala +++ /dev/null @@ -1,70 +0,0 @@ -package co.blocke.scalajack -package typeadapter -package collection - -import model._ -import co.blocke.scala_reflection._ - -import scala.collection.mutable -import java.lang.reflect.Method - - -case class MapLikeTypeAdapter[KEY, VALUE, TO <: Map[KEY, VALUE]]( - info: RType, - keyIsOptionalOrAny: Boolean, - valueIsOptionalOrAny: Boolean, - keyTypeAdapter: TypeAdapter[KEY], - valueTypeAdapter: TypeAdapter[VALUE], - companionInstance: Object, - builderMethod: Method - ) extends TypeAdapter[TO]: - - def read(parser: Parser): TO = - // We have to do some voodoo here and peek ahead for Null. Some types, e.g. Int, aren't nullable, - // but Maps are nullable, so we can't trust the valueTypeAdapter to catch and handle null in - // these cases. - parser.peekForNull match { - case true => null.asInstanceOf[TO] - case _ => - val builder = builderMethod.invoke(companionInstance).asInstanceOf[mutable.Builder[(KEY, VALUE),TO]] - parser.expectMap( - keyTypeAdapter, - valueTypeAdapter, - builder) - builder.result - } - - def write[WIRE]( - t: TO, - writer: Writer[WIRE], - out: mutable.Builder[WIRE, WIRE]): Unit = - t match { - case null => writer.writeNull(out) - case _ => - val filterKey = - if (keyIsOptionalOrAny && t != null) - t.asInstanceOf[Map[KEY, VALUE]].filterNot { case (k, v) => k match { - case None => true - case k: java.util.Optional[_] if !k.isPresent => true - case _ => false - }} - else - t - - val filterValue = - if (valueIsOptionalOrAny && t != null) - filterKey.asInstanceOf[Map[KEY, VALUE]].filterNot { case (k, v) => v match { - case None => true - case k: java.util.Optional[_] if !k.isPresent => true - case _ => false - }} - else - filterKey - - writer.writeMap( - filterValue.asInstanceOf[Map[KEY, VALUE]], - keyTypeAdapter, - valueTypeAdapter, - out - ) - } diff --git a/core/src/main/scala/co.blocke.scalajack/typeadapter/collection/SeqLikeTypeAdapter.scala b/core/src/main/scala/co.blocke.scalajack/typeadapter/collection/SeqLikeTypeAdapter.scala deleted file mode 100644 index 20001c2a..00000000 --- a/core/src/main/scala/co.blocke.scalajack/typeadapter/collection/SeqLikeTypeAdapter.scala +++ /dev/null @@ -1,53 +0,0 @@ -package co.blocke.scalajack -package typeadapter -package collection - -import model._ -import co.blocke.scala_reflection._ - -import scala.collection.mutable -import java.lang.reflect.Method - - -case class SeqLikeTypeAdapter[ELEM, TO]( - info: RType, - elemIsOptional: Boolean, - elementTypeAdapter: TypeAdapter[ELEM], - companionInstance: Object, - builderMethod: Method - ) extends TypeAdapter[TO]: - - def read(parser: Parser): TO = - // We have to do some voodoo here and peek ahead for Null. Some types, e.g. Int, aren't nullable, - // but Seqs are nullable, so we can't trust the valueTypeAdapter to catch and handle null in - // these cases. - parser.peekForNull match { - case true => null.asInstanceOf[TO] - case _ => - val builder = builderMethod.invoke(companionInstance).asInstanceOf[mutable.Builder[ELEM,TO]] - parser.expectList( - elementTypeAdapter, - builder - ) - builder.result - } - - def write[WIRE]( - t: TO, - writer: Writer[WIRE], - out: mutable.Builder[WIRE, WIRE]): Unit = - t match { - case null => writer.writeNull(out) - case _ if elemIsOptional => - writer.writeArray( - t.asInstanceOf[Iterable[ELEM]].filterNot{ case v => v match { - case None => true - case v: java.util.Optional[_] if !v.isPresent => true - case _ => false - }}, - elementTypeAdapter, - out - ) - case _ => - writer.writeArray(t.asInstanceOf[Iterable[ELEM]], elementTypeAdapter, out) - } diff --git a/core/src/main/scala/co.blocke.scalajack/util/BijectiveFunction.scala b/core/src/main/scala/co.blocke.scalajack/util/BijectiveFunction.scala deleted file mode 100644 index a0ff7281..00000000 --- a/core/src/main/scala/co.blocke.scalajack/util/BijectiveFunction.scala +++ /dev/null @@ -1,74 +0,0 @@ -package co.blocke.scalajack -package util - -object BijectiveFunction { - - def apply[A, B](apply: A => B, unapply: B => A): BijectiveFunction[A, B] = - BijectiveFunctionPair(apply, unapply) - - object Implicits { - - implicit final class FunctionReverse[A, B](private val apply: A => B) - extends AnyVal { - @inline def <=>(unapply: B => A): BijectiveFunction[A, B] = - BijectiveFunction(apply, unapply) - def ⇄(unapply: B => A): BijectiveFunction[A, B] = <=>(unapply) - } - - } - -} - -trait BijectiveFunction[A, B] extends Function[A, B] { - - override def apply(a: A): B - def unapply(b: B): A - - def inverse: BijectiveFunction[B, A] = InvertedBijectiveFunction(this) - def compose[X](f: BijectiveFunction[X, A]): BijectiveFunction[X, B] = - ComposedBijectiveFunction(f, this) - def andThen[C](g: BijectiveFunction[B, C]): BijectiveFunction[A, C] = - ComposedBijectiveFunction(this, g) - // $COVERAGE-OFF$Unused right now--may be part of future functionality - def memoized: BijectiveFunction[A, B] = MemoizedBijectiveFunction(this) - // $COVERAGE-ON$ -} - -case class BijectiveFunctionPair[A, B](applyFn: A => B, unapplyFn: B => A) - extends BijectiveFunction[A, B] { - - override def apply(a: A): B = applyFn(a) - override def unapply(b: B): A = unapplyFn(b) -} - -case class InvertedBijectiveFunction[A, B](f: BijectiveFunction[A, B]) - extends BijectiveFunction[B, A] { - - override def apply(b: B): A = f.unapply(b) - override def unapply(a: A): B = f.apply(a) - override def inverse: BijectiveFunction[A, B] = f -} - -case class ComposedBijectiveFunction[A, B, C]( - f: BijectiveFunction[A, B], - g: BijectiveFunction[B, C]) - extends BijectiveFunction[A, C] { - - override def apply(a: A): C = g.apply(f.apply(a)) - override def unapply(c: C): A = f.unapply(g.unapply(c)) -} - -// $COVERAGE-OFF$Unused right now--may be part of future functionality -case class MemoizedBijectiveFunction[A, B](f: BijectiveFunction[A, B]) - extends BijectiveFunction[A, B] { - - import scala.collection.mutable - - val applyCache = new mutable.WeakHashMap[A, B] - val unapplyCache = new mutable.WeakHashMap[B, A] - - override def apply(a: A): B = applyCache.getOrElseUpdate(a, f.apply(a)) - override def unapply(b: B): A = unapplyCache.getOrElseUpdate(b, f.unapply(b)) - override def memoized: BijectiveFunction[A, B] = this -} -// $COVERAGE-ON$ diff --git a/core/src/main/scala/co.blocke.scalajack/util/FixFloat.scala b/core/src/main/scala/co.blocke.scalajack/util/FixFloat.scala deleted file mode 100644 index 4e35e79d..00000000 --- a/core/src/main/scala/co.blocke.scalajack/util/FixFloat.scala +++ /dev/null @@ -1,11 +0,0 @@ -package co.blocke.scalajack -package util - -object FixFloat { - // Bizzare set of magic to try to "fix" the precision slop when moving from Float->Double (prints extra digits in JSON) - def capFloat(f: Float): Double = { - val d = f.toString.toDouble - val diff = f.toDouble - d - f - diff - } -} diff --git a/core/src/main/scala/co.blocke.scalajack/yaml/YamlBuilder.scala b/core/src/main/scala/co.blocke.scalajack/yaml/YamlBuilder.scala deleted file mode 100644 index 7ff15765..00000000 --- a/core/src/main/scala/co.blocke.scalajack/yaml/YamlBuilder.scala +++ /dev/null @@ -1,20 +0,0 @@ -package co.blocke.scalajack -package yaml - -import org.snakeyaml.engine.v2.nodes.Node -import scala.collection.mutable - -case class YamlBuilder() extends mutable.Builder[Node, Node] { - private var internalValue: Option[Node] = None - - def addOne(elem: Node): this.type = { - internalValue = Some(elem) - this - } - - def clear(): Unit = internalValue = None - - def result(): Node = internalValue.getOrElse( - throw new ScalaJackError("No value set for internal yaml builder") - ) -} \ No newline at end of file diff --git a/core/src/main/scala/co.blocke.scalajack/yaml/YamlFlavor.scala b/core/src/main/scala/co.blocke.scalajack/yaml/YamlFlavor.scala deleted file mode 100644 index 3b8418f9..00000000 --- a/core/src/main/scala/co.blocke.scalajack/yaml/YamlFlavor.scala +++ /dev/null @@ -1,77 +0,0 @@ -package co.blocke.scalajack -package yaml - -import model._ -import co.blocke.scala_reflection.RType -import typeadapter.MaybeStringWrapTypeAdapter - -import scala.collection.mutable -import org.snakeyaml.engine.v2.api.DumpSettings -import org.snakeyaml.engine.v2.api.lowlevel.{Present, Serialize} - - -opaque type YAML = String - -case class YamlFlavor( - override val defaultHint: String = "_hint", - override val permissivesOk: Boolean = false, - override val customAdapters: List[TypeAdapterFactory] = List.empty[TypeAdapterFactory], - override val hintMap: Map[String, String] = Map.empty[String, String], - override val hintValueModifiers: Map[String, HintValueModifier] = Map.empty[String, HintValueModifier], - override val typeValueModifier: HintValueModifier = DefaultHintModifier, - override val parseOrElseMap: Map[Class[_], RType] = Map.empty[Class[_], RType], - override val enumsAsInt: Boolean = false -) extends JackFlavor[YAML] { - - private val settings = DumpSettings.builder().setIndent(2).build() - private val serializer = new Serialize(settings) - private val presenter = new Present(settings) - - def _read[T](input: YAML, typeAdapter: TypeAdapter[T]): T = - val parser = YamlParser(input, this) - typeAdapter.read(parser).asInstanceOf[T] - - def _render[T](t: T, typeAdapter: TypeAdapter[T]): YAML = - val sb = YamlBuilder() - typeAdapter.write(t, writer, sb) - presenter.emitToString(serializer.serializeOne(sb.result()).iterator).asInstanceOf[YAML] - - def parse(input: YAML): Parser = YamlParser(input, this) - - private val writer = YamlWriter() //(this) - - override val stringifyMapKeys: Boolean = true - - def allowPermissivePrimitives(): JackFlavor[YAML] = - throw new ScalaJackError("Not available for YAML encoding") - def enumsAsInts(): JackFlavor[YAML] = this.copy(enumsAsInt = true) - def parseOrElse(poe: (RType, RType)*): JackFlavor[YAML] = - this.copy(parseOrElseMap = this.parseOrElseMap ++ poe.map{(p,oe) => p.infoClass->oe}) - def withAdapters(ta: TypeAdapterFactory*): JackFlavor[YAML] = - this.copy(customAdapters = this.customAdapters ++ ta.toList) - def withDefaultHint(hint: String): JackFlavor[YAML] = - this.copy(defaultHint = hint) - def withHints(h: (RType, String)*): JackFlavor[YAML] = - this.copy(hintMap = this.hintMap ++ h.map{(rt,hint) => rt.name->hint}) - def withHintModifiers(hm: (RType, HintValueModifier)*): JackFlavor[YAML] = - this.copy(hintValueModifiers = this.hintValueModifiers ++ hm.map{(rt,hintM) => rt.name->hintM}) - def withTypeValueModifier(tm: HintValueModifier): JackFlavor[YAML] = - this.copy(typeValueModifier = tm) - - def stringWrapTypeAdapterFactory[T]( - wrappedTypeAdapter: TypeAdapter[T], - emptyStringOk: Boolean = true - ): TypeAdapter[T] = wrappedTypeAdapter // no need to stringify--YAML handles complex key values! - - def maybeStringWrapTypeAdapterFactory[T]( - wrappedTypeAdapter: TypeAdapter[T], - emptyStringOk: Boolean = true - ): TypeAdapter[T] = wrappedTypeAdapter // no need to stringify--YAML handles complex key values! - - // OK... for YAML I've done a bad thing here and lied. The JackFlavor trait requires Builder[WIRE,WIRE], - // which is Builder[YAML,YAML]. But the internals of snakeyaml require me to use a Builder[Node,Node]. - // Correctly untangling this would be a huge amount of work...and unnecessary. Both producer and consumer - // of this Builder know and expect Builder[Node,Node], so it was expedient to just lie to the trait and - // say this is a Builder[YAML,YAML] to make the compiler happy. Sue me. - override def getBuilder: mutable.Builder[YAML, YAML] = YamlBuilder().asInstanceOf[mutable.Builder[YAML, YAML]] -} \ No newline at end of file diff --git a/core/src/main/scala/co.blocke.scalajack/yaml/YamlParser.scala b/core/src/main/scala/co.blocke.scalajack/yaml/YamlParser.scala deleted file mode 100644 index 7516899e..00000000 --- a/core/src/main/scala/co.blocke.scalajack/yaml/YamlParser.scala +++ /dev/null @@ -1,278 +0,0 @@ -package co.blocke.scalajack -package yaml - -import model._ -import typeadapter.classes.ClassTypeAdapterBase -import co.blocke.scala_reflection._ -import co.blocke.scala_reflection.info.TypeMemberInfo - -import scala.collection.mutable -import scala.jdk.CollectionConverters._ -import org.snakeyaml.engine.v2.parser.ParserImpl -import org.snakeyaml.engine.v2.scanner.StreamReader -import org.snakeyaml.engine.v2.api.LoadSettings -import org.snakeyaml.engine.v2.events._ - -case class YamlParser(input: YAML, jackFlavor: JackFlavor[YAML]) extends Parser { - - private val loadSettings = LoadSettings.builder().build() - private val snake = new ParserImpl(loadSettings, new StreamReader(loadSettings, input.asInstanceOf[String])) - - private var indentLevel = 0 - - private def doICare(e: Event) = e match { - case _: StreamStartEvent => false - case _: StreamEndEvent => false - case _: DocumentStartEvent => false - case _: DocumentEndEvent => false - case _ => true - } - - type WIRE = YAML - - // Just the Events I care about - private val events = snake.asScala.filter(doICare).toList - - private val indentLevelMap = mutable.Map.empty[Int, Int] - - private var i = 0 - - def expectString(nullOK: Boolean = true): String = { - events(i) match { - case s: ScalarEvent => - i += 1 - s.getScalarStyle.toString match { - case "|" | ">" => s.getValue().stripTrailing() - case _ if s.getValue == "null" && nullOK => null - case _ => s.getValue - } - case e => - throw new ScalaJackError(showError("Expected a String here: " + e)) - } - } - - private def expectCollecton[K, TO](nextTest: Boolean, builder: mutable.Builder[K, TO], fn: () => Unit, errStr: String): TO = { - indentLevel += 1 - val startIndent = indentLevel - if (!nextTest) - throw new ScalaJackError(showError(errStr + events(i))) - i += 1 - while (!events(i).isInstanceOf[CollectionEndEvent] || indentLevel != startIndent) - fn() - i += 1 // consume collection end event - indentLevel -= 1 - builder.result() - } - - def expectList[K, TO](elemTypeAdapter: TypeAdapter[K], builder: mutable.Builder[K, TO]): TO = - expectCollecton(nextIsArray, builder, () => { - builder += elemTypeAdapter.read(this) - }, "Expected a List here: ") - - def expectTuple( - tupleFieldTypeAdapters: List[TypeAdapter[_]] - ): List[Object] = { - val builder = new mutable.ListBuffer[Object]() - expectCollecton(nextIsArray, builder, () => { - tupleFieldTypeAdapters.foreach { fieldTypeAdapter => - builder += fieldTypeAdapter.read(this).asInstanceOf[Object] - } - }, "Expected a Tuple here: ") - } - - def expectMap[K, V, TO](keyTypeAdapter: TypeAdapter[K], valueTypeAdapter: TypeAdapter[V], builder: mutable.Builder[(K, V), TO]): TO = - expectCollecton( - nextIsObject, - builder, - () => { - val mapKey = keyTypeAdapter.read(this) - val mapValue = valueTypeAdapter.read(this) - builder += ((mapKey, mapValue)) - }, - "Expected a Map or Object here: " - ) - - def expectObject( - classBase: ClassTypeAdapterBase[_], - hintLabel: String - ): (mutable.BitSet, List[Object], java.util.HashMap[String, _]) = { - indentLevel += 1 - val startIndent = indentLevel - if (!nextIsObject) - throw new ScalaJackError(showError("Expected an Object here: " + events(i))) - i += 1 - - val args = classBase.argsTemplate.clone() - val fieldBits = mutable.BitSet() - val captured = - if (classBase.isSJCapture) new java.util.HashMap[String, List[Event]]() - else null - while (!events(i) - .isInstanceOf[CollectionEndEvent] || indentLevel != startIndent) { - val key = expectString(false) - classBase.fieldMembersByName.get(key) match { - case Some(field) => - fieldBits += field.info.index - args(field.info.index) = field.valueTypeAdapter.read(this).asInstanceOf[Object] - case None => // found some input field not present in class - val mark = i - skipOverElement(startIndent) - if (classBase.isSJCapture && key != hintLabel) - // This is weird but snakeyaml's internals expect a stream start/end event, so we need to - // create these or boom happens. - captured.put(key, new StreamStartEvent() +: events.slice(mark, i) :+ new StreamEndEvent()) - } - } - - i += 1 // consume collection end event - indentLevel -= 1 - (fieldBits, args.toList, captured) - } - - def expectBoolean(): Boolean = events(i) match { - case n: ScalarEvent if n.getValue == "true" => - i += 1 - true - case n: ScalarEvent if n.getValue == "false" => - i += 1 - false - case e => - throw new ScalaJackError(showError("Expected a Boolean value here: " + e)) - } - - @inline def isNumberChar(char: Char): Boolean = - ('0' <= char && char <= '9') || char == '.' || char == 'e' || char == 'E' || char == '-' || char == '+' - @inline def isNumber(s: String): Boolean = s.toCharArray.foldLeft(true) { - case (acc, c) => acc && isNumberChar(c) - } - def expectNumber(nullOK: Boolean = false): String = events(i) match { - case n: ScalarEvent if n.getValue == "null" || n.getValue == "" => - i += 1 - null - case n: ScalarEvent if isNumber(n.getValue) => - i += 1 - n.getValue - case e => - throw new ScalaJackError(showError("Expected a Number value here: " + e)) - } - - def peekForNull: Boolean = events(i) match { - case n: ScalarEvent if n.getValue == "null" => - i += 1 - true - case _ => false - } - - def skipOverElement(startIndent: Int): Unit = { - var done = false - while (!done) { - events(i) match { - case _: ScalarEvent if indentLevel == startIndent => - done = true - case _: CollectionStartEvent => - indentLevel += 1 - case _: CollectionEndEvent => // close of a nested collection - indentLevel -= 1 - if (indentLevel == startIndent) - done = true - case _ => - } - i += 1 - } - } - - def scanForHint(hint: String, converterFn: HintBijective): Class[_] = { - val mark = i - indentLevel += 1 - val startIndent = indentLevel - if (!nextIsObject) - throw new ScalaJackError( - showError("Expected an Object here: " + events(i)) - ) - i += 1 - - val max = events.length - var found = false - while (!found && i < max && !events(i).isInstanceOf[CollectionEndEvent]) { - found = expectString(false) == hint - if (!found) - skipOverElement(startIndent) - } - if (!found) - throw new ScalaJackError(showError(s"Type hint '$hint' not found")) - - // Found hint or we wouldn't be here - val rawHintString = expectString(false) - val hintType = try { - converterFn.apply(rawHintString) - } catch { - case t: Throwable => - throw new ScalaJackError( - showError(s"Couldn't marshal class for $rawHintString") - ) - } - i = mark // we found hint, but go back to parse object - indentLevel -= 1 - Class.forName(hintType) - } - - // For embedded type members. Convert the type member into runtime "actual" type, e.g. T --> Foo - def resolveTypeMembers( - typeMembersByName: Map[String, TypeMemberInfo], - converterFn: HintBijective - ): Map[String, TypeMemberInfo] = { - val mark = i - indentLevel += 1 - val startIndent = indentLevel - if (!nextIsObject) - throw new ScalaJackError( - showError("Expected an Object here: " + events(i)) - ) - i += 1 - - val collected = new java.util.HashMap[String, TypeMemberInfo]() - while (!events(i).isInstanceOf[CollectionEndEvent]) { - val key = expectString() - if (typeMembersByName.contains(key)) - collected.put( - key, - TypeMemberInfo(key, typeMembersByName(key).typeSymbol, RType.of(Class.forName(converterFn.apply(expectString())))) - ) - else - skipOverElement(startIndent) - } - i = mark // we found hint, but go back to parse object - indentLevel -= 1 - collected.asScala.toMap - } - - def showError(msg: String): String = s"Line ${events(i).getStartMark.get().getLine}: " + msg - def backspace(): Unit = i -= 1 - def mark(): Int = { - indentLevelMap(i) = indentLevel - i - } - def revertToMark(mark: Int): Unit = { - i = mark - indentLevel = indentLevelMap(i) - } - def nextIsString: Boolean = events(i).isInstanceOf[ScalarEvent] - def nextIsNumber: Boolean = - events(i).isInstanceOf[ScalarEvent] && isNumber( - events(i).asInstanceOf[ScalarEvent].getValue - ) - def nextIsObject: Boolean = - events(i).isInstanceOf[CollectionStartEvent] && events(i).toString - .startsWith("+MAP") - def nextIsArray: Boolean = - events(i).isInstanceOf[CollectionStartEvent] && events(i).toString - .startsWith("+SEQ") - def nextIsBoolean: Boolean = - events(i).isInstanceOf[ScalarEvent] && List("true", "false").contains( - events(i).asInstanceOf[ScalarEvent].getValue - ) - // $COVERAGE-OFF$Unused, un-called by YamlFlavor machinery - def subParser(input: YAML): Parser = this.copy(input = input) - def sourceAsString: String = input.asInstanceOf[String] - // $COVERAGE-ON$ -} \ No newline at end of file diff --git a/core/src/main/scala/co.blocke.scalajack/yaml/YamlWriter.scala b/core/src/main/scala/co.blocke.scalajack/yaml/YamlWriter.scala deleted file mode 100644 index 9243a120..00000000 --- a/core/src/main/scala/co.blocke.scalajack/yaml/YamlWriter.scala +++ /dev/null @@ -1,177 +0,0 @@ -package co.blocke.scalajack -package yaml - -import model._ - -import scala.collection.mutable -import org.snakeyaml.engine.v2.nodes._ -import org.snakeyaml.engine.v2.api.LoadSettings -import org.snakeyaml.engine.v2.common.{FlowStyle, ScalarStyle} -import org.snakeyaml.engine.v2.composer.Composer -import org.snakeyaml.engine.v2.events.Event -import org.snakeyaml.engine.v2.resolver.JsonScalarResolver - -import scala.jdk.CollectionConverters._ - -case class YamlWriter() extends Writer[Node] { - - def writeArray[Elem](t: Iterable[Elem], elemTypeAdapter: TypeAdapter[Elem], out: mutable.Builder[Node, Node]): Unit = t match { - case null => writeNull(out) - case a => - val arr = mutable.ListBuffer.empty[Node] - val outBuf = YamlBuilder() - a.iterator.foreach { item => - outBuf.clear() - elemTypeAdapter.write(item, this, outBuf) - arr += outBuf.result - } - val flow = elemTypeAdapter match { - case _: ScalarTypeAdapter[_] => FlowStyle.FLOW - case _ => FlowStyle.BLOCK - } - out += new SequenceNode(Tag.SEQ, arr.asJava, flow) - } - - def writeBigInt(t: BigInt, out: mutable.Builder[Node, Node]): Unit = - t match { - case null => writeNull(out) - case _ => out += new ScalarNode(Tag.INT, t.toString, ScalarStyle.PLAIN) - } - def writeBoolean(t: Boolean, out: mutable.Builder[Node, Node]): Unit = - out += new ScalarNode(Tag.BOOL, t.toString, ScalarStyle.PLAIN) - def writeDecimal(t: BigDecimal, out: mutable.Builder[Node, Node]): Unit = - t match { - case null => writeNull(out) - case _ => out += new ScalarNode(Tag.FLOAT, t.toString, ScalarStyle.PLAIN) - } - def writeDouble(t: Double, out: mutable.Builder[Node, Node]): Unit = - out += new ScalarNode(Tag.FLOAT, t.toString, ScalarStyle.PLAIN) - def writeInt(t: Int, out: mutable.Builder[Node, Node]): Unit = - out += new ScalarNode(Tag.INT, t.toString, ScalarStyle.PLAIN) - def writeLong(t: Long, out: mutable.Builder[Node, Node]): Unit = - out += new ScalarNode(Tag.INT, t.toString, ScalarStyle.PLAIN) - - def writeMap[Key, Value, To](t: collection.Map[Key, Value], keyTypeAdapter: TypeAdapter[Key], valueTypeAdapter: TypeAdapter[Value], out: mutable.Builder[Node, Node]): Unit = - t match { - case null => writeNull(out) - case daMap => - val outBuf = YamlBuilder() - val outMap = daMap - .map { - case (key, value) => - if (key == null) - throw new ScalaJackError("Map keys cannot be null.") - outBuf.clear() - keyTypeAdapter.write(key, this, outBuf) - val k = outBuf.result() - outBuf.clear() - valueTypeAdapter.write(value, this, outBuf) - new NodeTuple(k, outBuf.result()) - } - .toList - .asJava - out += new MappingNode(Tag.MAP, outMap, FlowStyle.AUTO) - } - - def writeNull(out: mutable.Builder[Node, Node]): Unit = - out += new ScalarNode(Tag.NULL, "null", ScalarStyle.PLAIN) - - @inline private def writeFields( - fields: List[(String, Any, TypeAdapter[Any])] - ): Map[Node, Node] = { - val outBuf = YamlBuilder() - fields.collect { - case (label, value, valueTypeAdapter) if value != None => - outBuf.clear() - valueTypeAdapter.write(value, this, outBuf) - new ScalarNode(Tag.STR, label, ScalarStyle.PLAIN) -> outBuf.result() - }.toMap - } - - def writeObject[T]( - t: T, - orderedFieldNames: List[String], - fieldMembersByName: collection.Map[String, ClassFieldMember[_,_]], - out: mutable.Builder[Node, Node], - extras: List[(String, ExtraFieldValue[_])] - ): Unit = { - t match { - case null => writeNull(out.asInstanceOf[collection.mutable.Builder[Node,Node]]) - case _ => - val extraFields = writeFields( - extras.map( - e => - ( - e._1, - e._2.value, - e._2.valueTypeAdapter.asInstanceOf[TypeAdapter[Any]] - ) - ) - ) - val classFields = writeFields(orderedFieldNames.map { orn => - val oneField = fieldMembersByName(orn) - (orn, oneField.info.valueOf(t), oneField.valueTypeAdapter.asInstanceOf[TypeAdapter[Any]]) - }) - val captureFields = t match { - case sjc: SJCapture => - import scala.jdk.CollectionConverters._ - - sjc.captured.asScala.map { - case (k, v) => - val composer = new Composer(LoadSettings.builder.build, new EventParser(v.asInstanceOf[List[Event]])) - (new ScalarNode(Tag.STR, k, ScalarStyle.PLAIN), composer.next) - } - case _ => Map.empty[Node, Node] - } - - val mapNodes = (extraFields ++ classFields ++ captureFields).map { case (k, v) => new NodeTuple(k, v) } - out += new MappingNode(Tag.MAP, mapNodes.toList.asJava, FlowStyle.AUTO).asInstanceOf[Node] - } - } - - def writeString(t: String, out: mutable.Builder[Node, Node]): Unit = t match { - case null => writeNull(out) - case _ => out += new ScalarNode(Tag.STR, t, ScalarStyle.PLAIN) - } - - def writeRaw(t: Node, out: mutable.Builder[Node, Node]): Unit = - out += t - - def writeTuple[T]( - t: T, - writeFn: (Product) => List[(TypeAdapter[_], Any)], - out: mutable.Builder[Node, Node] - ): Unit = { - val arr = mutable.ListBuffer.empty[Node] - val outBuf = YamlBuilder() - val flowStyle = writeFn(t.asInstanceOf[Product]).foldLeft(true) { case (acc, (fieldTA, fieldValue)) => - outBuf.clear() - fieldTA.castAndWrite(fieldValue, this, outBuf) - arr += outBuf.result - if fieldTA.isInstanceOf[ScalarTypeAdapter[_]] then - acc - else - false - } - out += new SequenceNode(Tag.SEQ, arr.asJava, {if flowStyle then FlowStyle.FLOW else FlowStyle.BLOCK}) - } -} - -// $COVERAGE-OFF$This is a snakeyaml thing. It works if consuming it works. -case class EventParser(events: List[Event]) extends org.snakeyaml.engine.v2.parser.Parser { - private var i = 0 - - def checkEvent(choice: Event.ID): Boolean = - if (i < events.length) - events(i).getEventId == choice - else - false - def peekEvent(): Event = events(i) - def next(): Event = { - val ret = events(i) - i += 1 - ret - } - def hasNext: Boolean = i < events.length - // $COVERAGE-ON$ -} \ No newline at end of file diff --git a/core/src/test/java/co/blocke/scalajack/JavaArray.java b/core/src/test/java/co/blocke/scalajack/JavaArray.java deleted file mode 100644 index 49400a5a..00000000 --- a/core/src/test/java/co/blocke/scalajack/JavaArray.java +++ /dev/null @@ -1,53 +0,0 @@ -package co.blocke.scalajack; - -import co.blocke.scalajack.SJCaptureJava; - -import java.math.*; - -public class JavaArray { - - private BigDecimal[] bigDecs; - private BigInteger[] bigInts; - private java.lang.Boolean[] booleans; - private java.lang.Byte[] bytes; - private Character[] characters; - private java.lang.Double[] doubles; - private java.lang.Float[] floats; - private java.lang.Integer[] integers; - private java.lang.Long[] longs; - private java.lang.Short[] shorts; - private BigInteger[][] multi; - - public BigDecimal[] getBigDecs() { return bigDecs; } - public void setBigDecs(BigDecimal[] n) { bigDecs = n; } - - public BigInteger[] getBigInts() { return bigInts; } - public void setBigInts(BigInteger[] n) { bigInts = n; } - - public java.lang.Boolean[] getBooleans() { return booleans; } - public void setBooleans(java.lang.Boolean[] n) { booleans = n; } - - public java.lang.Byte[] getBytes() { return bytes; } - public void setBytes(java.lang.Byte[] n) { bytes = n; } - - public Character[] getCharacters() { return characters; } - public void setCharacters(Character[] n) { characters = n; } - - public java.lang.Double[] getDoubles() { return doubles; } - public void setDoubles(java.lang.Double[] n) { doubles = n; } - - public java.lang.Float[] getFloats() { return floats; } - public void setFloats(java.lang.Float[] n) { floats = n; } - - public java.lang.Integer[] getIntegers() { return integers; } - public void setIntegers(java.lang.Integer[] n) { integers = n; } - - public java.lang.Long[] getLongs() { return longs; } - public void setLongs(java.lang.Long[] n) { longs = n; } - - public java.lang.Short[] getShorts() { return shorts; } - public void setShorts(java.lang.Short[] n) { shorts = n; } - - public BigInteger[][] getMulti() { return multi; } - public void setMulti(BigInteger[][] n) { multi = n; } -} \ No newline at end of file diff --git a/core/src/test/java/co/blocke/scalajack/JavaCap.java b/core/src/test/java/co/blocke/scalajack/JavaCap.java deleted file mode 100644 index a6242585..00000000 --- a/core/src/test/java/co/blocke/scalajack/JavaCap.java +++ /dev/null @@ -1,12 +0,0 @@ -package co.blocke.scalajack; - -import co.blocke.scalajack.SJCaptureJava; - -public class JavaCap extends SJCaptureJava { - - private String name; - - public String getName() { return name; } - public void setName(String n) { name = n; } -} - diff --git a/core/src/test/java/co/blocke/scalajack/JavaEnum.java b/core/src/test/java/co/blocke/scalajack/JavaEnum.java deleted file mode 100644 index beccf90e..00000000 --- a/core/src/test/java/co/blocke/scalajack/JavaEnum.java +++ /dev/null @@ -1,9 +0,0 @@ -package co.blocke.scalajack; - -public class JavaEnum { - - private Temperature temp; - - public Temperature getTemp() { return temp; } - public void setTemp(Temperature n) { temp = n; } -} diff --git a/core/src/test/java/co/blocke/scalajack/JavaSimpleBase.java b/core/src/test/java/co/blocke/scalajack/JavaSimpleBase.java deleted file mode 100644 index 5a4d9265..00000000 --- a/core/src/test/java/co/blocke/scalajack/JavaSimpleBase.java +++ /dev/null @@ -1,22 +0,0 @@ -package co.blocke.scalajack; - -import co.blocke.scalajack.*; - -public class JavaSimpleBase { - - private int two; - @DBKey - @Change(name="dos") public int getTwo(){ return two; } - public void setTwo(int v) { two = v; } - - private int three = -10; - @DBKey(index = 99) @Optional - public int getThree(){ return three; } - public void setThree(int v) { three = v; } - - private int bogus = -1; - public int getBogus(){ return bogus; } - @Ignore - public void setBogus(int v) { bogus = v; } - -} diff --git a/core/src/test/java/co/blocke/scalajack/JavaSimpleChild.java b/core/src/test/java/co/blocke/scalajack/JavaSimpleChild.java deleted file mode 100644 index 80ad0aca..00000000 --- a/core/src/test/java/co/blocke/scalajack/JavaSimpleChild.java +++ /dev/null @@ -1,4 +0,0 @@ -package co.blocke.scalajack; - -public class JavaSimpleChild extends JavaSimpleBase { -} diff --git a/core/src/test/java/co/blocke/scalajack/Maybe.java b/core/src/test/java/co/blocke/scalajack/Maybe.java deleted file mode 100644 index 66da7327..00000000 --- a/core/src/test/java/co/blocke/scalajack/Maybe.java +++ /dev/null @@ -1,15 +0,0 @@ -package co.blocke.scalajack; - -import java.util.Optional; - -public class Maybe { - - private Optional one = Optional.empty(); - public Optional getOne(){ return one; } - public void setOne(Optional v) { one = v; } - - private Optional two = Optional.of("stuff"); - public Optional getTwo(){ return two; } - public void setTwo(Optional v) { two = v; } - -} diff --git a/core/src/test/java/co/blocke/scalajack/Maybe2.java b/core/src/test/java/co/blocke/scalajack/Maybe2.java deleted file mode 100644 index 77897adf..00000000 --- a/core/src/test/java/co/blocke/scalajack/Maybe2.java +++ /dev/null @@ -1,15 +0,0 @@ -package co.blocke.scalajack; - -import java.util.Optional; - -public class Maybe2 { - - private String one = "foom"; - public String getOne(){ return one; } - public void setOne(String v) { one = v; } - - private Optional two = Optional.of("stuff"); - public Optional getTwo(){ return two; } - public void setTwo(Optional v) { two = v; } - -} diff --git a/core/src/test/java/co/blocke/scalajack/OnSetter.java b/core/src/test/java/co/blocke/scalajack/OnSetter.java deleted file mode 100644 index 279893a7..00000000 --- a/core/src/test/java/co/blocke/scalajack/OnSetter.java +++ /dev/null @@ -1,11 +0,0 @@ -package co.blocke.scalajack; - -import co.blocke.scalajack.*; - -public class OnSetter { - - private int two; - public int getTwo(){ return two; } - @Change(name="dos") public void setTwo(int v) { two = v; } - -} \ No newline at end of file diff --git a/core/src/test/java/co/blocke/scalajack/Temperature.java b/core/src/test/java/co/blocke/scalajack/Temperature.java deleted file mode 100644 index 1c2351b8..00000000 --- a/core/src/test/java/co/blocke/scalajack/Temperature.java +++ /dev/null @@ -1,6 +0,0 @@ -package co.blocke.scalajack; - -public enum Temperature -{ - Hot, Cold; -} diff --git a/core/src/test/java/co/blocke/scalajack/Unsupported.java b/core/src/test/java/co/blocke/scalajack/Unsupported.java deleted file mode 100644 index f7c41fc6..00000000 --- a/core/src/test/java/co/blocke/scalajack/Unsupported.java +++ /dev/null @@ -1,5 +0,0 @@ -package co.blocke.scalajack; - -public class Unsupported { - public Unsupported(String item){} -} diff --git a/core/src/test/scala/co.blocke.scalajack/ConverterSpec.scala b/core/src/test/scala/co.blocke.scalajack/ConverterSpec.scala deleted file mode 100644 index a551c0b6..00000000 --- a/core/src/test/scala/co.blocke.scalajack/ConverterSpec.scala +++ /dev/null @@ -1,173 +0,0 @@ -package co.blocke.scalajack - -import co.blocke.scalajack.json.JsonFlavor -import co.blocke.scalajack.model.JackFlavor -import TestUtil._ -import munit._ -import munit.internal.console - -import yaml._ -import json._ -import json4s._ -import delimited._ -import Converters._ -import org.json4s._ - -trait Human -case class Person(name: String, age: Int) extends Human -case class Typey[T](thing: T) { - type foom = T -} - -class ConverterSpec extends FunSuite: - - implicit val sj: JsonFlavor = ScalaJack() - implicit val sjY: JackFlavor[YAML] = ScalaJack(YamlFlavor()) - implicit val sjV: JackFlavor[JValue] = ScalaJack(Json4sFlavor()) - implicit val sjD: JackFlavor[DELIMITED] = ScalaJack(DelimitedFlavor()) - - val simple: Person = Person("Fred", 34) - val complex: Typey[Human] = Typey[Human](Person("Fred", 34)) - val simpleJson = """{"name":"Fred","age":34}""".asInstanceOf[JSON] - val complexJson = """{"foom":"co.blocke.scalajack.Person","thing":{"name":"Fred","age":34}}""".asInstanceOf[JSON] - val simpleJson4s = JObject(List(("name", JString("Fred")), ("age", JInt(34)))) - val complexJson4s = JObject( - List( - ("foom", JString("co.blocke.scalajack.Person")), - ("thing", JObject(List(("name", JString("Fred")), ("age", JInt(34))))) - )) - val simpleYaml = """name: Fred - |age: 34 - |""".stripMargin.asInstanceOf[YAML] - val complexYaml = """foom: co.blocke.scalajack.Person - |thing: - | name: Fred - | age: 34 - |""".stripMargin.asInstanceOf[YAML] - val simpleDelimited = "Fred,34".asInstanceOf[DELIMITED] - - test("mapJson") { - describe( - "-----------------------------\n: Converter Mapping Tests :\n-----------------------------", Console.BLUE - ) - assertEquals(simpleJson.mapJson[Person](_.copy(age = 45)), """{"name":"Fred","age":45}""".asInstanceOf[JSON]) - } - - test("mapJson4s") { - assertEquals(simpleJson4s.mapJson4s[Person](_.copy(age = 45)), JObject(List(("name", JString("Fred")), ("age", JInt(45))))) - } - - test("mapYaml") { - assertEquals(simpleYaml.mapYaml[Person](_.copy(age = 45)), """name: Fred - |age: 45 - |""".stripMargin.asInstanceOf[YAML]) - } - - test("mapDelimited") { - assertEquals(simpleDelimited.mapDelimited[Person](_.copy(age = 45)), """Fred,45""".asInstanceOf[DELIMITED]) - } - - test("mapJsonTo") { - assertEquals(simpleJson.mapJsonTo[Person, YAML](ScalaJack(YamlFlavor()))(_.copy(age = 45)), """name: Fred - |age: 45 - |""".stripMargin.asInstanceOf[YAML]) - } - - test("mapJson4sTo") { - assertEquals(simpleJson4s.mapJson4sTo[Person, JSON](ScalaJack())(_.copy(age = 45)), """{"name":"Fred","age":45}""".asInstanceOf[JSON]) - } - - test("mapYamlTo") { - assertEquals(simpleYaml.mapYamlTo[Person, JSON](ScalaJack())(_.copy(age = 45)), """{"name":"Fred","age":45}""".asInstanceOf[JSON]) - } - - test("mapDelimitedTo") { - assertEquals(simpleDelimited.mapDelimitedTo[Person, JSON](ScalaJack())(_.copy(age = 45)), """{"name":"Fred","age":45}""".asInstanceOf[JSON]) - } - - test("toJson") { - describe( - "---------------------------------\n: Convenience \"to/from\" Tests :\n---------------------------------", Console.BLUE - ) - assertEquals((simple: Person).toJson, simpleJson) - assertEquals(toJson[Person](simple), simpleJson) - assertEquals(toJson[Typey[Human]](complex), complexJson) - } - - test("fromJson") { - assertEquals(simpleJson.fromJson[Person], simple) - assertEquals(complexJson.fromJson[Typey[Human]], complex) - } - - test("toJson4s") { - assertEquals(toJson4s[Person](simple), simpleJson4s) - assertEquals(toJson4s[Typey[Human]](complex), complexJson4s) - } - - test("fromJson4s") { - assertEquals(simpleJson4s.fromJson4s[Person], simple) - assertEquals(complexJson4s.fromJson4s[Typey[Human]], complex) - } - - test("toYaml") { - assertEquals(toYaml[Person](simple), simpleYaml) - assertEquals(toYaml[Typey[Human]](complex), complexYaml) - } - - test("fromYaml") { - assertEquals(simpleYaml.fromYaml[Person], simple) - assertEquals(complexYaml.fromYaml[Typey[Human]], complex) - } - - test("toDelimited") { - assertEquals(toDelimited[Person](simple), simpleDelimited) - } - - test("fromDelimited") { - assertEquals(simpleDelimited.fromDelimited[Person], simple) - } - - test("yamlToJson") { - describe( - "----------------------\n: Converters Tests :\n----------------------", Console.BLUE - ) - assertEquals(simpleYaml.yamlToJson[Person], simpleJson) - assertEquals(complexYaml.yamlToJson[Typey[Human]], complexJson) - } - - test("yamlToJson4s") { - assertEquals(simpleYaml.yamlToJson4s[Person], simpleJson4s) - assertEquals(complexYaml.yamlToJson4s[Typey[Human]], complexJson4s) - } - - test("jsonToYaml") { - assertEquals(simpleJson.jsonToYaml[Person], simpleYaml) - assertEquals(complexJson.jsonToYaml[Typey[Human]], complexYaml) - } - - test("jsonToJson4s") { - assertEquals(simpleJson.jsonToJson4s[Person], simpleJson4s) - assertEquals(complexJson.jsonToJson4s[Typey[Human]], complexJson4s) - } - - test("delimitedToYaml") { - assertEquals(simpleDelimited.delimitedToYaml[Person], simpleYaml) - } - - test("delimitedToJson4s") { - assertEquals(simpleDelimited.delimitedToJson4s[Person], simpleJson4s) - } - - test("delimitedToJson") { - assertEquals(simpleDelimited.delimitedToJson[Person], simpleJson) - } - - test("json4sToYaml") { - assertEquals(simpleJson4s.json4sToYaml[Person], simpleYaml) - assertEquals(complexJson4s.json4sToYaml[Typey[Human]], complexYaml) - } - - test("json4sToJson") { - assertEquals(simpleJson4s.json4sToJson[Person], simpleJson) - assertEquals(complexJson4s.json4sToJson[Typey[Human]], complexJson) - } diff --git a/core/src/test/scala/co.blocke.scalajack/JsonMatcher.scala b/core/src/test/scala/co.blocke.scalajack/JsonMatcher.scala deleted file mode 100644 index ab19edc6..00000000 --- a/core/src/test/scala/co.blocke.scalajack/JsonMatcher.scala +++ /dev/null @@ -1,21 +0,0 @@ -package co.blocke.scalajack -package json - -import co.blocke.scalajack.json.JSON -import org.json4s.JsonAST.JValue -import org.json4s.native.JsonMethods._ -import org.json4s.string2JsonInput - -object JsonMatcher { - - def jsonMatches( expected: JSON, actual: JSON ): Boolean = - val diffs = JsonDiff.compare( - parseJValue(expected.asInstanceOf[String]), - parseJValue(actual.asInstanceOf[String]), - "expected", - "actual" - ) - diffs.isEmpty - - implicit def parseJValue(string: String): JValue = parse(string) -} diff --git a/core/src/test/scala/co.blocke.scalajack/delimited/DelimitedSpec.scala b/core/src/test/scala/co.blocke.scalajack/delimited/DelimitedSpec.scala deleted file mode 100644 index 13c44bed..00000000 --- a/core/src/test/scala/co.blocke.scalajack/delimited/DelimitedSpec.scala +++ /dev/null @@ -1,374 +0,0 @@ -package co.blocke.scalajack -package delimited - -import TestUtil._ -import munit._ -import munit.internal.console -import co.blocke.scala_reflection.RType - - -class DelimSpec extends FunSuite: - - val sj = ScalaJack(DelimitedFlavor()) - - test("Basic DelimSpec") { - describe("---------------------\n: Delimited Tests :\n---------------------", Console.BLUE) - describe("DelimSpec") - - val all = AllPrim( - 5, - 25L, - 123.45, - 12.3F, - 'x', - "Hey", - b = true, - BigInt(12345678), - BigDecimal(0.123458867) - ) - val delim = sj.render(all) - assertEquals(delim, """5,25,123.45,12.3,x,Hey,true,12345678,0.123458867""".asInstanceOf[DELIMITED]) - val inst = sj.read[AllPrim](delim) - assertEquals(inst, all) - } - - test("SalaJack configurations (DelimitedFlavor)") { - intercept[ScalaJackError]{ - ScalaJack(DelimitedFlavor()).withAdapters() - } - intercept[ScalaJackError]{ - ScalaJack(DelimitedFlavor()).withDefaultHint(null) - } - intercept[ScalaJackError]{ - ScalaJack(DelimitedFlavor()).withHintModifiers() - } - intercept[ScalaJackError]{ - ScalaJack(DelimitedFlavor()).withHints() - } - intercept[ScalaJackError]{ - ScalaJack(DelimitedFlavor()).withTypeValueModifier(null) - } - intercept[ScalaJackError]{ - ScalaJack(DelimitedFlavor()).allowPermissivePrimitives() - } - val sjx = ScalaJack(DelimitedFlavor()) - .parseOrElse(RType.of[Address] -> RType.of[DefaultAddress]) - assert(sjx.read[Address]("a;sdlfj".asInstanceOf[DELIMITED]) == DefaultAddress("a;sdlfj")) - } - - test("Enum support") { - val sjy = ScalaJack(DelimitedFlavor()).enumsAsInts() - val i = Shirt(1, Size.Small) - val d = sjy.render(i) - assertEquals(d, "1,0".asInstanceOf[DELIMITED]) - assertEquals(sjy.read[Shirt](d), i) - - val sjz = ScalaJack(DelimitedFlavor()) - val d2 = sjz.render(i) - assertEquals(d2, "1,Small".asInstanceOf[DELIMITED]) - assertEquals(sjz.read[Shirt](d2), i) - - val d3 = "1,\"Small\"".asInstanceOf[DELIMITED] - assertEquals(sjz.read[Shirt](d3), i) - - val d4 = "1,0".asInstanceOf[DELIMITED] - assertEquals(sjz.read[Shirt](d4), i) - - val d6 = "1,Huge".asInstanceOf[DELIMITED] - val msg = - """No value found in enumeration co.blocke.scalajack.delimited.Size$ for Huge - |1,Huge - |--^""".stripMargin - interceptMessage[ScalaJackError](msg){ - sjz.read[Shirt](d6) - } - } - - test("Empty input") { - assertEquals(sj.render("".asInstanceOf[DELIMITED]), "".asInstanceOf[DELIMITED]) - assertEquals(sj.read[Shirt]("".asInstanceOf[DELIMITED]), null) - } - - test("Unnecessary quotes on fields work") { // "a",b,"c" - val delim = "\"a\",b,\"c\"" - val inst = sj.read[ThreeStrings](delim.asInstanceOf[DELIMITED]) - assertEquals(inst, ThreeStrings("a", "b", "c")) - } - - test("Escaped quotes in string work") { // a,b""c,d - val delim = "a,\"b\"\"x\",c" - val inst = sj.read[ThreeStrings](delim.asInstanceOf[DELIMITED]) - assertEquals(inst, ThreeStrings("a", "b\"x", "c")) - } - - test("Basic fails") { - val delim = """5,25,123.45,12.3,x,Hey,true,bogus12345678,0.123458867""" - val msg = - """Expected a Number here - |5,25,123.45,12.3,x,Hey,true,bogus12345678,0.123458867 - |----------------------------^""".stripMargin - interceptMessage[co.blocke.scalajack.ScalaJackError](msg){ - sj.read[AllPrim](delim.asInstanceOf[DELIMITED]) - } - } - - test("Nested case class") { - describe("Classes") - val n = Nested("item", Inside(1, "One"), Inside(2, "Two")) - val delim = sj.render(n) - assertEquals(delim, """item,"1,One","2,Two"""".asInstanceOf[DELIMITED]) - val inst = sj.read[Nested](delim) - assertEquals(inst, n) - } - - test("Null class field value (nullable) w/no default") { - val delim = """item,,"2,Two"""".asInstanceOf[DELIMITED] - val inst = Nested("item", null, Inside(2, "Two")) - assertEquals(sj.read[Nested](delim), inst) - assertEquals(sj.render(inst), delim) - } - - test("Empty (String) and null primitive class field") { - val delim = ""","1,One","2,Two"""".asInstanceOf[DELIMITED] - val inst = Nested("", Inside(1, "One"), Inside(2, "Two")) - assertEquals(sj.read[Nested](delim), inst) - assertEquals(sj.render(inst), delim) - } - - test("Null class field value with default") { - val delim = """item,"1,One",""".asInstanceOf[DELIMITED] - assertEquals(sj.read[Nested](delim), - Nested("item", Inside(1, "One"), Inside(99, "dunno")) - ) - } - - test("Simple list works") { - describe("Lists") - val s = WithList(5, List("a", "b", "c")) - val delim = sj.render(s) - assertEquals(delim, """5,"a,b,c"""".asInstanceOf[DELIMITED]) - assertEquals(sj.read[WithList[String]](delim), s) - } - - test("Nullables") { - val s = WithList(5, List(BigInt(5), null, BigInt(7))) - val delim = sj.render(s) - assertEquals(delim, """5,"5,,7"""".asInstanceOf[DELIMITED]) - assertEquals(sj.read[WithList[BigInt]](delim), s) - } - - test("Null list") { - val s = WithList[Int](5, null) - val delim = sj.render[WithList[Int]](s) - assertEquals(delim, "5,".asInstanceOf[DELIMITED]) - } - - test("Empty List") { - val inst = sj.read[WithList[Int]]("5,".asInstanceOf[DELIMITED]) - assertEquals(inst, WithList(5, null)) - assertEquals(sj.render(inst), "5,".asInstanceOf[DELIMITED]) - } - - test("Non-nullables") { - val delim = """5,"1,,3"""".asInstanceOf[DELIMITED] - val msg = """Expected a Number here - |,,3 - |-^""".stripMargin - interceptMessage[co.blocke.scalajack.ScalaJackError](msg){ - sj.read[WithList[Int]](delim) - } - } - - test("Nested lists") { - val s = WithList(3, List(List("a,b", "c"), List("x", "y"))) - val delim = sj.render(s) - assertEquals(delim , "3,\"\"\"\"\"\"\"a,b\"\"\"\",c\"\",\"\"x,y\"\"\"".asInstanceOf[DELIMITED]) - assertEquals(sj.read[WithList[List[String]]](delim), s) - } - - test("Optional case class fields") { - describe("Options") - val s = HasOption(None, Some(7)) - val delim = sj.render(s) - assertEquals(delim, ",7".asInstanceOf[DELIMITED]) - assertEquals(sj.read[HasOption](delim), s) - } - - test("Missing optional case class field with default") { - val s = HasOption(Some(7), None) - val delim = sj.render(s) - assertEquals(delim, "7,".asInstanceOf[DELIMITED]) - assertEquals(sj.read[HasOption](delim), HasOption(Some(7), Some(5))) - } - - test("Optional list members") { - val s = WithList(3, List(Some(1), None, Some(2))) - val delim = sj.render(s) - assertEquals(delim, "3,\"1,2\"".asInstanceOf[DELIMITED]) - assertEquals(sj.read[WithList[Option[Int]]](delim), - WithList(3, List(Some(1), Some(2))) - ) - assertEquals(sj.read[WithList[Option[Int]]]("3,\"1,,2\"".asInstanceOf[DELIMITED]), s) - } - - test("Tuple case class fields") { - describe("Tuples") - val s = HasTuples(("a", 3), (false, 9)) - val delim = sj.render(s) - assertEquals(delim, "\"a,3\",\"false,9\"".asInstanceOf[DELIMITED]) - assertEquals(sj.read[HasTuples](delim), s) - } - - test("Tuple with class value") { - val delim = "\"thing,\"\"1,foo\"\"\"".asInstanceOf[DELIMITED] - assertEquals(sj.read[HasTuples2](delim), - HasTuples2(("thing", Inside(1, "foo"))) - ) - } - - test("Tuple with null value") { - val delim = "\"thing,\"".asInstanceOf[DELIMITED] - assertEquals(sj.read[HasTuples2](delim), HasTuples2(("thing", null))) - } - - test("Tuple with escaped quote in value") { - val s = HasTuples(("a\"b", 3), (false, 9)) - val delim = sj.render(s) - assertEquals(delim , "\"\"\"a\"\"\"\"b\"\",3\",\"false,9\"".asInstanceOf[DELIMITED]) - assertEquals(sj.read[HasTuples](delim), s) - } - - test("Missing optional case class field with default") { - val delim = "\"a,3\",".asInstanceOf[DELIMITED] - assertEquals(sj.read[HasTuples](delim), HasTuples(("a", 3), (true, 1))) - } - - test("Tuple with Classish elements") { - val inst = HasTuples3((true, Inside(4, "foom"))) - val delim = sj.render(inst) - assertEquals(delim, "\"true,\"\"4,foom\"\"\"".asInstanceOf[DELIMITED]) - assertEquals(sj.read[HasTuples3](delim), inst) - } - - test("Supports Either parsing") { - describe("Either") - val s1 = HasEither(1, Left(3)) - val delim1 = sj.render(s1) - assertEquals(delim1, "1,3".asInstanceOf[DELIMITED]) - assertEquals(sj.read[HasEither](delim1), s1) - - val s2 = HasEither(2, Right(Inside(99, "foo"))) - val delim2 = sj.render(s2) - assertEquals(delim2, "2,\"99,foo\"".asInstanceOf[DELIMITED]) - assertEquals(sj.read[HasEither](delim2), s2) - - val s3 = HasEither(2, Right(null)) - assertEquals( sj.render(s3), "2,".asInstanceOf[DELIMITED]) - } - - test("Null object") { - assertEquals(sj.render[HasEither](null), "".asInstanceOf[DELIMITED]) - } - - test("Either missing a value (no default)") { - val delim = ",\"99,foo\"".asInstanceOf[DELIMITED] - val msg = """Expected a Number here - |,"99,foo" - |^""".stripMargin - interceptMessage[co.blocke.scalajack.ScalaJackError](msg){ - sj.read[HasEither](delim) - } - } - - test("Supports Either field value with default specified") { - val delim = "15,".asInstanceOf[DELIMITED] - val i = sj.read[HasEither2](delim) - assertEquals(i, HasEither2(15, Right(Inside(1, "ok")))) - } - - test("Can't parse either side of Either") { - val msg = - """Failed to read either side of Either - |3,true - |^""".stripMargin - interceptMessage[co.blocke.scalajack.ScalaJackError](msg){ - sj.read[Shirt2]("3,true".asInstanceOf[DELIMITED]) - } - } - - test("Either with embedded quote in string value") { - val s = HasEither3(1, Left("a\"b")) - val delim = sj.render(s) - assertEquals(delim, """1,"a""b"""".asInstanceOf[DELIMITED]) - assertEquals(sj.read[HasEither3](delim), s) - } - - test("Write a null Either value") { - val delim = "5,".asInstanceOf[DELIMITED] - val inst = HasEither(5, null) - assertEquals(sj.render(inst), delim) - assertEquals(sj.read[HasEither](delim), inst) - } - - test("Write a Right non-Classish value") { - val delim = "5,true".asInstanceOf[DELIMITED] - val inst = HasEitherRev(5, Right(true)) - assertEquals(sj.render(inst), delim) - assertEquals(sj.read[HasEitherRev](delim), inst) - } - - test("Write a Left Classish value") { - val delim = "5,\"15,Mike\"".asInstanceOf[DELIMITED] - val inst = HasEitherRev(5, Left(Inside(15, "Mike"))) - assertEquals(sj.render(inst), delim) - assertEquals(sj.read[HasEitherRev](delim), inst) - } - - test("Non-case classes fail") { - describe("Plug coverage holes") - val delim = "John,35".asInstanceOf[DELIMITED] - val msg = - """Only case classes with non-empty constructors are supported for delimited data. - |John,35 - |^""".stripMargin - interceptMessage[co.blocke.scalajack.ScalaJackError](msg){ - sj.read[Busted](delim) - } - } - - test("No Map support") { - val delim = "John,35".asInstanceOf[DELIMITED] - val msg = """No Map support for delimited data. - |John,35 - |-----^""".stripMargin - interceptMessage[co.blocke.scalajack.ScalaJackError](msg){ - sj.read[Busted2](delim) - } - val msg2 = "Map-typed data is not supported for delimited output" - interceptMessage[co.blocke.scalajack.ScalaJackError](msg2){ - sj.render(Busted2("foo", Map("a" -> "b"))) - } - } - - test("Write null string") { - assertEquals(sj.render(Inside(5, null)), "5,".asInstanceOf[DELIMITED]) - } - - test("Invalid Boolean value") { - val delim = "John,35".asInstanceOf[DELIMITED] - val msg = """Expected a Boolean here - |John,35 - |-----^""".stripMargin - interceptMessage[co.blocke.scalajack.ScalaJackError](msg){ - sj.read[Busted3](delim) - } - } - - test("Null List value") { - val delim = "John,".asInstanceOf[DELIMITED] - assertEquals(sj.read[Busted4](delim), Busted4("John", null)) - } - - test("Parser") { - sj.parse("John,".asInstanceOf[DELIMITED]) - } diff --git a/core/src/test/scala/co.blocke.scalajack/delimited/Model.scala b/core/src/test/scala/co.blocke.scalajack/delimited/Model.scala deleted file mode 100644 index c6daab5f..00000000 --- a/core/src/test/scala/co.blocke.scalajack/delimited/Model.scala +++ /dev/null @@ -1,36 +0,0 @@ -package co.blocke.scalajack -package delimited - -case class AllPrim(i: Int, l: Long, d: Double, f: Float, c: Char, s: String, b: Boolean, bi: BigInt, bd: BigDecimal) - -case class Inside(id: Int, desc: String) -case class Nested(thing: String, in: Inside, other: Inside = Inside(99, "dunno")) - -case class ThreeStrings(a: String, b: String, c: String) - -case class WithList[T](id: Int, someList: List[T]) - -case class HasOption(one: Option[Int], two: Option[Int] = Some(5)) - -case class HasTuples(one: (String, Int), two: (Boolean, Int) = (true, 1)) -case class HasTuples2(one: (String, Inside)) -case class HasTuples3(a: (Boolean, Inside)) - -case class HasEither(one: Int, two: Either[Int, Inside]) -case class HasEitherRev(one: Int, two: Either[Inside, Boolean]) -case class HasEither2(one: Int, two: Either[Int, Inside] = Right(Inside(1, "ok"))) -case class HasEither3(one: Int, two: Either[String, Inside]) - -object Size extends Enumeration { - val Small, Medium, Large = Value -} -case class Shirt(id: Int, size: Size.Value) -case class Shirt2(id: Int, size: Either[Size.Value, Int]) - -class Busted(val name: String, val age: Int) -case class Busted2(name: String, m: Map[String, String]) -case class Busted3(name: String, isOk: Boolean) -case class Busted4(name: String, stuff: List[Int]) - -trait Address { val postalCode: String } -case class DefaultAddress( postalCode: String = "unknown" ) extends Address \ No newline at end of file diff --git a/core/src/test/scala/co.blocke.scalajack/json/collections/AnyCollections.scala b/core/src/test/scala/co.blocke.scalajack/json/collections/AnyCollections.scala deleted file mode 100644 index 83f1d8eb..00000000 --- a/core/src/test/scala/co.blocke.scalajack/json/collections/AnyCollections.scala +++ /dev/null @@ -1,66 +0,0 @@ -package co.blocke.scalajack -package json.collections - -import TestUtil._ -import munit._ -import munit.internal.console -import co.blocke.scalajack.json.JSON -import co.blocke.scala_reflection._ - -class AnyCollections() extends FunSuite: - - val sj = co.blocke.scalajack.ScalaJack() - - test("List works (Int)") { - describe("--------------------------\n: Any Collection Tests :\n--------------------------", Console.BLUE) - val inst: Any = List(1, 2, 3) - val js = sj.render(inst) - assertEquals("""[1,2,3]""".asInstanceOf[JSON], js) - assert(List(1, 2, 3) == sj.read[Any](js)) - } - - test("List works (String)") { - val inst: Any = List("one", "two", "three") - val js = sj.render(inst) - assertEquals("""["one","two","three"]""".asInstanceOf[JSON], js) - assert(List("one", "two", "three") == sj.read[Any](js)) - } - - test("First-Level List works (Class)") { - val inst: Any = List(Player("Mike", 34), Player("Sarah", 29)) - val js = sj.render(inst) - assertEquals( - """[{"_hint":"co.blocke.scalajack.json.collections.Player","name":"Mike","age":34},{"_hint":"co.blocke.scalajack.json.collections.Player","name":"Sarah","age":29}]""".asInstanceOf[JSON], js) - assert(List(Player("Mike", 34), Player("Sarah", 29)) == sj.read[List[Any]](js)) - } - - test("Map works (Int,Int)") { - val inst: Any = Map(1 -> 2, 3 -> 4) - val js = sj.render(inst) - assertEquals("""{"1":2,"3":4}""".asInstanceOf[JSON], js) - assertEquals(inst,sj.read[Any](js)) - } - - test("Map works (String,Int)") { - val inst: Any = Map("yes" -> 1, "no" -> 2) - val js = sj.render(inst) - assertEquals("""{"yes":1,"no":2}""".asInstanceOf[JSON], js) - assert(Map("yes" -> 1, "no" -> 2) == sj.read[Any](js)) - } - - test("First-Level Map works (Class)") { - val js = """{"_hint":"co.blocke.scalajack.json.collections.Player","name":"Mike","age":34}""".asInstanceOf[JSON] - assert(Player("Mike", 34) == sj.read[Any](js)) - } - - test("Second-Level Map works (Class keys) (Class)") { - val js = - """{"{\"_hint\":\"co.blocke.scalajack.json.collections.Player\",\"name\":\"Mike\",\"age\":34}":15, "{\"name\":\"Mike\",\"age\":34}":16}""".asInstanceOf[JSON] - assert(Map(Player("Mike", 34) -> 15, Map("name" -> "Mike", "age" -> 34) -> 16) == sj.read[Any](js)) - } - - test("Second-Level Map (List keys) works (Class)") { - val js = - """{"[{\"_hint\":\"co.blocke.scalajack.json.collections.Player\",\"name\":\"Mike\",\"age\":34},{\"name\":\"Mike\",\"age\":34}]":15}""".asInstanceOf[JSON] - assert(Map(List(Player("Mike", 34), Map("name" -> "Mike", "age" -> 34)) -> 15) == sj.read[Any](js)) - } diff --git a/core/src/test/scala/co.blocke.scalajack/json/collections/Arrays.scala b/core/src/test/scala/co.blocke.scalajack/json/collections/Arrays.scala deleted file mode 100644 index 1993b1dd..00000000 --- a/core/src/test/scala/co.blocke.scalajack/json/collections/Arrays.scala +++ /dev/null @@ -1,244 +0,0 @@ -package co.blocke.scalajack -package json.collections - -import co.blocke.scala_reflection._ -import scala.math._ -import java.util.UUID -import TestUtil._ -import munit._ -import munit.internal.console -import co.blocke.scalajack.json.JSON - -class Arrays() extends FunSuite: - - val sj = co.blocke.scalajack.ScalaJack() - - test("BigDecimal must work") { - describe("-----------------------\n: Scala Array Tests :\n-----------------------", Console.BLUE) - describe("+++ Primitive Types +++") - - val inst = BigDecimalArr(null, Array(BigDecimal(123.456),BigDecimal(78.91))) - val js = sj.render(inst) - assertEquals( - """{"a1":null,"a2":[123.456,78.91]}""".asInstanceOf[JSON], - js - ) - val i2 = sj.read[BigDecimalArr](js) - assertEquals(i2.a1, null) - assert(inst.a2.sameElements(i2.a2)) - } - - test("BigInt must work") { - val inst = BigIntArr(null, Array(BigInt(123),BigInt(78))) - val js = sj.render(inst) - assertEquals( - """{"a1":null,"a2":[123,78]}""".asInstanceOf[JSON], - js - ) - val i2 = sj.read[BigIntArr](js) - assertEquals(i2.a1, null) - assert(inst.a2.sameElements(i2.a2)) - } - - test("Boolean must work") { - val inst = BooleanArr(null, Array(true,false)) - val js = sj.render(inst) - assertEquals( - """{"a1":null,"a2":[true,false]}""".asInstanceOf[JSON], - js - ) - val i2 = sj.read[BooleanArr](js) - assertEquals(i2.a1, null) - assert(inst.a2.sameElements(i2.a2)) - } - - // No Array[Byte] because that's seen as binary data and treated differently - test("Char must work") { - val inst = CharArr(null, Array('a','b','c')) - val js = sj.render(inst) - assertEquals( - """{"a1":null,"a2":["a","b","c"]}""".asInstanceOf[JSON], - js - ) - val i2 = sj.read[CharArr](js) - assertEquals(i2.a1, null) - assert(inst.a2.sameElements(i2.a2)) - } - - test("Double must work") { - val inst = DoubleArr(null, Array(12.34,56.78)) - val js = sj.render(inst) - assertEquals( - """{"a1":null,"a2":[12.34,56.78]}""".asInstanceOf[JSON], - js - ) - val i2 = sj.read[DoubleArr](js) - assertEquals(i2.a1, null) - assert(inst.a2.sameElements(i2.a2)) - } - - test("Float must work") { - val inst = FloatArr(null, Array(12.34F,56.78F)) - val js = sj.render(inst) - assertEquals( - """{"a1":null,"a2":[12.34,56.78]}""".asInstanceOf[JSON], - js - ) - val i2 = sj.read[FloatArr](js) - assertEquals(i2.a1, null) - assert(inst.a2.sameElements(i2.a2)) - } - - test("Int must work") { - val inst = IntArr(null, Array(1,2,3)) - val js = sj.render(inst) - assertEquals( - """{"a1":null,"a2":[1,2,3]}""".asInstanceOf[JSON], - js - ) - val i2 = sj.read[IntArr](js) - assertEquals(i2.a1, null) - assert(inst.a2.sameElements(i2.a2)) - } - - test("Long must work") { - val inst = LongArr(null, Array(1L,2L,3L)) - val js = sj.render(inst) - assertEquals( - """{"a1":null,"a2":[1,2,3]}""".asInstanceOf[JSON], - js - ) - val i2 = sj.read[LongArr](js) - assertEquals(i2.a1, null) - assert(inst.a2.sameElements(i2.a2)) - } - - test("Short must work") { - val inst = ShortArr(null, Array(1.toShort,2.toShort,3.toShort)) - val js = sj.render(inst) - assertEquals( - """{"a1":null,"a2":[1,2,3]}""".asInstanceOf[JSON], - js - ) - val i2 = sj.read[ShortArr](js) - assertEquals(i2.a1, null) - assert(inst.a2.sameElements(i2.a2)) - } - - test("String must work") { - val inst = StringArr(null, Array("a","b","c")) - val js = sj.render(inst) - assertEquals( - """{"a1":null,"a2":["a","b","c"]}""".asInstanceOf[JSON], - js - ) - val i2 = sj.read[StringArr](js) - assertEquals(i2.a1, null) - assert(inst.a2.sameElements(i2.a2)) - } - - test("Lists must work") { - describe("+++ Collection Types +++") - val inst = ListArr(Array( List(1,2,3), List(4,5,6) )) - val js = sj.render(inst) - assertEquals( - """{"a1":[[1,2,3],[4,5,6]]}""".asInstanceOf[JSON], - js - ) - val i2 = sj.read[ListArr](js) - assert(inst.a1.sameElements(i2.a1)) - - // Try another Seq variant - val inst2 = SetArr(Array( Set(1,2,3), Set(4,5,6) )) - val js2 = sj.render(inst2) - assertEquals( - """{"a1":[[1,2,3],[4,5,6]]}""".asInstanceOf[JSON], - js2 - ) - val i3: SetArr = sj.read[SetArr](js) - assert(inst2.a1.sameElements(i3.a1)) - } - - test("Maps must work") { - val inst = MapArr(Array( Map("a"->1,"b"->2), Map("c"->3,"d"->4) )) - val js = sj.render(inst) - assertEquals( - """{"a1":[{"a":1,"b":2},{"c":3,"d":4}]}""".asInstanceOf[JSON], - js - ) - val i2 = sj.read[MapArr](js) - assert(inst.a1.sameElements(i2.a1)) - } - - test("Classes must work") { - describe("+++ Class Types +++") - val inst = ClassArr(Array(IntArr(null,Array(1,2)), IntArr(null,Array(1,2)))) - val js = sj.render(inst) - assertEquals( - """{"a1":[{"a1":null,"a2":[1,2]},{"a1":null,"a2":[1,2]}]}""".asInstanceOf[JSON], - js - ) - val i2 = sj.read[ClassArr](js) - assertEquals(inst.a1.size, i2.a1.size) - assertEquals(i2.a1(0).a1,null) - assert(inst.a1(0).a2.sameElements(i2.a1(0).a2)) - assertEquals(i2.a1(1).a1,null) - assert(inst.a1(1).a2.sameElements(i2.a1(1).a2)) - } - - test("Multidimensional arrays must work") { - describe("+++ Complex Types +++") - val inst = MultiArr(null, Array(Array( Array(1L,2L), Array(3L,4L) ), Array(Array(5L,6L), Array(7L,8L)) ), - Array(Array(BigInt(12),BigInt(13)), Array(BigInt(14),BigInt(15)))) - val js = sj.render(inst) - assertEquals( - """{"a0":null,"a1":[[[1,2],[3,4]],[[5,6],[7,8]]],"a2":[[12,13],[14,15]]}""".asInstanceOf[JSON], - js - ) - val i2 = sj.read[MultiArr](js) - assertEquals(inst.a1.size, i2.a1.size) - assertEquals(inst.a2.size, i2.a2.size) - assertEquals(i2.a0,null) - assert(inst.a1(0)(0).sameElements(i2.a1(0)(0))) - assert(inst.a1(0)(1).sameElements(i2.a1(0)(1))) - assert(inst.a1(1)(0).sameElements(i2.a1(1)(0))) - assert(inst.a1(1)(1).sameElements(i2.a1(1)(1))) - assert(inst.a2(0).sameElements(i2.a2(0))) - assert(inst.a2(1).sameElements(i2.a2(1))) - } - - - test("Java primitives must work") { - describe("----------------------\n: Java Array Tests :\n----------------------", Console.BLUE) - describe("+++ Primitive Types +++") - val inst = JavaArray() - inst.setBigDecs(Array(java.math.BigDecimal.valueOf(123.4),java.math.BigDecimal.valueOf(456.7))) - inst.setBigInts(Array(java.math.BigInteger.valueOf(123),java.math.BigInteger.valueOf(456))) - inst.setBooleans(Array(java.lang.Boolean.valueOf(true),java.lang.Boolean.valueOf("false"))) - inst.setBytes(Array(java.lang.Byte.valueOf(24.toByte),java.lang.Byte.valueOf(12.toByte))) - inst.setCharacters(Array(java.lang.Character.valueOf('a'),java.lang.Character.valueOf('z'))) - inst.setDoubles(Array(java.lang.Double.valueOf(12.34),java.lang.Double.valueOf(-56.78)).asInstanceOf[Array[java.lang.Double]]) - inst.setFloats(Array(java.lang.Float.valueOf(12.34F),java.lang.Float.valueOf(-56.78F))) - inst.setIntegers(Array(java.lang.Integer.valueOf(1),java.lang.Integer.valueOf(2))) - inst.setLongs(Array(java.lang.Long.valueOf(1),java.lang.Long.valueOf(2))) - inst.setShorts(Array(java.lang.Short.valueOf(1.toShort),java.lang.Short.valueOf(2.toShort))) - inst.setMulti(Array(Array(java.math.BigInteger.valueOf(123),java.math.BigInteger.valueOf(456)), - Array(java.math.BigInteger.valueOf(543),java.math.BigInteger.valueOf(222)))) - val js = sj.render(inst) - assertEquals( - """{"bigDecs":[123.4,456.7],"bigInts":[123,456],"booleans":[true,false],"bytes":[24,12],"characters":["a","z"],"doubles":[12.34,-56.78],"floats":[12.34,-56.78],"integers":[1,2],"longs":[1,2],"multi":[[123,456],[543,222]],"shorts":[1,2]}""".asInstanceOf[JSON], - js) - val readIn = sj.read[JavaArray](js) - assert(inst.getBigDecs.sameElements(readIn.getBigDecs)) - assert(inst.getBigInts.sameElements(readIn.getBigInts)) - assert(inst.getBooleans.sameElements(readIn.getBooleans)) - assert(inst.getBytes.sameElements(readIn.getBytes)) - assert(inst.getCharacters.sameElements(readIn.getCharacters)) - assert(inst.getDoubles.sameElements(readIn.getDoubles)) - assert(inst.getFloats.sameElements(readIn.getFloats)) - assert(inst.getIntegers.sameElements(readIn.getIntegers)) - assert(inst.getLongs.sameElements(readIn.getLongs)) - assert(inst.getShorts.sameElements(readIn.getShorts)) - assert(inst.getMulti()(0).sameElements(readIn.getMulti()(0))) - assert(inst.getMulti()(1).sameElements(readIn.getMulti()(1))) - } diff --git a/core/src/test/scala/co.blocke.scalajack/json/collections/Maps.scala b/core/src/test/scala/co.blocke.scalajack/json/collections/Maps.scala deleted file mode 100644 index 9fac2c16..00000000 --- a/core/src/test/scala/co.blocke.scalajack/json/collections/Maps.scala +++ /dev/null @@ -1,342 +0,0 @@ -package co.blocke.scalajack -package json.collections - -import co.blocke.scala_reflection._ -import scala.math._ -import java.util.UUID -import TestUtil._ -import munit._ -import munit.internal.console -import co.blocke.scalajack.json.JSON -import scala.collection.immutable._ -import scala.jdk.CollectionConverters._ -import scala.language.implicitConversions - -class Maps() extends FunSuite: - - val sj = co.blocke.scalajack.ScalaJack() - - test("BigDecimal must work") { - describe("---------------------\n: Scala Map Tests :\n---------------------", Console.BLUE) - describe("+++ Primitive Types +++") - - val inst = BigDecimalMap(null, Map(BigDecimal(123.456)->"a",BigDecimal(78.91)->"b"), Map("a"->BigDecimal(123.456),"b"->BigDecimal(78.91))) - val js = sj.render(inst) - assertEquals( - """{"a0":null,"a1":{"123.456":"a","78.91":"b"},"a2":{"a":123.456,"b":78.91}}""".asInstanceOf[JSON], - js - ) - assertEquals(inst, sj.read[BigDecimalMap](js)) - } - - test("BigInt must work") { - val inst = BigIntMap(null, Map(BigInt(123)->"a",BigInt(456)->"b"), Map("a"->BigInt(789),"b"->BigInt(321))) - val js = sj.render(inst) - assertEquals( - """{"a0":null,"a1":{"123":"a","456":"b"},"a2":{"a":789,"b":321}}""".asInstanceOf[JSON], - js - ) - assertEquals(inst, sj.read[BigIntMap](js)) - } - - test("Boolean must work") { - val inst = BooleanMap(null, Map(true->"a",false->"b"), Map("a"->true,"b"->false)) - val js = sj.render(inst) - assertEquals( - """{"a0":null,"a1":{"true":"a","false":"b"},"a2":{"a":true,"b":false}}""".asInstanceOf[JSON], - js - ) - assertEquals(inst, sj.read[BooleanMap](js)) - } - - test("Byte must work") { - val inst = ByteMap(null, Map(250.toByte->"a",200.toByte->"b"), Map("a"->150.toByte,"b"->100.toByte)) - val js = sj.render(inst) - assertEquals( - """{"a0":null,"a1":{"-6":"a","-56":"b"},"a2":{"a":-106,"b":100}}""".asInstanceOf[JSON], - js - ) - assertEquals(inst, sj.read[ByteMap](js)) - } - - test("Char must work") { - val inst = CharMap(null, Map('t'->"a",'u'->"b"), Map("a"->'v',"b"->'w')) - val js = sj.render(inst) - assertEquals( - """{"a0":null,"a1":{"t":"a","u":"b"},"a2":{"a":"v","b":"w"}}""".asInstanceOf[JSON], - js - ) - assertEquals(inst, sj.read[CharMap](js)) - } - - test("Double must work") { - val inst = DoubleMap(null, Map(12.34->"a",45.67->"b"), Map("a"->67.89,"b"->1923.432)) - val js = sj.render(inst) - assertEquals( - """{"a0":null,"a1":{"12.34":"a","45.67":"b"},"a2":{"a":67.89,"b":1923.432}}""".asInstanceOf[JSON], - js - ) - assertEquals(inst, sj.read[DoubleMap](js)) - } - - test("Float must work") { - val inst = FloatMap(null, Map(12.34F->"a",45.67F->"b"), Map("a"->67.89F,"b"->1923.432F)) - val js = sj.render(inst) - assertEquals( - """{"a0":null,"a1":{"12.34":"a","45.67":"b"},"a2":{"a":67.89,"b":1923.432}}""".asInstanceOf[JSON], - js - ) - assertEquals(inst, sj.read[FloatMap](js)) - } - - test("Int must work") { - val inst = IntMap2(null, Map(12->"a",-45->"b"), Map("a"->67,"b"->1923)) - val js = sj.render(inst) - assertEquals( - """{"a0":null,"a1":{"12":"a","-45":"b"},"a2":{"a":67,"b":1923}}""".asInstanceOf[JSON], - js - ) - assertEquals(inst, sj.read[IntMap2](js)) - } - - test("Long must work") { - val inst = LongMap2(null, Map(12L->"a",-45L->"b"), Map("a"->67L,"b"->1923L)) - val js = sj.render(inst) - assertEquals( - """{"a0":null,"a1":{"12":"a","-45":"b"},"a2":{"a":67,"b":1923}}""".asInstanceOf[JSON], - js - ) - assertEquals(inst, sj.read[LongMap2](js)) - } - - test("Short must work") { - val inst = ShortMap2(null, Map(12.toShort->"a",-45.toShort->"b"), Map("a"->67.toShort,"b"->19.toShort)) - val js = sj.render(inst) - assertEquals( - """{"a0":null,"a1":{"12":"a","-45":"b"},"a2":{"a":67,"b":19}}""".asInstanceOf[JSON], - js - ) - assertEquals(inst, sj.read[ShortMap2](js)) - } - - test("Arrays must work") { - describe("+++ Collection Types +++") - val inst = ArrayMap( Map("a"->Array(1,2,3),"b"->Array(4,5,6)), Map(Array(1,2,3)->"a",Array(4,5,6)->"b") ) - val js = sj.render(inst) - assertEquals( - """{"a1":{"a":[1,2,3],"b":[4,5,6]},"a2":{"[1,2,3]":"a","[4,5,6]":"b"}}""".asInstanceOf[JSON], - js - ) - val i2 = sj.read[ArrayMap](js) - assert( inst.a1("a").sameElements(i2.a1("a")) ) - assert( inst.a1("b").sameElements(i2.a1("b")) ) - assert( inst.a2.keySet.toList(0).sameElements(i2.a2.keySet.toList(0))) - assert( inst.a2.keySet.toList(1).sameElements(i2.a2.keySet.toList(1))) - } - - test("Maps must work") { - val inst = SeqMap2( Map("a"->List(1,2,3),"b"->List(4,5,6)), Map(List(1,2,3)->"a",List(4,5,6)->"b") ) - val js = sj.render(inst) - assertEquals( - """{"a1":{"a":[1,2,3],"b":[4,5,6]},"a2":{"[1,2,3]":"a","[4,5,6]":"b"}}""".asInstanceOf[JSON], - js - ) - assertEquals(inst, sj.read[SeqMap2](js)) - } - - test("Classes must work") { - describe("+++ Class Types +++") - val inst = ClassMap(Map("a"->IntArr(Array(1,2),null),"b"->IntArr(Array(3,4),null)), Map(IntArr(Array(1,2),null)->"a",IntArr(Array(3,4),null)->"b")) - val js = sj.render(inst) - assertEquals( - """{"a1":{"a":{"a1":[1,2],"a2":null},"b":{"a1":[3,4],"a2":null}},"a2":{"{\"a1\":[1,2],\"a2\":null}":"a","{\"a1\":[3,4],\"a2\":null}":"b"}}""".asInstanceOf[JSON], - js - ) - val i2 = sj.read[ClassMap](js) - assert( inst.a1("a").a1.sameElements(i2.a1("a").a1) ) - assertEquals( i2.a1("a").a2, null ) - assert( inst.a1("b").a1.sameElements(i2.a1("b").a1) ) - assertEquals( i2.a1("b").a2, null ) - assert( inst.a2.keySet.toList(0).a1.sameElements(i2.a2.keySet.toList(0).a1) ) - assertEquals( i2.a2.keySet.toList(0).a2, null ) - assert( inst.a2.keySet.toList(1).a1.sameElements(i2.a2.keySet.toList(1).a1) ) - assertEquals( i2.a2.keySet.toList(1).a2, null ) - } - - test("Multidimensional arrays must work") { - describe("+++ Complex Types +++") - val inst = MultiMap( Map( Map("a"->true,"b"->false)->1, Map("c"->true,"d"->false)->2), Map( 3->Map("a"->true,"b"->false), 4->Map("c"->true,"d"->false)) ) - val js = sj.render(inst) - assertEquals( - """{"a1":{"{\"a\":true,\"b\":false}":1,"{\"c\":true,\"d\":false}":2},"a2":{"3":{"a":true,"b":false},"4":{"c":true,"d":false}}}""".asInstanceOf[JSON], - js - ) - assertEquals(inst, sj.read[MultiMap](js)) - } - - test("BigDecimal must work") { - describe("--------------------\n: Java Map Tests :\n--------------------", Console.BLUE) - describe("+++ Primitive Types +++") - - val inst = JBigDecimalMap(null, new java.util.HashMap( Map(BigDecimal(123.456)->"a",BigDecimal(78.91)->"b").asJava ), java.util.HashMap( Map("a"->BigDecimal(123.456),"b"->BigDecimal(78.91)).asJava )) - val js = sj.render(inst) - assertEquals( - """{"a0":null,"a1":{"123.456":"a","78.91":"b"},"a2":{"a":123.456,"b":78.91}}""".asInstanceOf[JSON], - js - ) - assertEquals(inst, sj.read[JBigDecimalMap](js)) - } - - test("BigInt must work") { - val inst = JBigIntMap(null, new java.util.HashMap(Map(BigInt(123)->"a",BigInt(456)->"b").asJava), new java.util.HashMap(Map("a"->BigInt(789),"b"->BigInt(321)).asJava)) - val js = sj.render(inst) - assertEquals( - """{"a0":null,"a1":{"456":"b","123":"a"},"a2":{"a":789,"b":321}}""".asInstanceOf[JSON], - js - ) - assertEquals(inst, sj.read[JBigIntMap](js)) - } - - test("Boolean must work") { - val inst = JBooleanMap(null, java.util.HashMap(Map(true->"a",false->"b").asJava), java.util.HashMap(Map("a"->true,"b"->false).asJava)) - val js = sj.render(inst) - assertEquals( - """{"a0":null,"a1":{"false":"b","true":"a"},"a2":{"a":true,"b":false}}""".asInstanceOf[JSON], - js - ) - assertEquals(inst, sj.read[JBooleanMap](js)) - } - - test("Byte must work") { - val inst = JByteMap(null, java.util.HashMap(Map(250.toByte->"a",200.toByte->"b").asJava), java.util.HashMap(Map("a"->150.toByte,"b"->100.toByte).asJava)) - val js = sj.render(inst) - assertEquals( - """{"a0":null,"a1":{"-6":"a","-56":"b"},"a2":{"a":-106,"b":100}}""".asInstanceOf[JSON], - js - ) - assertEquals(inst, sj.read[JByteMap](js)) - } - - test("Char must work") { - val inst = JCharMap(null, java.util.HashMap(Map('t'->"a",'u'->"b").asJava), java.util.HashMap(Map("a"->'v',"b"->'w').asJava)) - val js = sj.render(inst) - assertEquals( - """{"a0":null,"a1":{"t":"a","u":"b"},"a2":{"a":"v","b":"w"}}""".asInstanceOf[JSON], - js - ) - assertEquals(inst, sj.read[JCharMap](js)) - } - - test("Double must work") { - val inst = JDoubleMap(null, java.util.HashMap(Map(12.34->"a",45.67->"b").asJava), java.util.HashMap(Map("a"->67.89,"b"->1923.432).asJava)) - val js = sj.render(inst) - assertEquals( - """{"a0":null,"a1":{"45.67":"b","12.34":"a"},"a2":{"a":67.89,"b":1923.432}}""".asInstanceOf[JSON], - js - ) - assertEquals(inst, sj.read[JDoubleMap](js)) - } - - test("Float must work") { - val inst = JFloatMap(null, java.util.HashMap(Map(12.34F->"a",45.67F->"b").asJava), java.util.HashMap(Map("a"->67.89F,"b"->1923.432F).asJava)) - val js = sj.render(inst) - assertEquals( - """{"a0":null,"a1":{"12.34":"a","45.67":"b"},"a2":{"a":67.89,"b":1923.432}}""".asInstanceOf[JSON], - js - ) - assertEquals(inst, sj.read[JFloatMap](js)) - } - - test("Int must work") { - val inst = JIntMap2(null, java.util.HashMap(Map(12->"a",-45->"b").asJava), java.util.HashMap(Map("a"->67,"b"->1923).asJava)) - val js = sj.render(inst) - assertEquals( - """{"a0":null,"a1":{"12":"a","-45":"b"},"a2":{"a":67,"b":1923}}""".asInstanceOf[JSON], - js - ) - assertEquals(inst, sj.read[JIntMap2](js)) - } - - test("Long must work") { - val inst = JLongMap2(null, java.util.HashMap(Map(12L->"a",-45L->"b").asJava), java.util.HashMap(Map("a"->67L,"b"->1923L).asJava)) - val js = sj.render(inst) - assertEquals( - """{"a0":null,"a1":{"12":"a","-45":"b"},"a2":{"a":67,"b":1923}}""".asInstanceOf[JSON], - js - ) - assertEquals(inst, sj.read[JLongMap2](js)) - } - - test("Short must work") { - val inst = JShortMap2(null, java.util.HashMap(Map(12.toShort->"a",-45.toShort->"b").asJava), java.util.HashMap(Map("a"->67.toShort,"b"->19.toShort).asJava)) - val js = sj.render(inst) - assertEquals( - """{"a0":null,"a1":{"12":"a","-45":"b"},"a2":{"a":67,"b":19}}""".asInstanceOf[JSON], - js - ) - assertEquals(inst, sj.read[JShortMap2](js)) - } - - test("Lists must work") { - val hm1 = new java.util.HashMap[String,scala.collection.mutable.Seq[Int]]() - hm1.put("a",scala.collection.mutable.Seq(1,2,3) ) - hm1.put("b",scala.collection.mutable.Seq(4,5,6) ) - val hm2 = new java.util.HashMap[scala.collection.mutable.Seq[Int],String]() - hm2.put(scala.collection.mutable.Seq(1,2,3),"a" ) - hm2.put(scala.collection.mutable.Seq(4,5,6),"b" ) - - val inst = JSeqMap2(hm1, hm2) - val js = sj.render(inst) - assertEquals( - """{"a1":{"a":[1,2,3],"b":[4,5,6]},"a2":{"[1,2,3]":"a","[4,5,6]":"b"}}""".asInstanceOf[JSON], - js - ) - assertEquals(inst, sj.read[JSeqMap2](js)) - } - - test("Classes must work") { - describe("+++ Class Types +++") - - val hm1 = new java.util.HashMap[String,IntSeq]() - hm1.put("a",IntSeq(List(1,2),null)) - hm1.put("b",IntSeq(List(3,4),null)) - val hm2 = new java.util.HashMap[IntSeq,String]() - hm2.put(IntSeq(List(1,2),null),"a") - hm2.put(IntSeq(List(3,4),null),"b") - - val inst = JClassMap(hm1, hm2) - val js = sj.render(inst) - assertEquals( - """{"a1":{"a":{"a1":[1,2],"a2":null},"b":{"a1":[3,4],"a2":null}},"a2":{"{\"a1\":[1,2],\"a2\":null}":"a","{\"a1\":[3,4],\"a2\":null}":"b"}}""".asInstanceOf[JSON], - js - ) - assertEquals(inst, sj.read[JClassMap](js)) - } - - test("Multidimensional arrays must work") { - describe("+++ Complex Types +++") - - //case class JMultiMap( a1: java.util.HashMap[java.util.HashMap[String,Boolean],Int], a2: java.util.HashMap[Int,java.util.HashMap[String,Boolean]] ) - val m1: java.util.HashMap[String,Boolean] = java.util.HashMap( Map( ("a",true), ("b",false) ).asJava ) - val m2: java.util.HashMap[String,Boolean] = java.util.HashMap( Map( ("c",true), ("d",false) ).asJava ) - val inst = JMultiMap( - java.util.HashMap( - Map( - (m1, 1), - (m2, 2) - ).asJava - ), - java.util.HashMap( - Map( - (1, m1), - (2, m2) - ).asJava - ) - ) - val js = sj.render(inst) - assertEquals( - """{"a1":{"{\"a\":true,\"b\":false}":1,"{\"d\":false,\"c\":true}":2},"a2":{"1":{"a":true,"b":false},"2":{"d":false,"c":true}}}""".asInstanceOf[JSON], - js - ) - assertEquals(inst, sj.read[JMultiMap](js)) - } diff --git a/core/src/test/scala/co.blocke.scalajack/json/collections/Model.scala b/core/src/test/scala/co.blocke.scalajack/json/collections/Model.scala deleted file mode 100644 index c5334dee..00000000 --- a/core/src/test/scala/co.blocke.scalajack/json/collections/Model.scala +++ /dev/null @@ -1,105 +0,0 @@ -package co.blocke.scalajack -package json.collections - -import scala.math._ -import scala.collection.Map -import scala.collection.Seq - -//------ Arrays (Scala) -case class BigDecimalArr( a1: Array[BigDecimal], a2: Array[BigDecimal] ) -case class BigIntArr( a1: Array[BigInt], a2: Array[BigInt] ) -case class BooleanArr( a1: Array[Boolean], a2: Array[Boolean] ) -case class CharArr( a1: Array[Char], a2: Array[Char] ) -case class DoubleArr( a1: Array[Double], a2: Array[Double] ) -case class FloatArr( a1: Array[Float], a2: Array[Float] ) -case class IntArr( a1: Array[Int], a2: Array[Int] ) -case class LongArr( a1: Array[Long], a2: Array[Long] ) -case class ShortArr( a1: Array[Short], a2: Array[Short] ) -case class StringArr( a1: Array[String], a2: Array[String] ) - -case class MultiArr( a0: Array[Array[Boolean]], a1: Array[Array[Array[Long]]], a2: Array[Array[BigInt]] ) -case class ClassArr( a1: Array[IntArr] ) - -case class ListArr( a1: Array[List[Int]]) -case class SetArr( a1: Array[Set[Int]]) -case class MapArr( a1: Array[Map[String,Int]]) - -//------ Seqs (Scala) -case class BigDecimalSeq( a1: Seq[BigDecimal], a2: Seq[BigDecimal] ) -case class BigIntSeq( a1: Seq[BigInt], a2: Seq[BigInt] ) -case class BooleanSeq( a1: Seq[Boolean], a2: Seq[Boolean] ) -case class ByteSeq( a1: Seq[Byte], a2: Seq[Byte] ) -case class CharSeq( a1: Seq[Char], a2: Seq[Char] ) -case class DoubleSeq( a1: Seq[Double], a2: Seq[Double] ) -case class FloatSeq( a1: Seq[Float], a2: Seq[Float] ) -case class IntSeq( a1: Seq[Int], a2: Seq[Int] ) -case class LongSeq( a1: Seq[Long], a2: Seq[Long] ) -case class ShortSeq( a1: Seq[Short], a2: Seq[Short] ) -case class StringSeq( a1: Seq[String], a2: Seq[String] ) - -case class MultiSeq( a0: Seq[Seq[Boolean]], a1: Seq[Seq[Seq[Long]]], a2: Seq[Seq[BigInt]] ) -case class ClassSeq( a1: Seq[IntArr] ) - -case class SeqSeq( a1: Seq[List[Int]]) -case class MapSeq( a1: Seq[Map[String,Int]]) - -//------ Seqs (Java) -case class JBigDecimalSeq( a1: java.util.LinkedList[BigDecimal], a2: java.util.LinkedList[BigDecimal] ) -case class JBigIntSeq( a1: java.util.Vector[BigInt], a2: java.util.Vector[BigInt] ) -case class JBooleanSeq( a1: java.util.ArrayList[Boolean], a2: java.util.ArrayList[Boolean] ) -case class JCharSeq( a1: java.util.PriorityQueue[Char], a2: java.util.PriorityQueue[Char] ) -case class JIntSeq( a1: java.util.Stack[Int], a2: java.util.Stack[Int] ) -// Other primitive-type tests would simply be redundant in this series... we've already confirmed Java primtivie serializations. -case class JClassSeq( a1: java.util.LinkedList[IntArr] ) -case class JMapSeq( a1: java.util.ArrayList[Map[String,Int]]) - - -//------ Maps (Some "2" variants to avoid same-name collision with Scala collection classes) -case class BigDecimalMap( a0: Map[BigDecimal,String], a1: Map[BigDecimal,String], a2: Map[String,BigDecimal] ) -case class BigIntMap( a0: Map[BigInt,String], a1: Map[BigInt,String], a2: Map[String,BigInt] ) -case class BooleanMap( a0: Map[Boolean,String], a1: Map[Boolean,String], a2: Map[String,Boolean] ) -case class ByteMap( a0: Map[Byte,String], a1: Map[Byte,String], a2: Map[String,Byte] ) -case class CharMap( a0: Map[Char,String], a1: Map[Char,String], a2: Map[String,Char] ) -case class DoubleMap( a0: Map[Double,String], a1: Map[Double,String], a2: Map[String,Double] ) -case class FloatMap( a0: Map[Float,String], a1: Map[Float,String], a2: Map[String,Float] ) -case class IntMap2( a0: Map[Int,String], a1: Map[Int,String], a2: Map[String,Int] ) -case class LongMap2( a0: Map[Long,String], a1: Map[Long,String], a2: Map[String,Long] ) -case class ShortMap2( a0: Map[Short,String], a1: Map[Short,String], a2: Map[String,Short] ) - -case class MultiMap( a1: Map[Map[String,Boolean],Int], a2: Map[Int,Map[String,Boolean]] ) -case class ClassMap( a1: Map[String,IntArr], a2: Map[IntArr,String] ) -case class ArrayMap( a1: Map[String,Array[Int]], a2:Map[Array[Int],String]) -case class SeqMap2( a1: Map[String,Seq[Int]], a2: Map[Seq[Int],String]) - -//------ Maps (Java) -case class JBigDecimalMap( a0: java.util.HashMap[BigDecimal,String], a1: java.util.HashMap[BigDecimal,String], a2: java.util.HashMap[String,BigDecimal] ) -case class JBigIntMap( a0: java.util.HashMap[BigInt,String], a1: java.util.HashMap[BigInt,String], a2: java.util.HashMap[String,BigInt] ) -case class JBooleanMap( a0: java.util.HashMap[Boolean,String], a1: java.util.HashMap[Boolean,String], a2: java.util.HashMap[String,Boolean] ) -case class JByteMap( a0: java.util.HashMap[Byte,String], a1: java.util.HashMap[Byte,String], a2: java.util.HashMap[String,Byte] ) -case class JCharMap( a0: java.util.HashMap[Char,String], a1: java.util.HashMap[Char,String], a2: java.util.HashMap[String,Char] ) -case class JDoubleMap( a0: java.util.HashMap[Double,String], a1: java.util.HashMap[Double,String], a2: java.util.HashMap[String,Double] ) -case class JFloatMap( a0: java.util.HashMap[Float,String], a1: java.util.HashMap[Float,String], a2: java.util.HashMap[String,Float] ) -case class JIntMap2( a0: java.util.HashMap[Int,String], a1: java.util.HashMap[Int,String], a2: java.util.HashMap[String,Int] ) -case class JLongMap2( a0: java.util.HashMap[Long,String], a1: java.util.HashMap[Long,String], a2: java.util.HashMap[String,Long] ) -case class JShortMap2( a0: java.util.HashMap[Short,String], a1: java.util.HashMap[Short,String], a2: java.util.HashMap[String,Short] ) - -case class JMultiMap( a1: java.util.HashMap[java.util.HashMap[String,Boolean],Int], a2: java.util.HashMap[Int,java.util.HashMap[String,Boolean]] ) -case class JClassMap( a1: java.util.HashMap[String,IntSeq], a2: java.util.HashMap[IntSeq,String] ) -case class JArrayMap( a1: java.util.HashMap[String,Array[Int]], a2:java.util.HashMap[Array[Int],String]) -case class JSeqMap2( a1: java.util.HashMap[String,scala.collection.mutable.Seq[Int]], a2: java.util.HashMap[scala.collection.mutable.Seq[Int],String]) - -//------- Options -trait Person { val name: String } -case class SomeClass(name: String, age: Int) extends Person -trait Thing[A, B] { val a: A; val b: B } -case class AThing[Y, X](a: X, b: Y) extends Thing[X, Y] -case class OptionBigInt(o: Option[BigInt]) -case class OptionClass(name: String, age: Option[Int]) -case class OptionTuple(foo: Int, t: (Boolean, Option[String], Int)) - -case class OptionalBigInt(o: java.util.Optional[BigInt]) -case class OptionalClass(name: String, age: java.util.Optional[Int]) -case class OptionalTuple(foo: Int, t: (Boolean, java.util.Optional[String], Int)) - -//------- Any -case class Player(name: String, age: Int) \ No newline at end of file diff --git a/core/src/test/scala/co.blocke.scalajack/json/collections/Options.scala b/core/src/test/scala/co.blocke.scalajack/json/collections/Options.scala deleted file mode 100644 index 35ba45bc..00000000 --- a/core/src/test/scala/co.blocke.scalajack/json/collections/Options.scala +++ /dev/null @@ -1,435 +0,0 @@ -package co.blocke.scalajack -package json.collections - -import co.blocke.scala_reflection._ -import scala.math._ -import TestUtil._ -import munit._ -import munit.internal.console -import co.blocke.scalajack.json.JSON -import scala.collection.immutable._ -import scala.jdk.CollectionConverters._ -import scala.language.implicitConversions -import java.util.Optional -import json.JsonMatcher - - -class Options() extends FunSuite: - - val sj = co.blocke.scalajack.ScalaJack() - - test("Option of primitive (naked)") { - describe("--------------------------------------------\n: Option (Scala) & Optional (Java) Tests :\n--------------------------------------------", Console.BLUE) - describe("+++ Scala Options") - - val inst: Option[BigInt] = Some(BigInt(5)) - val js = sj.render(inst) - assertEquals("5".asInstanceOf[JSON],js) - assertEquals(inst, sj.read[Option[BigInt]](js)) - } - - test("Option of primitive (in class)") { - val inst = OptionBigInt(Some(BigInt(5))) - val js = sj.render(inst) - assertEquals("""{"o":5}""".asInstanceOf[JSON],js) - assertEquals(inst, sj.read[OptionBigInt](js)) - } - - test("Option of List") { - val inst: Option[List[Int]] = Some(List(1, 2, 3)) - val js = sj.render(inst) - assertEquals("""[1,2,3]""".asInstanceOf[JSON],js) - assertEquals(inst, sj.read[Option[List[Int]]](js)) - - val inst2: Option[List[Int]] = None - val js2 = sj.render(inst2) - assertEquals("".asInstanceOf[JSON],js2) - // Can't read nothing into something - - val inst3: Map[Option[List[Int]], Int] = - Map(None -> 2, Some(List(1, 2, 3)) -> 1) - val js3 = sj.render(inst3) - assertEquals("""{"[1,2,3]":1}""".asInstanceOf[JSON],js3) - assertEquals(Map(Some(List(1, 2, 3)) -> 1), sj.read[Map[Option[List[Int]], Int]](js3)) - } - - test("Option of Map") { - val inst: Option[Map[String, Boolean]] = - Some(Map("hey" -> true, "you" -> false)) - val js = sj.render(inst) - assertEquals("""{"hey":true,"you":false}""".asInstanceOf[JSON],js) - assertEquals(inst, sj.read[Option[Map[String, Boolean]]](js)) - - val inst2: Option[Map[String, Boolean]] = None - val js2 = sj.render(inst2) - assertEquals("".asInstanceOf[JSON],js2) - // Can't read nothing into something - - val inst3: Map[Option[Map[String, Boolean]], Int] = - Map(None -> 2, Some(Map("hey" -> true, "you" -> false)) -> 1) - val js3 = sj.render(inst3) - assertEquals("""{"{\"hey\":true,\"you\":false}":1}""".asInstanceOf[JSON],js3) - assertEquals(Map(Some(Map("hey" -> true, "you" -> false)) -> 1), sj.read[Map[Option[Map[String, Boolean]], Int]](js3)) - } - - test("Option of Tuple") { - val inst: Option[(String, Boolean)] = Some(("a", true)) - val js = sj.render(inst) - assertEquals("""["a",true]""".asInstanceOf[JSON],js) - assertEquals(inst, sj.read[Option[(String, Boolean)]](js)) - - val inst2: Option[(String, Boolean)] = None - val js2 = sj.render(inst2) - assertEquals("".asInstanceOf[JSON],js2) - // Can't read nothing into something - - val inst3: Map[Option[(String, Boolean)], Int] = - Map(None -> 2, Some(("a", true)) -> 1) - val js3 = sj.render(inst3) - assertEquals("""{"[\"a\",true]":1}""".asInstanceOf[JSON],js3) - assertEquals(Map(Some(("a", true)) -> 1), sj.read[Map[Option[(String, Boolean)], Int]](js3)) - } - - test("Option of Case Class") { - val inst: Option[SomeClass] = Some(SomeClass("Mike", 2)) - val js = sj.render(inst) - assertEquals("""{"name":"Mike","age":2}""".asInstanceOf[JSON],js) - assertEquals(inst,sj.read[Option[SomeClass]](js)) - - val inst2: Option[SomeClass] = None - val js2 = sj.render(inst2) - assertEquals("".asInstanceOf[JSON],js2) - // Can't read nothing into something - - val inst3: Map[Option[SomeClass], Int] = Map(None -> 2, Some(SomeClass("Mike", 2)) -> 1) - val js3 = sj.render(inst3) - assertEquals("""{"{\"name\":\"Mike\",\"age\":2}":1}""".asInstanceOf[JSON],js3) - assertEquals(Map(Some(SomeClass("Mike", 2)) -> 1),sj.read[Map[Option[SomeClass], Int]](js3)) - } - - test("Option of Parameterized Class") { - val inst: Option[AThing[Int, String]] = Some(AThing("wow", 5)) - val js = sj.render(inst) - assertEquals("""{"a":"wow","b":5}""".asInstanceOf[JSON],js) - assertEquals(inst, sj.read[Option[AThing[Int, String]]](js)) - - val inst2: Option[AThing[Int, String]] = None - val js2 = sj.render(inst2) - assertEquals("".asInstanceOf[JSON],js2) - - val inst3: Map[Option[AThing[Int, String]], Int] = Map(None -> 2, Some(AThing("wow", 5)) -> 1) - val js3 = sj.render(inst3) - assertEquals("""{"{\"a\":\"wow\",\"b\":5}":1}""".asInstanceOf[JSON],js3) - assertEquals(Map(Some(AThing("wow", 5)) -> 1), sj.read[Map[Option[AThing[Int, String]], Int]](js3)) - } - - test("Option of Trait") { - val inst: Option[Person] = Some(SomeClass("Mike", 2)) - val js = sj.render(inst) - assertEquals( - """{"_hint":"co.blocke.scalajack.json.collections.SomeClass","name":"Mike","age":2}""".asInstanceOf[JSON],js) - assertEquals(inst,sj.read[Option[Person]](js)) - - val inst2: Option[Person] = None - val js2 = sj.render(inst2) - assertEquals("".asInstanceOf[JSON],js2) - // Can't read nothing into something - - val inst3: Map[Option[Person], Int] = - Map(None -> 2, Some(SomeClass("Mike", 2)) -> 1) - val js3 = sj.render(inst3) - assertEquals( - """{"{\"_hint\":\"co.blocke.scalajack.json.collections.SomeClass\",\"name\":\"Mike\",\"age\":2}":1}""".asInstanceOf[JSON],js3) - assertEquals(Map(Some(SomeClass("Mike", 2)) -> 1),sj.read[Map[Option[Person], Int]](js3)) - } - - test("Option of Parameterized Trait") { - val inst: Option[Thing[String, Int]] = Some(AThing("wow", 5)) - val js = sj.render(inst) - assertEquals( - """{"_hint":"co.blocke.scalajack.json.collections.AThing","a":"wow","b":5}""".asInstanceOf[JSON],js) - assertEquals(inst,sj.read[Option[Thing[String, Int]]](js)) - - val inst2: Option[Thing[String, Int]] = None - val js2 = sj.render(inst2) - assertEquals("".asInstanceOf[JSON],js2) - - val inst3: Map[Option[Thing[String, Int]], Int] = - Map(None -> 2, Some(AThing("wow", 5)) -> 1) - val js3 = sj.render(inst3) - assertEquals( - """{"{\"_hint\":\"co.blocke.scalajack.json.collections.AThing\",\"a\":\"wow\",\"b\":5}":1}""".asInstanceOf[JSON],js3) - assertEquals(Map(Some(AThing("wow", 5)) -> 1),sj.read[Map[Option[Thing[String, Int]], Int]](js3)) - } - - //------------------- None tests - test("Option is None (in class)") { - describe("+++ Scala None/null tests") - - val inst = OptionClass("Mike", None) - val js = sj.render(inst) - assertEquals("""{"name":"Mike"}""".asInstanceOf[JSON],js) - assertEquals(inst, sj.read[OptionClass](js)) - } - - test("Option is None (in List)") { - val inst: List[Option[Int]] = List(Some(1), None, Some(2)) - val js = sj.render(inst) - assertEquals("""[1,2]""".asInstanceOf[JSON],js) - assertEquals(List(Some(1), Some(2)).asInstanceOf[List[Option[Int]]], sj.read[List[Option[Int]]](js)) // None gets erased here - } - - test("Option is None (value in Map)") { - val inst: Map[Int, Option[String]] = Map(1 -> Some("one"), 2 -> None, 3 -> Some("three")) - val js = sj.render(inst) - assertEquals("""{"1":"one","3":"three"}""".asInstanceOf[JSON],js) - assertEquals(Map(1 -> Some("one"), 3 -> Some("three")).asInstanceOf[Map[Int,Option[String]]], sj.read[Map[Int, Option[String]]](js)) // None gets erased here - } - - test("Option is None (key in Map)") { - val inst: Map[Option[String], Int] = - Map(Some("one") -> 1, None -> 2, Some("three") -> 3) - val js = sj.render(inst) - assertEquals("""{"one":1,"three":3}""".asInstanceOf[JSON],js) - assertEquals(Map(Some("one") -> 1, Some("three") -> 3),sj.read[Map[Option[String], Int]](js)) - } - - test("Option is None (in Tuple)") { - val inst = List( - OptionTuple(1, (true, Some("ok"), 2)), - OptionTuple(5, (false, None, 3)) - ) - val js = sj.render(inst) - assertEquals( - """[{"foo":1,"t":[true,"ok",2]},{"foo":5,"t":[false,null,3]}]""".asInstanceOf[JSON],js) - assertEquals(inst,sj.read[List[OptionTuple]](js)) - } - - test("Reading null into optional (naked)") { - val js = "null".asInstanceOf[JSON] - assertEquals(null.asInstanceOf[Option[Int]], sj.read[Option[Int]](js)) - } - - test("Reading null into optional class field") { - val js = """{"name":"Mike","age":null}""".asInstanceOf[JSON] - assertEquals(OptionClass("Mike", null), sj.read[OptionClass](js)) - } - - test("Reading null into optional List item") { - val js = """[1,null,2]""".asInstanceOf[JSON] - assertEquals(List(Some(1), null, Some(2)).asInstanceOf[List[Option[Int]]],sj.read[List[Option[Int]]](js)) - } - - test("Reading null into optional Map item") { - val js = """{"1":"one","2":null,"3":"three"}""".asInstanceOf[JSON] - assertEquals(Map(1 -> Some("one"), 2 -> None, 3 -> Some("three")),sj.read[Map[Int, Option[String]]](js)) - } - - test("Reading null into optional Tuple item") { - val js = """[{"foo":1,"t":[true,"ok",2]},{"foo":5,"t":[false,null,3]}]""".asInstanceOf[JSON] - assertEquals( - List( - OptionTuple(1, (true, Some("ok"), 2)), - OptionTuple(5, (false, None, 3)) - ),sj.read[List[OptionTuple]](js)) - } - - //-------------- Java - - test("Option of primitive (naked)") { - describe("++++ Java Optional") - - val inst: Optional[BigInt] = Optional.of(BigInt(5)) - val js = sj.render(inst) - assertEquals("5".asInstanceOf[JSON],js) - assertEquals(inst, sj.read[Optional[BigInt]](js)) - } - - test("Java class with Optional - empty JSON") { - val js = """{}""".asInstanceOf[JSON] - val inst = sj.read[Maybe](js) - assertEquals(inst.getOne, Optional.empty) - assertEquals(inst.getTwo, Optional.of("stuff")) - assertEquals( """{"two":"stuff"}""".asInstanceOf[JSON], sj.render(inst)) - - val msg = """Class co.blocke.scalajack.Maybe2 missing required fields: one - |{} - |-^""".stripMargin - interceptMessage[co.blocke.scalajack.ScalaJackError](msg){ - sj.read[Maybe2](js) - } - } - - test("Java class with Optional - some args specified") { - val js = """{"one":"meh"}""".asInstanceOf[JSON] - val inst = sj.read[Maybe](js) - assertEquals(inst.getOne, Optional.of("meh")) - assertEquals(inst.getTwo, Optional.of("stuff")) - assert(JsonMatcher.jsonMatches("""{"one":"meh","two":"stuff"}""".asInstanceOf[JSON], sj.render(inst))) - } - - test("Optional of primitive (in class)") { - val inst = OptionalBigInt(Optional.of(BigInt(5))) - val js = sj.render(inst) - assertEquals("""{"o":5}""".asInstanceOf[JSON],js) - assertEquals(inst, sj.read[OptionalBigInt](js)) - } - - test("Optional of List") { - val inst: Optional[List[Int]] = Optional.of(List(1, 2, 3)) - val js = sj.render(inst) - assertEquals("""[1,2,3]""".asInstanceOf[JSON],js) - assertEquals(inst, sj.read[Optional[List[Int]]](js)) - - val inst2: Optional[List[Int]] = Optional.empty[List[Int]] - val js2 = sj.render(inst2) - assertEquals("".asInstanceOf[JSON],js2) - // Can't read nothing into something - - val inst3 = Map(Optional.empty[List[Int]] -> 2, Optional.of(List(1, 2, 3)) -> 1) - val js3 = sj.render(inst3) - assertEquals("""{"[1,2,3]":1}""".asInstanceOf[JSON],js3) - assert(Map(Optional.of(List(1, 2, 3)) -> 1) == sj.read[Map[Optional[List[Int]], Int]](js3)) - } - - test("Optional of Map") { - val inst: Optional[Map[String, Boolean]] = - Optional.of(Map("hey" -> true, "you" -> false)) - val js = sj.render(inst) - assertEquals("""{"hey":true,"you":false}""".asInstanceOf[JSON],js) - assertEquals(inst, sj.read[Optional[Map[String, Boolean]]](js)) - - val inst2: Option[Map[String, Boolean]] = None - val js2 = sj.render(inst2) - assertEquals("".asInstanceOf[JSON],js2) - // Can't read nothing into something - - val inst3: Map[Option[Map[String, Boolean]], Int] = - Map(None -> 2, Some(Map("hey" -> true, "you" -> false)) -> 1) - val js3 = sj.render(inst3) - assertEquals("""{"{\"hey\":true,\"you\":false}":1}""".asInstanceOf[JSON],js3) - assertEquals(Map(Some(Map("hey" -> true, "you" -> false)) -> 1), sj.read[Map[Option[Map[String, Boolean]], Int]](js3)) - } - - test("Optional of Tuple") { - val inst: Optional[(String, Boolean)] = Optional.of(("a", true)) - val js = sj.render(inst) - assertEquals("""["a",true]""".asInstanceOf[JSON],js) - assertEquals(inst, sj.read[Optional[(String, Boolean)]](js)) - - val inst2: Optional[(String, Boolean)] = Optional.empty[(String,Boolean)] - val js2 = sj.render(inst2) - assertEquals("".asInstanceOf[JSON],js2) - // Can't read nothing into something - - val inst3: Map[Optional[(String, Boolean)], Int] = - Map(Optional.empty[(String, Boolean)] -> 2, Optional.of(("a", true)) -> 1) - val js3 = sj.render(inst3) - assertEquals("""{"[\"a\",true]":1}""".asInstanceOf[JSON],js3) - assertEquals(Map(Optional.of(("a", true)) -> 1), sj.read[Map[Optional[(String, Boolean)], Int]](js3)) - } - - test("Optional of Case Class") { - val inst: Optional[SomeClass] = Optional.of(SomeClass("Mike", 2)) - val js = sj.render(inst) - assertEquals("""{"name":"Mike","age":2}""".asInstanceOf[JSON],js) - assertEquals(inst,sj.read[Optional[SomeClass]](js)) - - val inst2: Optional[SomeClass] = Optional.empty[SomeClass] - val js2 = sj.render(inst2) - assertEquals("".asInstanceOf[JSON],js2) - // Can't read nothing into something - - val inst3: Map[Optional[SomeClass], Int] = Map(Optional.empty[SomeClass] -> 2, Optional.of(SomeClass("Mike", 2)) -> 1) - val js3 = sj.render(inst3) - assertEquals("""{"{\"name\":\"Mike\",\"age\":2}":1}""".asInstanceOf[JSON],js3) - assertEquals(Map(Optional.of(SomeClass("Mike", 2)) -> 1),sj.read[Map[Optional[SomeClass], Int]](js3)) - } - - test("Optional of Parameterized Class") { - val inst: Optional[AThing[Int, String]] = Optional.of(AThing("wow", 5)) - val js = sj.render(inst) - assertEquals("""{"a":"wow","b":5}""".asInstanceOf[JSON],js) - assertEquals(inst, sj.read[Optional[AThing[Int, String]]](js)) - - val inst2: Optional[AThing[Int, String]] = Optional.empty[AThing[Int, String]] - val js2 = sj.render(inst2) - assertEquals("".asInstanceOf[JSON],js2) - - val inst3: Map[Optional[AThing[Int, String]], Int] = - Map(Optional.empty[AThing[Int, String]] -> 2, Optional.of(AThing("wow", 5)) -> 1) - val js3 = sj.render(inst3) - assertEquals("""{"{\"a\":\"wow\",\"b\":5}":1}""".asInstanceOf[JSON],js3) - assertEquals(Map(Optional.of(AThing("wow", 5)) -> 1), sj.read[Map[Optional[AThing[Int, String]], Int]](js3)) - } - - test("Optional is None (in class)") { - describe("+++ Java Empty/null tests") - - val inst = OptionalClass("Mike", Optional.empty[Int]) - val js = sj.render(inst) - assertEquals("""{"name":"Mike"}""".asInstanceOf[JSON],js) - assertEquals(inst, sj.read[OptionalClass](js)) - } - - test("Optional is None (in List)") { - val inst: List[Optional[Int]] = List(Optional.of(1), Optional.empty[Int], Optional.of(2)) - val js = sj.render(inst) - assertEquals("""[1,2]""".asInstanceOf[JSON],js) - assertEquals(List(Optional.of(1), Optional.of(2)).asInstanceOf[List[Optional[Int]]], sj.read[List[Optional[Int]]](js)) // None gets erased here - } - - test("Optional is None (value in Map)") { - val inst: Map[Int, Optional[String]] = Map(1 -> Optional.of("one"), 2 -> Optional.empty[String], 3 -> Optional.of("three")) - val js = sj.render(inst) - assertEquals("""{"1":"one","3":"three"}""".asInstanceOf[JSON],js) - assert(Map(1 -> Optional.of("one"), 3 -> Optional.of("three")).asInstanceOf[Map[Int,Option[String]]] == sj.read[Map[Int, Optional[String]]](js)) // None gets erased here - } - - test("Optional is None (key in Map)") { - val inst: Map[Optional[String], Int] = - Map(Optional.of("one") -> 1, Optional.empty[String] -> 2, Optional.of("three") -> 3) - val js = sj.render(inst) - assertEquals("""{"one":1,"three":3}""".asInstanceOf[JSON],js) - assertEquals(Map(Optional.of("one") -> 1, Optional.of("three") -> 3),sj.read[Map[Optional[String], Int]](js)) - } - - test("Optional is Empty (in Tuple)") { - val inst = List( - OptionalTuple(1, (true, Optional.of("ok"), 2)), - OptionalTuple(5, (false, Optional.empty[String], 3)) - ) - val js = sj.render(inst) - assertEquals( - """[{"foo":1,"t":[true,"ok",2]},{"foo":5,"t":[false,null,3]}]""".asInstanceOf[JSON],js) - assertEquals(inst,sj.read[List[OptionalTuple]](js)) - } - - test("Reading null into Optional (naked)") { - val js = "null".asInstanceOf[JSON] - assertEquals(null.asInstanceOf[Optional[Int]], sj.read[Optional[Int]](js)) - } - - test("Reading null into Optional class field") { - val js = """{"name":"Mike","age":null}""".asInstanceOf[JSON] - assertEquals(OptionalClass("Mike", null), sj.read[OptionalClass](js)) - } - - test("Reading null into Optional List item") { - val js = """[1,null,2]""".asInstanceOf[JSON] - assertEquals(List(Optional.of(1), null, Optional.of(2)).asInstanceOf[List[Optional[Int]]],sj.read[List[Optional[Int]]](js)) - } - - test("Reading null into Optional Map item") { - val js = """{"1":"one","2":null,"3":"three"}""".asInstanceOf[JSON] - assertEquals(Map(1 -> Optional.of("one"), 2 -> Optional.empty[String], 3 -> Optional.of("three")),sj.read[Map[Int, Optional[String]]](js)) - } - - test("Reading null into Optional Tuple item") { - val js = """[{"foo":1,"t":[true,"ok",2]},{"foo":5,"t":[false,null,3]}]""".asInstanceOf[JSON] - assertEquals( - List( - OptionalTuple(1, (true, Optional.of("ok"), 2)), - OptionalTuple(5, (false, Optional.empty[String], 3)) - ),sj.read[List[OptionalTuple]](js)) - } diff --git a/core/src/test/scala/co.blocke.scalajack/json/collections/Seqs.scala b/core/src/test/scala/co.blocke.scalajack/json/collections/Seqs.scala deleted file mode 100644 index 68e1fb6d..00000000 --- a/core/src/test/scala/co.blocke.scalajack/json/collections/Seqs.scala +++ /dev/null @@ -1,272 +0,0 @@ -package co.blocke.scalajack -package json.collections - -import co.blocke.scala_reflection._ -import scala.math._ -import java.util.UUID -import TestUtil._ -import munit._ -import munit.internal.console -import co.blocke.scalajack.json.JSON -import scala.collection.immutable._ -import scala.jdk.CollectionConverters._ -import scala.language.implicitConversions._ - -class Seqs() extends FunSuite: - - val sj = co.blocke.scalajack.ScalaJack() - - test("BigDecimal must work") { - describe("---------------------\n: Scala Seq Tests :\n---------------------", Console.BLUE) - describe("+++ Primitive Types +++") - - val inst = BigDecimalSeq(null, Seq(BigDecimal(123.456),BigDecimal(78.91))) - val js = sj.render(inst) - assertEquals( - """{"a1":null,"a2":[123.456,78.91]}""".asInstanceOf[JSON], - js - ) - assertEquals(inst, sj.read[BigDecimalSeq](js)) - } - - test("BigInt must work") { - val inst = BigIntSeq(null, List(BigInt(123),BigInt(78))) - val js = sj.render(inst) - assertEquals( - """{"a1":null,"a2":[123,78]}""".asInstanceOf[JSON], - js - ) - assertEquals(inst, sj.read[BigIntSeq](js)) - } - - test("Boolean must work") { - val inst = BooleanSeq(null, scala.collection.mutable.ListBuffer(true,false)) - val js = sj.render(inst) - assertEquals( - """{"a1":null,"a2":[true,false]}""".asInstanceOf[JSON], - js - ) - assertEquals(inst, sj.read[BooleanSeq](js)) - } - - test("Byte must work") { - val inst = ByteSeq(null, List(123.toByte,200.toByte)) - val js = sj.render(inst) - assertEquals( - """{"a1":null,"a2":[123,-56]}""".asInstanceOf[JSON], - js - ) - assertEquals(inst, sj.read[ByteSeq](js)) - } - - test("Char must work") { - val inst = CharSeq(null, Queue('a','b','c')) - val js = sj.render(inst) - assertEquals( - """{"a1":null,"a2":["a","b","c"]}""".asInstanceOf[JSON], - js - ) - assertEquals(inst, sj.read[CharSeq](js)) - } - - test("Double must work") { - val inst = DoubleSeq(null, scala.collection.mutable.ArrayBuffer(12.34,56.78)) - val js = sj.render(inst) - assertEquals( - """{"a1":null,"a2":[12.34,56.78]}""".asInstanceOf[JSON], - js - ) - assertEquals(inst, sj.read[DoubleSeq](js)) - } - - test("Float must work") { - val inst = FloatSeq(null, LinearSeq(12.34F,56.78F)) - val js = sj.render(inst) - assertEquals( - """{"a1":null,"a2":[12.34,56.78]}""".asInstanceOf[JSON], - js - ) - assertEquals(inst, sj.read[FloatSeq](js)) - } - - test("Int must work") { - val inst = IntSeq(null, scala.collection.mutable.IndexedSeq(1,2,3)) - val js = sj.render(inst) - assertEquals( - """{"a1":null,"a2":[1,2,3]}""".asInstanceOf[JSON], - js - ) - assertEquals(inst, sj.read[IntSeq](js)) - } - - test("Long must work") { - val inst = LongSeq(null, List(1L,2L,3L)) - val js = sj.render(inst) - assertEquals( - """{"a1":null,"a2":[1,2,3]}""".asInstanceOf[JSON], - js - ) - assertEquals(inst, sj.read[LongSeq](js)) - } - - test("Short must work") { - val inst = ShortSeq(null, List(1.toShort,2.toShort,3.toShort)) - val js = sj.render(inst) - assertEquals( - """{"a1":null,"a2":[1,2,3]}""".asInstanceOf[JSON], - js - ) - assertEquals(inst, sj.read[ShortSeq](js)) - } - - test("String must work") { - val inst = StringSeq(null, List("a","b","c")) - val js = sj.render(inst) - assertEquals( - """{"a1":null,"a2":["a","b","c"]}""".asInstanceOf[JSON], - js - ) - assertEquals(inst, sj.read[StringSeq](js)) - } - - test("Lists must work") { - describe("+++ Collection Types +++") - val inst = SeqSeq(List( List(1,2,3), List(4,5,6) )) - val js = sj.render(inst) - assertEquals( - """{"a1":[[1,2,3],[4,5,6]]}""".asInstanceOf[JSON], - js - ) - assertEquals(inst, sj.read[SeqSeq](js)) - } - - test("Maps must work") { - val inst = MapSeq(List( Map("a"->1,"b"->2), Map("c"->3,"d"->4) )) - val js = sj.render(inst) - assertEquals( - """{"a1":[{"a":1,"b":2},{"c":3,"d":4}]}""".asInstanceOf[JSON], - js - ) - assertEquals(inst, sj.read[MapSeq](js)) - } - - test("Classes must work") { - describe("+++ Class Types +++") - val inst = ClassSeq(List(IntArr(null,Array(1,2)), IntArr(null,Array(1,2)))) - val js = sj.render(inst) - assertEquals( - """{"a1":[{"a1":null,"a2":[1,2]},{"a1":null,"a2":[1,2]}]}""".asInstanceOf[JSON], - js - ) - val i2 = sj.read[ClassSeq](js) - assertEquals(i2.a1.toList(0).a1,null) - assert(inst.a1.toList(0).a2.sameElements(i2.a1.toList(0).a2)) - assertEquals(i2.a1.toList(1).a1,null) - assert(inst.a1.toList(1).a2.sameElements(i2.a1.toList(1).a2)) - } - - test("Multidimensional arrays must work") { - describe("+++ Complex Types +++") - val inst = MultiSeq(null, Seq(Seq( Seq(1L,2L), Seq(3L,4L) ), Seq(Seq(5L,6L), Seq(7L,8L)) ), - Seq(Seq(BigInt(12),BigInt(13)), Seq(BigInt(14),BigInt(15)))) - val js = sj.render(inst) - assertEquals( - """{"a0":null,"a1":[[[1,2],[3,4]],[[5,6],[7,8]]],"a2":[[12,13],[14,15]]}""".asInstanceOf[JSON], - js - ) - assertEquals(inst, sj.read[MultiSeq](js)) - } - - - test("BigDecimal (LinkedList) must work") { - describe("--------------------\n: Java Seq Tests :\n--------------------", Console.BLUE) - describe("+++ Primitive Types (Mix of List/Set/Queue +++") - - val inst = JBigDecimalSeq(null, new java.util.LinkedList(List(BigDecimal(123.456),BigDecimal(78.91)).asJava)) - val js = sj.render(inst) - assertEquals( - """{"a1":null,"a2":[123.456,78.91]}""".asInstanceOf[JSON], - js - ) - val readIn = sj.read[JBigDecimalSeq](js) - assertEquals(inst.a1, readIn.a1) - assertEquals(inst.a2, readIn.a2) - } - - test("BigInt (Vector) must work") { - val inst = JBigIntSeq(null, new java.util.Vector(List(BigInt(123),BigInt(78)).asJava)) - val js = sj.render(inst) - assertEquals( - """{"a1":null,"a2":[123,78]}""".asInstanceOf[JSON], - js - ) - val readIn = sj.read[JBigIntSeq](js) - assertEquals(inst.a1, readIn.a1) - assertEquals(inst.a2, readIn.a2) - } - - test("Boolean (ArrayList) must work") { - val inst = JBooleanSeq(null, new java.util.ArrayList(List(true,false).asJava)) - val js = sj.render(inst) - assertEquals( - """{"a1":null,"a2":[true,false]}""".asInstanceOf[JSON], - js - ) - val readIn = sj.read[JBooleanSeq](js) - assertEquals(inst.a1, readIn.a1) - assertEquals(inst.a2, readIn.a2) - } - - test("Char (PriorityQueue) must work") { - val inst = JCharSeq(null, new java.util.PriorityQueue(List('a','b','c').asJava)) - val js = sj.render(inst) - assertEquals( - """{"a1":null,"a2":["a","b","c"]}""".asInstanceOf[JSON], - js - ) - val readIn = sj.read[JCharSeq](js) - assertEquals(inst.a1, readIn.a1) - assertEquals(inst.a2.toString, readIn.a2.toString) - } - //case class JIntSeq( a1: java.util.Stack[Int], a2: java.util.Stack[Int] ) - - test("Int (Stack) must work") { - val stack = new java.util.Stack[Int]() - stack.push(1) - stack.push(2) - stack.push(3) - val inst = JIntSeq(null, stack) - val js = sj.render(inst) - assertEquals( - """{"a1":null,"a2":[1,2,3]}""".asInstanceOf[JSON], - js - ) - val readIn = sj.read[JIntSeq](js) - assertEquals(inst.a1, readIn.a1) - assertEquals(inst.a2, readIn.a2) - } - - test("Maps must work") { - val inst = JMapSeq(java.util.ArrayList( List(Map("a"->1,"b"->2), Map("c"->3,"d"->4)).asJava )) - val js = sj.render(inst) - assertEquals( - """{"a1":[{"a":1,"b":2},{"c":3,"d":4}]}""".asInstanceOf[JSON], - js - ) - assertEquals(inst.a1.asScala, sj.read[JMapSeq](js).a1.asScala) - } - - test("Classes must work") { - describe("+++ Class Types +++") - val inst = JClassSeq( new java.util.LinkedList(List(IntArr(null,Array(1,2)), IntArr(null,Array(1,2))).asJava)) - val js = sj.render(inst) - assertEquals( - """{"a1":[{"a1":null,"a2":[1,2]},{"a1":null,"a2":[1,2]}]}""".asInstanceOf[JSON], - js - ) - val i2 = sj.read[JClassSeq](js) - assertEquals(i2.a1.get(0).a1, null) - assert(inst.a1.get(0).a2.sameElements(i2.a1.get(0).a2)) - assertEquals(i2.a1.get(1).a1, null) - assert(inst.a1.get(1).a2.sameElements(i2.a1.get(1).a2)) - } diff --git a/core/src/test/scala/co.blocke.scalajack/json/collections/Tuples.scala b/core/src/test/scala/co.blocke.scalajack/json/collections/Tuples.scala deleted file mode 100644 index 45fa0e11..00000000 --- a/core/src/test/scala/co.blocke.scalajack/json/collections/Tuples.scala +++ /dev/null @@ -1,52 +0,0 @@ -package co.blocke.scalajack -package json.collections - -import TestUtil._ -import munit._ -import munit.internal.console -import co.blocke.scalajack.json.JSON -import scala.collection.immutable._ - -class Tuples() extends FunSuite: - - val sj = co.blocke.scalajack.ScalaJack() - - test("null tuples work") { - describe("-----------------\n: Tuple Tests :\n-----------------", Console.BLUE) - val jsNull = "null".asInstanceOf[JSON] - assert(sj.read[(Int, Boolean)](jsNull) == null) - assert(sj.render[(Int,Boolean)](null) == jsNull) - } - - test("missing start bracken") { - val js = """12,5""".asInstanceOf[JSON] - val msg = - """Expected start of tuple here - |12,5 - |^""".stripMargin - interceptMessage[co.blocke.scalajack.ScalaJackError](msg){ - sj.read[(Int, Int)](js) - } - } - - test("missing comma") { - val js = """[12""".asInstanceOf[JSON] - val msg = - """Expected comma here - |[12 - |---^""".stripMargin - interceptMessage[co.blocke.scalajack.ScalaJackError](msg){ - sj.read[(Int, Int)](js) - } - } - - test("no closing bracket") { - val js = """[12,5""".asInstanceOf[JSON] - val msg = - """Expected end of tuple here - |[12,5 - |-----^""".stripMargin - interceptMessage[co.blocke.scalajack.ScalaJackError](msg){ - sj.read[(Int, Int)](js) - } - } diff --git a/core/src/test/scala/co.blocke.scalajack/json/custom/CustomAdapter.scala b/core/src/test/scala/co.blocke.scalajack/json/custom/CustomAdapter.scala deleted file mode 100644 index c13c0426..00000000 --- a/core/src/test/scala/co.blocke.scalajack/json/custom/CustomAdapter.scala +++ /dev/null @@ -1,20 +0,0 @@ -package co.blocke.scalajack -package json.custom - -import co.blocke.scala_reflection._ -import TestUtil._ -import munit._ -import munit.internal.console -import co.blocke.scalajack.json.JSON - -class CustomAdapter() extends FunSuite: - - test("Overrides type adapter for specific (given) type") { - describe("--------------------------\n: Custom Adapter Tests :\n--------------------------", Console.BLUE) - - val sj = co.blocke.scalajack.ScalaJack().withAdapters(PhoneAdapter) - val inst = Person("Bartholomew", "5555555555".asInstanceOf[Phone]) - val js = sj.render(inst) - assertEquals("""{"name":"Bartholomew","phone":"555-555-5555"}""".asInstanceOf[JSON],js) - assertEquals(inst, sj.read[Person](js)) - } \ No newline at end of file diff --git a/core/src/test/scala/co.blocke.scalajack/json/custom/CustomTypeHints.scala b/core/src/test/scala/co.blocke.scalajack/json/custom/CustomTypeHints.scala deleted file mode 100644 index f897ddd6..00000000 --- a/core/src/test/scala/co.blocke.scalajack/json/custom/CustomTypeHints.scala +++ /dev/null @@ -1,134 +0,0 @@ -package co.blocke.scalajack -package json.custom - -import co.blocke.scala_reflection._ -import TestUtil._ -import munit._ -import munit.internal.console -import co.blocke.scalajack.json.JSON -import co.blocke.scalajack.model._ - -class CustomTypeHints() extends FunSuite: - - test("Override default trait/polymorphic type hint") { - describe("-----------------------------\n: Custom Type Hints Tests :\n-----------------------------", Console.BLUE) - describe("+++ Positive Tests +++") - - val sj = co.blocke.scalajack.ScalaJack().withDefaultHint("which") - val inst: Address = USAddress("123 Main", "New York", "NY", "39822") - val js = sj.render(inst) - assertEquals( - """{"which":"co.blocke.scalajack.json.custom.USAddress","street":"123 Main","city":"New York","state":"NY","postalCode":"39822"}""".asInstanceOf[JSON], - js) - assertEquals(inst, sj.read[Address](js)) - } - - test("Override type-specific trait/polymorphic type hint") { - val sj = co.blocke.scalajack.ScalaJack().withHints(RType.of[Address] -> "addr_kind") - val inst: Demographic = - USDemographic(50, USAddress("123 Main", "New York", "NY", "39822")) - val js = sj.render(inst) - assertEquals( - """{"_hint":"co.blocke.scalajack.json.custom.USDemographic","age":50,"address":{"addr_kind":"co.blocke.scalajack.json.custom.USAddress","street":"123 Main","city":"New York","state":"NY","postalCode":"39822"}}""".asInstanceOf[JSON], - js) - assertEquals(inst, sj.read[Demographic](js)) - } - - test( - "Use ClassNameHintModifier to modify trait/polymorphic type hint value" - ) { - val prependHintMod = ClassNameHintModifier( - (hint: String) => "co.blocke.scalajack.json.custom." + hint, - (cname: String) => cname.split('.').last - ) - val sj = - co.blocke.scalajack.ScalaJack().withHintModifiers((RType.of[Address], prependHintMod)) - val inst: Demographic = - USDemographic(50, USAddress("123 Main", "New York", "NY", "39822")) - val js = sj.render(inst) - assertEquals( - """{"_hint":"co.blocke.scalajack.json.custom.USDemographic","age":50,"address":{"_hint":"USAddress","street":"123 Main","city":"New York","state":"NY","postalCode":"39822"}}""".asInstanceOf[JSON], - js) - assertEquals(inst, sj.read[Demographic](js)) - } - - test( - "Use StringMatchHintModifier to modify trait/polymorphic type hint value" - ) { - val strMatchHintMod = - StringMatchHintModifier(Map("US" -> classOf[USAddress].getName)) - val sj = - co.blocke.scalajack.ScalaJack().withHintModifiers((RType.of[Address], strMatchHintMod)) - val inst: Demographic = - USDemographic(50, USAddress("123 Main", "New York", "NY", "39822")) - val js = sj.render(inst) - assertEquals( - """{"_hint":"co.blocke.scalajack.json.custom.USDemographic","age":50,"address":{"_hint":"US","street":"123 Main","city":"New York","state":"NY","postalCode":"39822"}}""".asInstanceOf[JSON], - js) - assertEquals(inst, sj.read[Demographic](js)) - } - - test("Use unspecified type hint") { - describe("--- Negative Tests ---") - val sj = co.blocke.scalajack.ScalaJack().withDefaultHint("which") - val js = - """{"bogus":"co.blocke.scalajack.json.custom.USAddress","street":"123 Main","city":"New York","state":"NY","postalCode":"39822"}""".asInstanceOf[JSON] - val msg = """Type hint 'which' not found - |...ity":"New York","state":"NY","postalCode":"39822"} - |----------------------------------------------------^""".stripMargin - interceptMessage[co.blocke.scalajack.ScalaJackError](msg){ - sj.read[Address](js) - } - } - - test("Hint value after modification doesn't resolve to known class name") { - val prependHintMod = ClassNameHintModifier( - (hint: String) => "co.blocke.scalajack.bogus." + hint, - (cname: String) => cname.split('.').last - ) - val sj = - co.blocke.scalajack.ScalaJack().withHintModifiers((RType.of[Address], prependHintMod)) - val js = - """{"_hint":"co.blocke.scalajack.json.custom.USDemographic","age":50,"address":{"_hint":"USAddress","street":"123 Main","city":"New York","state":"NY","postalCode":"39822"}}""".asInstanceOf[JSON] - val msg = - """Couldn't marshal class for USAddress - |...mographic","age":50,"address":{"_hint":"USAddress","street":"123 Main","city... - |----------------------------------------------------^""".stripMargin - interceptMessage[java.lang.ClassNotFoundException]("""co.blocke.scalajack.bogus.USAddress"""){ - sj.read[Demographic](js) - } - } - - test( - "Unknown string given as type hint value (no cooresponding match to class in mapping)" - ) { - val strMatchHintMod = - StringMatchHintModifier(Map("US" -> "co.blocke.scalajack.json.custom.USDemographic")) - val sj = - co.blocke.scalajack.ScalaJack().withHintModifiers((RType.of[Address], strMatchHintMod)) - val js = - """{"_hint":"co.blocke.scalajack.json.custom.USDemographic","age":50,"address":{"_hint":"Bogus","street":"123 Main","city":"New York","state":"NY","postalCode":"39822"}}""".asInstanceOf[JSON] - val msg = - """Couldn't marshal class for Bogus - |...USDemographic","age":50,"address":{"_hint":"Bogus","street":"123 Main","city... - |----------------------------------------------------^""".stripMargin - interceptMessage[co.blocke.scalajack.ScalaJackError](msg){ - sj.read[Demographic](js) - } - } - - test("Serialize object with unmapped hint class") { - val strMatchHintMod = - StringMatchHintModifier(Map("US" -> "co.blocke.scalajack.json.custom.USDemographic")) - val sj = - co.blocke.scalajack.ScalaJack().withHintModifiers((RType.of[Address], strMatchHintMod)) - val inst: Demographic = USDemographic( - 50, - CanadaAddress("123 Main", "New York", "NY", "39822") - ) - val msg = - """key not found: co.blocke.scalajack.json.custom.CanadaAddress""" - interceptMessage[java.util.NoSuchElementException](msg){ - sj.render(inst) - } - } diff --git a/core/src/test/scala/co.blocke.scalajack/json/custom/Model.scala b/core/src/test/scala/co.blocke.scalajack/json/custom/Model.scala deleted file mode 100644 index adfd8d61..00000000 --- a/core/src/test/scala/co.blocke.scalajack/json/custom/Model.scala +++ /dev/null @@ -1,59 +0,0 @@ -package co.blocke.scalajack -package json.custom - -import co.blocke.scala_reflection.impl.Clazzes._ -import co.blocke.scala_reflection.info.AliasInfo -import co.blocke.scala_reflection._ -import co.blocke.scalajack.model._ - -opaque type Phone >: Null = String - -import scala.collection.mutable - -// Override just Phone -object PhoneAdapter extends TypeAdapterFactory with TypeAdapter[Phone]: - def matches(concrete: RType): Boolean = - concrete match { - case a: AliasInfo if a.name == "Phone" => true - case _ => false - } - def makeTypeAdapter(concrete: RType)(implicit taCache: TypeAdapterCache): TypeAdapter[Phone] = this - val info = RType.of[Phone] - override def isStringish: Boolean = true - - def read(parser: Parser): Phone = - parser.expectString() match { - case null => null.asInstanceOf[Phone] - case s: String => s.replaceAll("-", "").asInstanceOf[Phone] - } - - def write[WIRE]( - t: Phone, - writer: Writer[WIRE], - out: mutable.Builder[WIRE, WIRE]): Unit = t match { - case null => writer.writeNull(out) - case _ => - writer.writeString( - "%s-%s-%s".format(t.toString.substring(0, 3), t.toString.substring(3, 6), t.toString.substring(6)), - out - ) - } - -case class Person(name: String, phone: Phone) - -trait Address { val postalCode: String } -case class USAddress( - street: String, - city: String, - state: String, - postalCode: String) - extends Address -case class CanadaAddress( - street: String, - city: String, - province: String, - postalCode: String) - extends Address -case class DefaultAddress(postalCode: String) extends Address -trait Demographic { val address: Address } -case class USDemographic(age: Int, address: Address) extends Demographic diff --git a/core/src/test/scala/co.blocke.scalajack/json/custom/ParseOrElse.scala b/core/src/test/scala/co.blocke.scalajack/json/custom/ParseOrElse.scala deleted file mode 100644 index 10a31998..00000000 --- a/core/src/test/scala/co.blocke.scalajack/json/custom/ParseOrElse.scala +++ /dev/null @@ -1,19 +0,0 @@ -package co.blocke.scalajack -package json.custom - -import co.blocke.scala_reflection._ -import TestUtil._ -import munit._ -import munit.internal.console -import co.blocke.scalajack.json.JSON - -class ParseOrElse() extends FunSuite: - - test("Provide a default object if the object specified in the type hint is unknown") { - describe("-----------------------\n: ParseOrElse Tests :\n-----------------------", Console.BLUE) - - val sj = co.blocke.scalajack.ScalaJack().parseOrElse( (RType.of[Address] -> RType.of[DefaultAddress]) ) - val js = - """{"_hint":"co.blocke.scalajack.json.custom.USDemographic","age":50,"address":{"_hint":"co.blocke.scalajack.json.custom.UnknownAddress","street":"123 Main","city":"New York","state":"NY","postalCode":"39822"}}""".asInstanceOf[JSON] - assert(USDemographic(50, DefaultAddress("39822")) == sj.read[Demographic](js)) - } \ No newline at end of file diff --git a/core/src/test/scala/co.blocke.scalajack/json/mapkeys/ClassPrimKeys.scala b/core/src/test/scala/co.blocke.scalajack/json/mapkeys/ClassPrimKeys.scala deleted file mode 100644 index c83cb248..00000000 --- a/core/src/test/scala/co.blocke.scalajack/json/mapkeys/ClassPrimKeys.scala +++ /dev/null @@ -1,319 +0,0 @@ -package co.blocke.scalajack -package json.mapkeys - -import co.blocke.scala_reflection._ -import TestUtil._ -import munit._ -import munit.internal.console -import co.blocke.scalajack.json.JSON -import java.util.UUID -import co.blocke.scalajack.model._ - -class ClassPrimKeys() extends FunSuite: - - val sj = co.blocke.scalajack.ScalaJack() - - test("Simple (flat) class as key") { - describe( - "-------------------------\n: Class Map Key Tests :\n-------------------------", Console.BLUE - ) - describe("+++ Positive Tests +++") - - val a = SimpleClass("Larry", 32, isOk = true, "golf") - val b = SimpleClass("Mike", 27, isOk = false, 125) - val inst = SampleSimple(Map(a -> b)) - val js = sj.render(inst) - assertEquals( - """{"m":{"{\"name\":\"Larry\",\"age\":32,\"isOk\":true,\"favorite\":\"golf\"}":{"name":"Mike","age":27,"isOk":false,"favorite":125}}}""".asInstanceOf[JSON],js) - assertEquals(inst, sj.read[SampleSimple](js)) - } - - test("Complex class (having members that are classes) as key") { - val a = SimpleClass("Larry", 32, isOk = true, "golf") - val b = SimpleClass("Mike", 27, isOk = false, 125) - val c1 = ComplexClass( - UUID.fromString("1e6c2b31-4dfe-4bf6-a0a0-882caaff0e9c"), - a, - allDone = true - ) - val c2 = ComplexClass( - UUID.fromString("1e6c2b31-4dfe-4bf6-a0a0-882caaff0e9d"), - b, - allDone = false - ) - val inst = SampleComplex(Map(c1 -> c2)) - val js = sj.render(inst) - assertEquals( - """{"m":{"{\"id\":\"1e6c2b31-4dfe-4bf6-a0a0-882caaff0e9c\",\"simple\":{\"name\":\"Larry\",\"age\":32,\"isOk\":true,\"favorite\":\"golf\"},\"allDone\":true}":{"id":"1e6c2b31-4dfe-4bf6-a0a0-882caaff0e9d","simple":{"name":"Mike","age":27,"isOk":false,"favorite":125},"allDone":false}}}""".asInstanceOf[JSON],js) - assertEquals(inst, sj.read[SampleComplex](js)) - } - - test("Simple (flat) trait as key") { - val a: Pet = FishPet("Flipper", Food.Veggies, 74.33) - val b: Pet = DogPet("Fido", Food.Meat, 3) - val inst = SamplePet(Map(a -> b)) - val js = sj.render(inst) - assertEquals( - """{"m":{"{\"_hint\":\"co.blocke.scalajack.json.mapkeys.FishPet\",\"name\":\"Flipper\",\"food\":\"Veggies\",\"waterTemp\":74.33}":{"_hint":"co.blocke.scalajack.json.mapkeys.DogPet","name":"Fido","food":"Meat","numLegs":3}}}""".asInstanceOf[JSON],js) - assertEquals(inst, sj.read[SamplePet](js)) - } - - test("Complex trait (having members that are traits) as key") { - val a: Pet = FishPet("Flipper", Food.Veggies, 74.33) - val b: Pet = DogPet("Fido", Food.Meat, 3) - val c: Pet = CompoundPet("Legion", Food.Pellets, b) - val inst = SamplePet(Map(c -> a)) - val js = sj.render(inst) - assertEquals( - """{"m":{"{\"_hint\":\"co.blocke.scalajack.json.mapkeys.CompoundPet\",\"name\":\"Legion\",\"food\":\"Pellets\",\"pet\":{\"_hint\":\"co.blocke.scalajack.json.mapkeys.DogPet\",\"name\":\"Fido\",\"food\":\"Meat\",\"numLegs\":3}}":{"_hint":"co.blocke.scalajack.json.mapkeys.FishPet","name":"Flipper","food":"Veggies","waterTemp":74.33}}}""".asInstanceOf[JSON],js) - assertEquals(inst, sj.read[SamplePet](js)) - } - - test( - "Complex trait (having members that are traits) as key where trait member is null" - ) { - val a: Pet = FishPet("Flipper", Food.Veggies, 74.33) - val b: Pet = null.asInstanceOf[Pet] // DogPet("Fido", Food.Meat, 3) - val c: Pet = CompoundPet("Legion", Food.Pellets, b) - val inst = SamplePet(Map(c -> a)) - val js = sj.render(inst) - assertEquals( - """{"m":{"{\"_hint\":\"co.blocke.scalajack.json.mapkeys.CompoundPet\",\"name\":\"Legion\",\"food\":\"Pellets\",\"pet\":null}":{"_hint":"co.blocke.scalajack.json.mapkeys.FishPet","name":"Flipper","food":"Veggies","waterTemp":74.33}}}""".asInstanceOf[JSON],js) - assertEquals(inst, sj.read[SamplePet](js)) - } - - test("Class having collections as members") { - val a = PolyClass(Map("a" -> 1, "b" -> 2), List("one", "two")) - val b = PolyClass(Map("x" -> 9, "y" -> 10), List("aye", "you")) - val inst = SamplePolyClass(Map(a -> b)) - val js = sj.render(inst) - assertEquals( - """{"m":{"{\"lookup\":{\"a\":1,\"b\":2},\"favs\":[\"one\",\"two\"]}":{"lookup":{"x":9,"y":10},"favs":["aye","you"]}}}""".asInstanceOf[JSON],js) - assertEquals(inst, sj.read[SamplePolyClass](js)) - } - - test("Class having collections as members (empty collections") { - val a = PolyClass(Map.empty[String, Int], List.empty[String]) - val b = PolyClass(Map.empty[String, Int], List.empty[String]) - val inst = SamplePolyClass(Map(a -> b)) - val js = sj.render(inst) - assertEquals( - """{"m":{"{\"lookup\":{},\"favs\":[]}":{"lookup":{},"favs":[]}}}""".asInstanceOf[JSON],js) - assertEquals(inst, sj.read[SamplePolyClass](js)) - } - - test("Custom trait hint field and value for key trait") { - val petHintMod = StringMatchHintModifier( - Map("BreathsWater" -> "co.blocke.scalajack.json.mapkeys.FishPet", "BreathsAir" -> "co.blocke.scalajack.json.mapkeys.DogPet") - ) - val sj2 = co.blocke.scalajack.ScalaJack() - .withHints((RType.of[Pet] -> "kind")) - .withHintModifiers((RType.of[Pet] -> petHintMod)) - - val a: Pet = FishPet("Flipper", Food.Veggies, 74.33) - val b: Pet = DogPet("Fido", Food.Meat, 3) - val inst = SamplePet(Map(a -> b)) - val js = sj2.render(inst) - assertEquals( - """{"m":{"{\"kind\":\"BreathsWater\",\"name\":\"Flipper\",\"food\":\"Veggies\",\"waterTemp\":74.33}":{"kind":"BreathsAir","name":"Fido","food":"Meat","numLegs":3}}}""".asInstanceOf[JSON],js) - assertEquals(inst, sj2.read[SamplePet](js)) - } - - test("Custom trait hint field and value for key member's trait") { - val petHintMod = StringMatchHintModifier( - Map("BreathsWater" -> "co.blocke.scalajack.json.mapkeys.FishPet", "BreathsAir" -> "co.blocke.scalajack.json.mapkeys.DogPet") - ) - val sj2 = co.blocke.scalajack.ScalaJack() - .withHints((RType.of[Pet] -> "kind")) - .withHintModifiers((RType.of[Pet] -> petHintMod)) - - val a: PetHolder = - ShinyPetHolder("123 Main", FishPet("Flipper", Food.Veggies, 74.33)) - val b: PetHolder = - ShinyPetHolder("210 North", DogPet("Fido", Food.Meat, 3)) - val inst = SampleShiny(Map(a -> b)) - val js = sj2.render(inst) - assertEquals( - """{"m":{"{\"_hint\":\"co.blocke.scalajack.json.mapkeys.ShinyPetHolder\",\"address\":\"123 Main\",\"pet\":{\"kind\":\"BreathsWater\",\"name\":\"Flipper\",\"food\":\"Veggies\",\"waterTemp\":74.33}}":{"_hint":"co.blocke.scalajack.json.mapkeys.ShinyPetHolder","address":"210 North","pet":{"kind":"BreathsAir","name":"Fido","food":"Meat","numLegs":3}}}}""".asInstanceOf[JSON],js) - assertEquals(inst, sj2.read[SampleShiny](js)) - } - - test("Key value is a class having a noncanoncial map") { - val a = NCKey(Map(0 -> false, 1 -> true), "truth") - val b = NCKey(Map(1 -> false, 0 -> true), "lie") - val inst = SampleNCKey(Map(a -> b)) - val js = sj.render(inst) - assertEquals( - """{"m":{"{\"nc\":{\"0\":false,\"1\":true},\"name\":\"truth\"}":{"nc":{"1":false,"0":true},"name":"lie"}}}""".asInstanceOf[JSON],js) - assertEquals(inst, sj.read[SampleNCKey](js)) - } - - test("Extra/unneeded fields in key's JSON harmlessly ignored") { - val js = - """{"m":{"{\"name\":\"Larry\",\"bogus\":false,\"age\":32,\"isOk\":true,\"favorite\":\"golf\"}":{"name":"Mike","age":27,"isOk":false,"favorite":125}}}""".asInstanceOf[JSON] - val a = SimpleClass("Larry", 32, isOk = true, "golf") - val b = SimpleClass("Mike", 27, isOk = false, 125) - val inst = SampleSimple(Map(a -> b)) - assertEquals(inst, sj.read[SampleSimple](js)) - } - - test("Bad (invalid--missing field) class json as map key") { - describe("--- Negative Tests ---") - - val js = - """{"m":{"{\"age\":32,\"favorite\":\"golf\"}":{"name":"Mike","age":27,"isOk":false,"favorite":125}}}""".asInstanceOf[JSON] - val msg = """Class co.blocke.scalajack.json.mapkeys.SimpleClass missing required fields: isOk, name - |{"age":32,"favorite":"golf"} - |---------------------------^""".stripMargin - interceptMessage[co.blocke.scalajack.ScalaJackError](msg){ - sj.read[SampleSimple](js) - } - } - - test("Bad class json as map key (valid json, but wrong for given class)") { - val js = - """{"m":{"{\"name\":\"Larry\",\"age\":32,\"favorite\":\"golf\"}":{"name":"Mike","age":27,"isOk":false,"favorite":125}}}""".asInstanceOf[JSON] - val msg = """Class co.blocke.scalajack.json.mapkeys.SimpleClass missing required fields: isOk - |{"name":"Larry","age":32,"favorite":"golf"} - |------------------------------------------^""".stripMargin - interceptMessage[co.blocke.scalajack.ScalaJackError](msg){ - sj.read[SampleSimple](js) - } - } - - test("Bad json for member class") { - val js = - """{"m":{"{\"id\":\"1e6c2b31-4dfe-4bf6-a0a0-882caaff0e9c\",\"simple\":{\"name\":\"Larry\",\"isOk\":true,\"favorite\":\"golf\"},\"allDone\":true}":{"id":"1e6c2b31-4dfe-4bf6-a0a0-882caaff0e9d","simple":{"name":"Mike","age":27,"isOk":false,"favorite":125},"allDone":false}}}""".asInstanceOf[JSON] - val msg = - """Class co.blocke.scalajack.json.mapkeys.SimpleClass missing required fields: age - |...le":{"name":"Larry","isOk":true,"favorite":"golf"},"allDone":true} - |----------------------------------------------------^""".stripMargin - interceptMessage[co.blocke.scalajack.ScalaJackError](msg){ - sj.read[SampleComplex](js) - } - } - - test("Bad (invalid) trait json as map key") { - val js = - """{"m":{"{\"_hint\":\"co.blocke.scalajack.json.mapkeys.FishPet\":\"Flipper\" \"food\":\"Veggies\",\"waterTemp\":74.33}":{"_hint":"co.blocke.scalajack.mapkeys.DogPet","name":"Fido","food":"Meat","numLegs":3}}}""".asInstanceOf[JSON] - val msg = - """Expected comma here - |..._hint":"co.blocke.scalajack.json.mapkeys.FishPet":"Flipper" "food":"Veggies"... - |----------------------------------------------------^""".stripMargin - interceptMessage[co.blocke.scalajack.ScalaJackError](msg){ - sj.read[SamplePet](js) - } - } - - test("Bad trait json (missing hint) as map key") { - val js = - """{"m":{"{\"name\":\"Flipper\",\"food\":\"Veggies\",\"waterTemp\":74.33}":{"_hint":"co.blocke.scalajack.json.mapkeys.DogPet","name":"Fido","food":"Meat","numLegs":3}}}""".asInstanceOf[JSON] - val msg = """Type hint '_hint' not found - |...ame":"Flipper","food":"Veggies","waterTemp":74.33} - |----------------------------------------------------^""".stripMargin - interceptMessage[co.blocke.scalajack.ScalaJackError](msg){ - sj.read[SamplePet](js) - } - } - - test("Bad trait json (hint to unknown class) as map key") { - val js = - """{"m":{"{\"_hint\":\"co.blocke.scalajack.json.mapkeys.Bogus\",\"name\":\"Flipper\",\"food\":\"Veggies\",\"waterTemp\":74.33}":{"_hint":"co.blocke.scalajack.json.mapkeys.DogPet","name":"Fido","food":"Meat","numLegs":3}}}""".asInstanceOf[JSON] - // val msg = - // """Couldn't marshal class for co.blocke.scalajack.json.mapkeys.Bogus - // |{"_hint":"co.blocke.scalajack.json.mapkeys.Bogus","name":"Flipper","food":"Ve... - // |------------------------------------------------^""".stripMargin - interceptMessage[java.lang.ClassNotFoundException]("co.blocke.scalajack.json.mapkeys.Bogus"){ - sj.read[SamplePet](js) - } - } - - test("Bad (invalid) trait json for member trait") { - val js = - """{"m":{"{\"_hint\":\"co.blocke.scalajack.json.mapkeys.CompoundPet\",\"name\":\"Legion\",\"food\":\"Pellets\",\"pet\":{\"_hint\":\"co.blocke.scalajack.json.mapkeys.DogPet\",\"name\":\"Fido\",\"food\":\"Meat\",\"numLegs\":3}}":{"_hint":"co.blocke.scalajack.json.mapkeys.FishPet","name"}"Flipper","food":"Veggies","waterTemp":74.33}}}""".asInstanceOf[JSON] - val msg = - """Expected colon here - |..."co.blocke.scalajack.json.mapkeys.FishPet","name"}"Flipper","food":"Veggies"... - |----------------------------------------------------^""".stripMargin - interceptMessage[co.blocke.scalajack.ScalaJackError](msg){ - sj.read[SamplePet](js) - } - } - - test("Bad trait json (missing hint) for member trait") { - val js = - """{"m":{"{\"_hint\":\"co.blocke.scalajack.json.mapkeys.CompoundPet\",\"name\":\"Legion\",\"food\":\"Pellets\",\"pet\":{\"_hint\":\"co.blocke.scalajack.json.mapkeys.DogPet\",\"name\":\"Fido\",\"food\":\"Meat\",\"numLegs\":3}}":{"name":"Flipper","food":"Veggies","waterTemp":74.33}}}""".asInstanceOf[JSON] - val msg = """Type hint '_hint' not found - |...ame":"Flipper","food":"Veggies","waterTemp":74.33}}} - |----------------------------------------------------^""".stripMargin - interceptMessage[co.blocke.scalajack.ScalaJackError](msg){ - sj.read[SamplePet](js) - } - } - - test("Bad trait json (hint to unknown classs) for member trait") { - val js = - """{"m":{"{\"_hint\":\"co.blocke.scalajack.json.mapkeys.CompoundPet\",\"name\":\"Legion\",\"food\":\"Pellets\",\"pet\":{\"_hint\":\"co.blocke.scalajack.json.mapkeys.DogPet\",\"name\":\"Fido\",\"food\":\"Meat\",\"numLegs\":3}}":{"_hint":"co.blocke.scalajack.json.mapkeys.Bogus","name":"Flipper","food":"Veggies","waterTemp":74.33}}}""".asInstanceOf[JSON] - interceptMessage[java.lang.ClassNotFoundException]("co.blocke.scalajack.json.mapkeys.Bogus"){ - sj.read[SamplePet](js) - } - } - - test("Bad collection value in map key class having collections") { - val js = - """{"m":{"{\"lookup\":{\"a\":true,\"b\":2},\"favs\":[\"one\",\"two\"]}":{"lookup":{"x":9,"y":10},"favs":["aye","you"]}}}""".asInstanceOf[JSON] - val msg = """Expected a Number here - |{"lookup":{"a":true,"b":2},"favs":["one","two"]} - |---------------^""".stripMargin - interceptMessage[co.blocke.scalajack.ScalaJackError](msg){ - sj.read[SamplePolyClass](js) - } - } - - test("Bad custom hint value for map key trait (sort instead of kind") { - val petHintMod = StringMatchHintModifier( - Map("BreathsWater" -> "co.blocke.scalajack.json.mapkeys.FishPet", "BreathsAir" -> "co.blocke.scalajack.json.mapkeys.DogPet") - ) - val sj2 = co.blocke.scalajack.ScalaJack() - .withHints(RType.of[Pet] -> "kind") - .withHintModifiers(RType.of[Pet] -> petHintMod) - val js = - """{"m":{"{\"_hint\":\"co.blocke.scalajack.json.mapkeys.ShinyPetHolder\",\"address\":\"123 Main\",\"pet\":{\"sort\":\"BreathsWater\",\"name\":\"Flipper\",\"food\":\"Veggies\",\"waterTemp\":74.33}}":{"_hint":"co.blocke.scalajack.json.mapkeys.ShinyPetHolder","address":"210 North","pet":{"kind":"BreathsAir","name":"Fido","food":"Meat","numLegs":3}}}}""".asInstanceOf[JSON] - val msg = """Type hint 'kind' not found - |...ame":"Flipper","food":"Veggies","waterTemp":74.33}} - |----------------------------------------------------^""".stripMargin - interceptMessage[co.blocke.scalajack.ScalaJackError](msg){ - sj2.read[SampleShiny](js) - } - } - - test("Bad class for hint in Map key (trait)") { - val petHintMod = StringMatchHintModifier( - Map("BreathsWater" -> "co.blocke.scalajack.json.mapkeys.FishPet", "BreathsAir" -> "co.blocke.scalajack.json.mapkeys.DogPet") - ) - val sj2 = co.blocke.scalajack.ScalaJack() - .withHints(RType.of[Pet] -> "kind") - .withHintModifiers(RType.of[Pet] -> petHintMod) - val js = - """{"m":{"{\"_hint\":\"co.blocke.scalajack.mapkeys.Bogus\",\"address\":\"123 Main\",\"pet\":{\"kind\":\"BreathsLava\",\"name\":\"Flipper\",\"food\":\"Veggies\",\"waterTemp\":74.33}}":{"_hint":"co.blocke.scalajack.json.mapkeys.ShinyPetHolder","address":"210 North","pet":{"kind":"BreathsAir","name":"Fido","food":"Meat","numLegs":3}}}}""".asInstanceOf[JSON] - interceptMessage[java.lang.ClassNotFoundException]("co.blocke.scalajack.mapkeys.Bogus"){ - sj2.read[SampleShiny](js) - } - } - - test("Bad custom hint value for Map key member's trait") { - val petHintMod = StringMatchHintModifier( - Map("BreathsWater" -> "co.blocke.scalajack.json.mapkeys.FishPet", "BreathsAir" -> "co.blocke.scalajack.json.mapkeys.DogPet") - ) - val sj2 = co.blocke.scalajack.ScalaJack() - .withHints(RType.of[Pet] -> "kind") - .withHintModifiers((RType.of[Pet] -> petHintMod)) - val js = - """{"m":{"{\"_hint\":\"co.blocke.scalajack.json.mapkeys.ShinyPetHolder\",\"address\":\"123 Main\",\"pet\":{\"kind\":\"BreathsLava\",\"name\":\"Flipper\",\"food\":\"Veggies\",\"waterTemp\":74.33}}":{"_hint":"co.blocke.scalajack.json.mapkeys.ShinyPetHolder","address":"210 North","pet":{"kind":"BreathsAir","name":"Fido","food":"Meat","numLegs":3}}}}""".asInstanceOf[JSON] - val msg = """Couldn't marshal class for BreathsLava - |...","address":"123 Main","pet":{"kind":"BreathsLava","name":"Flipper","food":"... - |----------------------------------------------------^""".stripMargin - interceptMessage[co.blocke.scalajack.ScalaJackError](msg){ - sj2.read[SampleShiny](js) - } - } \ No newline at end of file diff --git a/core/src/test/scala/co.blocke.scalajack/json/mapkeys/JavaPrimKeys.scala b/core/src/test/scala/co.blocke.scalajack/json/mapkeys/JavaPrimKeys.scala deleted file mode 100644 index b28689e5..00000000 --- a/core/src/test/scala/co.blocke.scalajack/json/mapkeys/JavaPrimKeys.scala +++ /dev/null @@ -1,500 +0,0 @@ -package co.blocke.scalajack -package json.mapkeys - -import co.blocke.scala_reflection._ -import TestUtil._ -import munit._ -import munit.internal.console -import co.blocke.scalajack.json.JSON -import java.lang.{ - Boolean => JBoolean, - Byte => JByte, - Character => JChar, - Double => JDouble, - Float => JFloat, - Integer => JInteger, - Long => JLong, - Short => JShort -} -import java.math.{ BigDecimal => JBigDecimal, BigInteger => JBigInteger } -import java.time._ - -import co.blocke.scalajack.json.JsonMatcher - -class JavaPrimKeys() extends FunSuite: - - val sj = co.blocke.scalajack.ScalaJack() - - test("With BigDecimal Key") { - describe( - "----------------------------------\n: Java Primitive Map Key Tests :\n----------------------------------", Console.BLUE - ) - describe("+++ Positive Tests +++") - describe("Simple DelimSpec:") - - val inst = SampleJBigDecimal( - Map( - new JBigDecimal("123.456") -> new JBigDecimal("1"), - new JBigDecimal("789.123") -> new JBigDecimal("2") - ) - ) - val js = sj.render(inst) - assertEquals("""{"m":{"123.456":1,"789.123":2}}""".asInstanceOf[JSON], js) - assertEquals(inst, sj.read[SampleJBigDecimal](js)) - } - - test("With BigInteger Key") { - val inst = SampleJBigInteger( - Map( - new JBigInteger("123") -> new JBigInteger("1"), - new JBigInteger("789") -> new JBigInteger("2") - ) - ) - val js = sj.render(inst) - assertEquals("""{"m":{"123":1,"789":2}}""".asInstanceOf[JSON], js) - assertEquals(inst, sj.read[SampleJBigInteger](js)) - } - - test("With Boolean Key") { - val inst = SampleJBoolean( - Map( - true.asInstanceOf[JBoolean] -> false.asInstanceOf[JBoolean], - false.asInstanceOf[JBoolean] -> true.asInstanceOf[JBoolean] - ) - ) - val js = sj.render(inst) - assertEquals("""{"m":{"true":false,"false":true}}""".asInstanceOf[JSON], js) - assertEquals(inst, sj.read[SampleJBoolean](js)) - } - - test("With Byte Key") { - val inst = SampleJByte( - Map( - 16.toByte.asInstanceOf[JByte] -> 2.toByte.asInstanceOf[JByte], - 48.toByte.asInstanceOf[JByte] -> 9.toByte.asInstanceOf[JByte] - ) - ) - val js = sj.render(inst) - assertEquals("""{"m":{"16":2,"48":9}}""".asInstanceOf[JSON], js) - assertEquals(inst, sj.read[SampleJByte](js)) - } - - test("With Char Key") { - val inst = SampleJChar( - Map( - 'a'.asInstanceOf[JChar] -> 'A'.asInstanceOf[JChar], - 'z'.asInstanceOf[JChar] -> 'Z'.asInstanceOf[JChar] - ) - ) - val js = sj.render(inst) - assertEquals("""{"m":{"a":"A","z":"Z"}}""".asInstanceOf[JSON], js) - assertEquals(inst, sj.read[SampleJChar](js)) - } - - test("With Double Key") { - val inst = SampleJDouble( - Map( - 12.34.asInstanceOf[JDouble] -> 56.78.asInstanceOf[JDouble], - 90.12.asInstanceOf[JDouble] -> 34.56.asInstanceOf[JDouble] - ) - ) - val js = sj.render(inst) - assertEquals("""{"m":{"12.34":56.78,"90.12":34.56}}""".asInstanceOf[JSON], js) - assertEquals(inst, sj.read[SampleJDouble](js)) - } - - test("With Float Key") { - val inst = SampleJFloat( - Map( - 12.34F.asInstanceOf[JFloat] -> 56.78F.asInstanceOf[JFloat], - 90.12F.asInstanceOf[JFloat] -> 34.56F.asInstanceOf[JFloat] - ) - ) - val js = sj.render(inst) - assertEquals("""{"m":{"12.34":56.78,"90.12":34.56}}""".asInstanceOf[JSON], js) - assertEquals(inst, sj.read[SampleJFloat](js)) - } - - test("With Integer Key") { - val inst = SampleJInteger( - Map( - 12.asInstanceOf[JInteger] -> 56.asInstanceOf[JInteger], - 90.asInstanceOf[JInteger] -> 34.asInstanceOf[JInteger] - ) - ) - val js = sj.render(inst) - assertEquals("""{"m":{"12":56,"90":34}}""".asInstanceOf[JSON], js) - assertEquals(inst, sj.read[SampleJInteger](js)) - } - - test("With Long Key") { - val inst = SampleJLong( - Map( - 12L.asInstanceOf[JLong] -> 56L.asInstanceOf[JLong], - 90L.asInstanceOf[JLong] -> 34L.asInstanceOf[JLong] - ) - ) - val js = sj.render(inst) - assertEquals("""{"m":{"12":56,"90":34}}""".asInstanceOf[JSON], js) - assertEquals(inst, sj.read[SampleJLong](js)) - } - - - test("With Number Key") { - val inst = SampleJNumber( - Map( - JByte.valueOf("-128") -> JByte.valueOf("127"), - JShort.valueOf("-32768") -> JShort.valueOf("32767"), - JInteger.valueOf("-2147483648") -> JInteger.valueOf("2147483647"), - JLong.valueOf("-9223372036854775808") -> JLong - .valueOf("9223372036854755807"), - JByte.valueOf("0") -> new JBigInteger("9923372036854755810"), - JFloat.valueOf("3.4e-038") -> JFloat.valueOf("3.4e+038"), - JDouble.valueOf("1.7e-308") -> JDouble.valueOf("1.7e+308"), - new JBigDecimal("1.8e+308") -> JFloat.valueOf("0.0") - ) - ) - val result = SampleJNumber( - Map( - JByte.valueOf("0") -> new JBigDecimal("9923372036854755810"), - JInteger.valueOf("-2147483648") -> JInteger.valueOf("2147483647"), - JLong.valueOf("-9223372036854775808") -> JLong - .valueOf("9223372036854755807"), - JByte.valueOf("-128") -> JByte.valueOf("127"), - JFloat.valueOf("3.4E-38") -> JFloat.valueOf("3.4E38"), - JShort.valueOf("-32768") -> JShort.valueOf("32767"), - new JBigDecimal("1.8E+308") -> JByte.valueOf("0"), - JDouble.valueOf("1.7E-308") -> JDouble.valueOf("1.7E308") - ) - ) - val js = sj.render(inst) - assert( JsonMatcher.jsonMatches(js, - """{"m":{"0":9923372036854755810,"-2147483648":2147483647,"-9223372036854775808":9223372036854755807,"-128":127,"3.4E-38":3.4E38,"-32768":32767,"1.8E+308":0.0,"1.7E-308":1.7E308}}""".asInstanceOf[JSON])) - val read = sj.read[SampleJNumber](js) - assertEquals(result, read) - } - - test("With Short Key") { - val inst = SampleJShort( - Map( - 12.toShort.asInstanceOf[JShort] -> 56.toShort - .asInstanceOf[JShort], - 90.toShort.asInstanceOf[JShort] -> 34.toShort.asInstanceOf[JShort] - ) - ) - val js = sj.render(inst) - assertEquals("""{"m":{"12":56,"90":34}}""".asInstanceOf[JSON], js) - assertEquals(inst, sj.read[SampleJShort](js)) - } - - - test("With Duration Key") { - describe("Time DelimSpec:") - - val inst = - SampleDuration(Map(Duration.ZERO -> Duration.parse("P2DT3H4M"))) - val js = sj.render(inst) - assertEquals("""{"m":{"PT0S":"PT51H4M"}}""".asInstanceOf[JSON], js) - assertEquals(inst, sj.read[SampleDuration](js)) - } - - test("With Instant Key") { - val inst = SampleInstant( - Map( - Instant.EPOCH -> Instant.MAX, - Instant.MIN -> Instant.parse("2007-12-03T10:15:30.00Z") - ) - ) - val js = sj.render(inst) - assertEquals( - """{"m":{"1970-01-01T00:00:00Z":"+1000000000-12-31T23:59:59.999999999Z","-1000000000-01-01T00:00:00Z":"2007-12-03T10:15:30Z"}}""".asInstanceOf[JSON], js) - assertEquals(inst, sj.read[SampleInstant](js)) - } - - test("With LocalDateTime Key") { - val inst = SampleLocalDateTime( - Map( - LocalDateTime.MAX -> LocalDateTime.MIN, - LocalDateTime.parse("2007-12-03T10:15:30") -> null - ) - ) - val js = sj.render(inst) - assertEquals( - """{"m":{"+999999999-12-31T23:59:59.999999999":"-999999999-01-01T00:00:00","2007-12-03T10:15:30":null}}""".asInstanceOf[JSON], js) - assertEquals(inst, sj.read[SampleLocalDateTime](js)) - } - - test("With LocalDate Key") { - val inst = SampleLocalDate( - Map( - LocalDate.MAX -> LocalDate.MIN, - LocalDate.parse("2007-12-03") -> null - ) - ) - val js = sj.render(inst) - assertEquals( - """{"m":{"+999999999-12-31":"-999999999-01-01","2007-12-03":null}}""".asInstanceOf[JSON], js) - assertEquals(inst, sj.read[SampleLocalDate](js)) - } - - test("With LocalTime Key") { - val inst = SampleLocalTime( - Map( - LocalTime.MAX -> LocalTime.MIN, - LocalTime.MIDNIGHT -> LocalTime.NOON, - LocalTime.parse("10:15:30") -> null - ) - ) - val js = sj.render(inst) - assertEquals( - """{"m":{"23:59:59.999999999":"00:00:00","00:00:00":"12:00:00","10:15:30":null}}""".asInstanceOf[JSON], js) - assertEquals(inst, sj.read[SampleLocalTime](js)) - } - - test("With OffsetDateTime Key") { - val inst = SampleOffsetDateTime( - Map( - OffsetDateTime.MAX -> OffsetDateTime.MIN, - OffsetDateTime.parse("2007-12-03T10:15:30+01:00") -> null - ) - ) - val js = sj.render(inst) - assertEquals( - """{"m":{"+999999999-12-31T23:59:59.999999999-18:00":"-999999999-01-01T00:00:00+18:00","2007-12-03T10:15:30+01:00":null}}""".asInstanceOf[JSON], js) - assertEquals(inst, sj.read[SampleOffsetDateTime](js)) - } - - test("With OffsetTime Key") { - val inst = SampleOffsetTime( - Map( - OffsetTime.MAX -> OffsetTime.MIN, - OffsetTime.parse("10:15:30+01:00") -> null - ) - ) - val js = sj.render(inst) - assertEquals( - """{"m":{"23:59:59.999999999-18:00":"00:00:00+18:00","10:15:30+01:00":null}}""".asInstanceOf[JSON], js) - assertEquals(inst, sj.read[SampleOffsetTime](js)) - } - - test("With Period Key") { - val inst = SamplePeriod(Map(Period.ZERO -> Period.parse("P1Y2M3D"))) - val js = sj.render(inst) - assertEquals("""{"m":{"P0D":"P1Y2M3D"}}""".asInstanceOf[JSON], js) - assertEquals(inst, sj.read[SamplePeriod](js)) - } - - test("With ZonedDateTime Key") { - val inst = SampleZonedDateTime( - Map( - ZonedDateTime - .parse("2007-12-03T10:15:30+01:00[Europe/Paris]") -> null - ) - ) - val js = sj.render(inst) - assertEquals( - """{"m":{"2007-12-03T10:15:30+01:00[Europe/Paris]":null}}""" - .asInstanceOf[JSON], js) - assertEquals(inst, sj.read[SampleZonedDateTime](js)) - } - - test("Bad BigDecimal Key") { - describe("--- Negative Tests ---") - describe("Simple DelimSpec:") - - val js = """{"m":{"fred":1,"789.123":2}}""".asInstanceOf[JSON] - val msg = """Expected a Number here - |fred - |^""".stripMargin - interceptMessage[co.blocke.scalajack.ScalaJackError](msg){ - sj.read[SampleJBigDecimal](js) - } - } - - test("Bad BigInt Key") { - val js = """{"m":{"fred":1,"789":2}}""".asInstanceOf[JSON] - val msg = """Expected a Number here - |fred - |^""".stripMargin - interceptMessage[co.blocke.scalajack.ScalaJackError](msg){ - sj.read[SampleJBigInteger](js) - } - } - - test("Bad Boolean Key") { - val js = """{"m":{"true":false,"123":true}}""".asInstanceOf[JSON] - val msg = """Expected a Boolean here - |123 - |^""".stripMargin - interceptMessage[co.blocke.scalajack.ScalaJackError](msg){ - sj.read[SampleJBoolean](js) - } - } - - test("Bad Byte Key") { - val js = """{"m":{"16":2,"4x8":9}}""".asInstanceOf[JSON] - val msg = """Expected a Number here - |4x8 - |-^""".stripMargin - interceptMessage[co.blocke.scalajack.ScalaJackError](msg){ - sj.read[SampleJByte](js) - } - } - - test("Bad Char Key") { // NOTE: This comprehensively tests for any null keyed Map - val js = """{"m":{null:"A","z":"Z"}}""".asInstanceOf[JSON] - val msg = """Map keys cannot be null - |{"m":{null:"A","z":"Z"}} - |----------^""".stripMargin - interceptMessage[co.blocke.scalajack.ScalaJackError](msg){ - sj.read[SampleJChar](js) - } - } - - test("Bad Double Key") { - val js = """{"m":{"12.34":56.78,"true":34.56}}""".asInstanceOf[JSON] - val msg = """Expected a Number here - |true - |^""".stripMargin - interceptMessage[co.blocke.scalajack.ScalaJackError](msg){ - sj.read[SampleJDouble](js) - } - } - - test("Bad Float Key") { - val js = """{"m":{"12.34":56.78,"90.12.3":34.56}}""".asInstanceOf[JSON] - interceptMessage[java.lang.NumberFormatException]("multiple points"){ - sj.read[SampleJFloat](js) - } - } - - test("Bad Int Key") { - val js = """{"m":{"12.0":56,"90":34}}""".asInstanceOf[JSON] - interceptMessage[java.lang.NumberFormatException]("For input string: \"12.0\""){ - sj.read[SampleJInteger](js) - } - } - - test("Bad Long Key") { - val js = """{"m":{"12":56,"hey":34}}""".asInstanceOf[JSON] - val msg = """Expected a Number here - |hey - |^""".stripMargin - interceptMessage[co.blocke.scalajack.ScalaJackError](msg){ - sj.read[SampleJLong](js) - } - } - - test("Bad Number Key") { - val js = - """{"m":{"flume":9923372036854755810,"-2147483648":2147483647,"-9223372036854775808":9223372036854755807,"-128":127,"3.4E-38":3.4E38,"-32768":32767,"1.8E+308":0.0,"1.7E-308":1.7E308}}""".asInstanceOf[JSON] - val msg = """Expected a Number here - |flume - |^""".stripMargin - interceptMessage[co.blocke.scalajack.ScalaJackError](msg){ - sj.read[SampleJNumber](js) - } - } - - test("Bad Duration Key") { - describe("Time DelimSpec:") - - val js = """{"m":{"PT0SXXX":"PT51H4M"}}""".asInstanceOf[JSON] - val msg = """Failed to parse Duration from input 'PT0SXXX' - |{"m":{"PT0SXXX":"PT51H4M"}} - |--------------^""".stripMargin - interceptMessage[co.blocke.scalajack.ScalaJackError](msg){ - sj.read[SampleDuration](js) - } - } - - test("Bad Instant Key") { - val js = - """{"m":{"1970-01-01T00:00:00Z":"+1000000000-12-31T23:59:59.999999999Z","bogus":"2007-12-03T10:15:30Z"}}""".asInstanceOf[JSON] - val msg = - """Failed to parse Instant from input 'bogus' - |...Z":"+1000000000-12-31T23:59:59.999999999Z","bogus":"2007-12-03T10:15:30Z"}} - |----------------------------------------------------^""".stripMargin - interceptMessage[co.blocke.scalajack.ScalaJackError](msg){ - sj.read[SampleInstant](js) - } - } - - test("Bad LocalDateTime Key") { - val js = - """{"m":{"+999999999-12-31T23:59:59.999999999":"-999999999-01-01T00:00:00","bogus":null}}""".asInstanceOf[JSON] - val msg = - """Failed to parse LocalDateTime from input 'bogus' - |...:59.999999999":"-999999999-01-01T00:00:00","bogus":null}} - |----------------------------------------------------^""".stripMargin - interceptMessage[co.blocke.scalajack.ScalaJackError](msg){ - sj.read[SampleLocalDateTime](js) - } - } - - test("Bad LocalDate Key") { - val js = """{"m":{"bogus":"-999999999-01-01","2007-12-03":null}}""".asInstanceOf[JSON] - val msg = """Failed to parse LocalDate from input 'bogus' - |{"m":{"bogus":"-999999999-01-01","2007-12-03":null}} - |------------^""".stripMargin - interceptMessage[co.blocke.scalajack.ScalaJackError](msg){ - sj.read[SampleLocalDate](js) - } - } - - test("Bad LocalTime Key") { - val js = - """{"m":{"23:59:59.999999999":"00:00:00","nada":"12:00:00","10:15:30":null}}""".asInstanceOf[JSON] - val msg = - """Failed to parse LocalTime from input 'nada' - |{"m":{"23:59:59.999999999":"00:00:00","nada":"12:00:00","10:15:30":null}} - |-------------------------------------------^""".stripMargin - interceptMessage[co.blocke.scalajack.ScalaJackError](msg){ - sj.read[SampleLocalTime](js) - } - } - - test("Bad OffsetDateTime Key") { - val js = - """{"m":{"false":"-999999999-01-01T00:00:00+18:00","2007-12-03T10:15:30+01:00":null}}""".asInstanceOf[JSON] - val msg = - """Failed to parse OffsetDateTime from input 'false' - |{"m":{"false":"-999999999-01-01T00:00:00+18:00","2007-12-03T10:15:30+01:00":n... - |------------^""".stripMargin - interceptMessage[co.blocke.scalajack.ScalaJackError](msg){ - sj.read[SampleOffsetDateTime](js) - } - } - - test("Bad OffsetTime Key") { - val js = """{"m":{"2007-12-03T10:15:30+01:00[Europe\/Bogus]":null}}""".asInstanceOf[JSON] - val msg = - """Failed to parse OffsetTime from input '2007-12-03T10:15:30+01:00[Europe/Bogus]' - |{"m":{"2007-12-03T10:15:30+01:00[Europe\/Bogus]":null}} - |-----------------------------------------------^""".stripMargin - interceptMessage[co.blocke.scalajack.ScalaJackError](msg){ - sj.read[SampleOffsetTime](js) - } - } - - test("Bad Period Key") { - val js = """{"m":{"P0D???":"P1Y2M3D"}}""".asInstanceOf[JSON] - val msg = """Failed to parse Period from input 'P0D???' - |{"m":{"P0D???":"P1Y2M3D"}} - |-------------^""".stripMargin - interceptMessage[co.blocke.scalajack.ScalaJackError](msg){ - sj.read[SamplePeriod](js) - } - } - - test("Bad ZonedDateTime Key") { - val js = - """{"m":{"FRED23:59:59.999999999-18:00":"00:00:00+18:00","10:15:30+01:00":null}}""".asInstanceOf[JSON] - val msg = - """Failed to parse ZonedDateTime from input 'FRED23:59:59.999999999-18:00' - |{"m":{"FRED23:59:59.999999999-18:00":"00:00:00+18:00","10:15:30+01:00":null}} - |-----------------------------------^""".stripMargin - interceptMessage[co.blocke.scalajack.ScalaJackError](msg){ - sj.read[SampleZonedDateTime](js) - } - } \ No newline at end of file diff --git a/core/src/test/scala/co.blocke.scalajack/json/mapkeys/ListCollKeys.scala b/core/src/test/scala/co.blocke.scalajack/json/mapkeys/ListCollKeys.scala deleted file mode 100644 index 599a3e8d..00000000 --- a/core/src/test/scala/co.blocke.scalajack/json/mapkeys/ListCollKeys.scala +++ /dev/null @@ -1,125 +0,0 @@ -package co.blocke.scalajack -package json.mapkeys - -import co.blocke.scala_reflection._ -import TestUtil._ -import munit._ -import munit.internal.console -import co.blocke.scalajack.json.JSON - -class ListCollKeys() extends FunSuite: - - val sj = co.blocke.scalajack.ScalaJack() - - test("List as key") { - describe( - "------------------------\n: List Map Key Tests :\n------------------------", Console.BLUE - ) - - val l1 = List(1, 2, 3) - val l2 = List(4, 5, 6) - val inst = Map(l1 -> l2) - val js = sj.render(inst) - assertEquals("""{"[1,2,3]":[4,5,6]}""".asInstanceOf[JSON],js) - assertEquals(inst, sj.read[Map[List[Int], List[Int]]](js)) - } - - test("List of Lists as key") { - val l1 = List(List(1, 2, 3), List(9, 8, 7)) - val l2 = List(List(4, 5, 6), List(1, 3, 5)) - val inst = Map(l1 -> l2) - val js = sj.render(inst) - assertEquals("""{"[[1,2,3],[9,8,7]]":[[4,5,6],[1,3,5]]}""".asInstanceOf[JSON],js) - assertEquals(inst, sj.read[Map[List[List[Int]], List[List[Int]]]](js)) - } - - test("List of Tuples as key") { - val l1: List[(String, String)] = List(("A", "a"), ("B", "b"), (null, "c")) - val l2: List[(String, String)] = List(("X", "x"), ("Y", "y"), (null, "z")) - val inst = Map(l1 -> l2) - val js = sj.render(inst) - assertEquals( - """{"[[\"A\",\"a\"],[\"B\",\"b\"],[null,\"c\"]]":[["X","x"],["Y","y"],[null,"z"]]}""".asInstanceOf[JSON],js) - assertEquals(inst, sj.read[Map[List[(String, String)], List[(String, String)]]](js)) - } - - test("List of Maps as key") { - val l1 = List(Map("wow" -> true), Map("ya" -> false)) - val l2 = List(Map("zing" -> false), Map("bling" -> true)) - val inst = Map(l1 -> l2) - val js = sj.render(inst) - assertEquals( - """{"[{\"wow\":true},{\"ya\":false}]":[{"zing":false},{"bling":true}]}""".asInstanceOf[JSON],js) - assertEquals(inst, sj.read[Map[List[Map[String, Boolean]], List[Map[String, Boolean]]]](js)) - } - - test("List of Case Class as key") { - val fish = FishPet("Flipper", Food.Meat, 68.9) - val inst = Map(List(fish, fish) -> List(fish, fish)) - val js = sj.render(inst) - assertEquals( - """{"[{\"name\":\"Flipper\",\"food\":\"Meat\",\"waterTemp\":68.9},{\"name\":\"Flipper\",\"food\":\"Meat\",\"waterTemp\":68.9}]":[{"name":"Flipper","food":"Meat","waterTemp":68.9},{"name":"Flipper","food":"Meat","waterTemp":68.9}]}""".asInstanceOf[JSON],js) - assertEquals(inst, sj.read[Map[List[FishPet], List[FishPet]]](js)) - } - - test("List of Trait as key") { - val fish: Pet = FishPet("Flipper", Food.Meat, 68.9) - val inst = Map(List(fish, fish) -> List(fish, fish)) - val js = sj.render(inst) - assertEquals( - """{"[{\"_hint\":\"co.blocke.scalajack.json.mapkeys.FishPet\",\"name\":\"Flipper\",\"food\":\"Meat\",\"waterTemp\":68.9},{\"_hint\":\"co.blocke.scalajack.json.mapkeys.FishPet\",\"name\":\"Flipper\",\"food\":\"Meat\",\"waterTemp\":68.9}]":[{"_hint":"co.blocke.scalajack.json.mapkeys.FishPet","name":"Flipper","food":"Meat","waterTemp":68.9},{"_hint":"co.blocke.scalajack.json.mapkeys.FishPet","name":"Flipper","food":"Meat","waterTemp":68.9}]}""".asInstanceOf[JSON],js) - assertEquals(inst, sj.read[Map[List[Pet], List[Pet]]](js)) - } - - test("List of Any as key") { - val inst: Map[List[Any], List[Any]] = - Map(List(23L, "wow", true) -> List(12.2, 0)) - val js = sj.render(inst) - assertEquals("""{"[23,\"wow\",true]":[12.2,0.0]}""".asInstanceOf[JSON],js) - assertEquals(true, sj.read[Map[List[Any], List[Any]]](js).isInstanceOf[Map[List[Any], List[Any]]]) - } - - test("List of parameterized class as key") { - val inst = Map( - List(AThing(true, "True"), AThing(false, "False")) -> List( - AThing(true, "Yes"), - AThing(false, "No") - ) - ) - val js = sj.render(inst) - assertEquals( - """{"[{\"a\":true,\"b\":\"True\"},{\"a\":false,\"b\":\"False\"}]":[{"a":true,"b":"Yes"},{"a":false,"b":"No"}]}""".asInstanceOf[JSON],js) - assertEquals(true, sj.read[Map[List[AThing[String, Boolean]], List[AThing[String, Boolean]]]](js) - .isInstanceOf[Map[List[AThing[String, Boolean]], List[AThing[String, Boolean]]]]) - } - - test("List of parameterized trait as key") { - val inst: Map[List[Thing[Boolean, String]], List[Thing[Boolean, String]]] = - Map( - List(AThing(true, "True"), AThing(false, "False")) -> List( - AThing(true, "Yes"), - AThing(false, "No") - ) - ) - val js = sj.render(inst) - assertEquals( - """{"[{\"_hint\":\"co.blocke.scalajack.json.mapkeys.AThing\",\"a\":true,\"b\":\"True\"},{\"_hint\":\"co.blocke.scalajack.json.mapkeys.AThing\",\"a\":false,\"b\":\"False\"}]":[{"_hint":"co.blocke.scalajack.json.mapkeys.AThing","a":true,"b":"Yes"},{"_hint":"co.blocke.scalajack.json.mapkeys.AThing","a":false,"b":"No"}]}""".asInstanceOf[JSON],js) - assertEquals(true, sj.read[Map[List[Thing[Boolean, String]], List[Thing[Boolean, String]]]](js) - .isInstanceOf[Map[List[Thing[Boolean, String]], List[Thing[Boolean, String]]]]) - } - - test("List of Optional as key") { - val inst: Map[List[Option[String]], List[Option[String]]] = - Map(List(Some("hey"), Some("you")) -> List(Some("stop"), Some("go"))) - val js = sj.render(inst) - assertEquals("""{"[\"hey\",\"you\"]":["stop","go"]}""".asInstanceOf[JSON],js) - assertEquals(inst, sj.read[Map[List[Option[String]], List[Option[String]]]](js)) - } - - test("List of ValueClass as key") { - val inst = - Map(List(VCChar('A'), VCChar('a')) -> List(VCChar('B'), VCChar('b'))) - val js = sj.render(inst) - assertEquals("""{"[\"A\",\"a\"]":["B","b"]}""".asInstanceOf[JSON],js) - assertEquals(inst, sj.read[Map[List[VCChar], List[VCChar]]](js)) - } diff --git a/core/src/test/scala/co.blocke.scalajack/json/mapkeys/MapCollKeys.scala b/core/src/test/scala/co.blocke.scalajack/json/mapkeys/MapCollKeys.scala deleted file mode 100644 index e618172c..00000000 --- a/core/src/test/scala/co.blocke.scalajack/json/mapkeys/MapCollKeys.scala +++ /dev/null @@ -1,166 +0,0 @@ -package co.blocke.scalajack -package json.mapkeys - -import co.blocke.scala_reflection._ -import TestUtil._ -import munit._ -import munit.internal.console -import co.blocke.scalajack.json.JSON - -class MapCollKeys() extends FunSuite: - - val sj = co.blocke.scalajack.ScalaJack() - - test("Map as key") { - describe( - "-----------------------\n: Map Map Key Tests :\n-----------------------", Console.BLUE - ) - - val m1 = Map(1 -> 2) - val m2 = Map(3 -> 4) - val inst = Map(m1 -> m2) - val js = sj.render(inst) - assertEquals("""{"{\"1\":2}":{"3":4}}""".asInstanceOf[JSON],js) - assertEquals(inst, sj.read[Map[Map[Int, Int], Map[Int, Int]]](js)) - } - - test("Map as key, map value is null") { - val m1 = Map(1 -> 2) - val m2: Map[Int, Int] = null - val inst = Map(m1 -> m2) - val js = sj.render(inst) - assertEquals("""{"{\"1\":2}":null}""".asInstanceOf[JSON],js) - assertEquals(inst, sj.read[Map[Map[Int, Int], Map[Int, Int]]](js)) - } - - test("Map of Lists as key") { - val m1 = List(Food.Meat, Food.Veggies) - val m2 = List(Food.Seeds, Food.Pellets) - val inst = Map(Map(m1 -> m2) -> Map(m2 -> m1)) - val js = sj.render(inst) - assertEquals( - """{"{\"[\\\"Meat\\\",\\\"Veggies\\\"]\":[\"Seeds\",\"Pellets\"]}":{"[\"Seeds\",\"Pellets\"]":["Meat","Veggies"]}}""".asInstanceOf[JSON],js) - assertEquals(inst, sj.read[Map[Map[List[Food.Value], List[Food.Value]], Map[List[Food.Value], List[Food.Value]]]](js)) - } - - test("Map of Maps as key") { - val m1 = Map(Food.Meat -> Food.Veggies) - val m2 = Map(Food.Seeds -> Food.Pellets) - val inst = Map(Map(m1 -> m2) -> Map(m2 -> m1)) - val js = sj.render(inst) - assertEquals( - """{"{\"{\\\"Meat\\\":\\\"Veggies\\\"}\":{\"Seeds\":\"Pellets\"}}":{"{\"Seeds\":\"Pellets\"}":{"Meat":"Veggies"}}}""".asInstanceOf[JSON],js) - assertEquals(inst, sj.read[Map[Map[Map[Food.Value, Food.Value], Map[Food.Value, Food.Value]], Map[Map[Food.Value, Food.Value], Map[Food.Value, Food.Value]]]](js)) - } - - test("Map of Tuples as key") { - val m1 = (Food.Meat, Food.Veggies) - val m2 = (Food.Seeds, Food.Pellets) - val inst = Map(Map(m1 -> m2) -> Map(m2 -> m1)) - val js = sj.render(inst) - assertEquals( - """{"{\"[\\\"Meat\\\",\\\"Veggies\\\"]\":[\"Seeds\",\"Pellets\"]}":{"[\"Seeds\",\"Pellets\"]":["Meat","Veggies"]}}""".asInstanceOf[JSON],js) - assertEquals(inst, sj.read[Map[Map[(Food.Value, Food.Value), (Food.Value, Food.Value)], Map[(Food.Value, Food.Value), (Food.Value, Food.Value)]]](js)) - } - - test("Map of Case Class as key") { - val m1 = - Map(DogPet("Fido", Food.Meat, 4) -> FishPet("Flipper", Food.Meat, 87.3)) - val m2 = - Map(FishPet("Flipper", Food.Meat, 87.3) -> DogPet("Fido", Food.Meat, 4)) - val inst = Map(Map(m1 -> m2) -> Map(m2 -> m1)) - val js = sj.render(inst) - assertEquals( - """{"{\"{\\\"{\\\\\\\"name\\\\\\\":\\\\\\\"Fido\\\\\\\",\\\\\\\"food\\\\\\\":\\\\\\\"Meat\\\\\\\",\\\\\\\"numLegs\\\\\\\":4}\\\":{\\\"name\\\":\\\"Flipper\\\",\\\"food\\\":\\\"Meat\\\",\\\"waterTemp\\\":87.3}}\":{\"{\\\"name\\\":\\\"Flipper\\\",\\\"food\\\":\\\"Meat\\\",\\\"waterTemp\\\":87.3}\":{\"name\":\"Fido\",\"food\":\"Meat\",\"numLegs\":4}}}":{"{\"{\\\"name\\\":\\\"Flipper\\\",\\\"food\\\":\\\"Meat\\\",\\\"waterTemp\\\":87.3}\":{\"name\":\"Fido\",\"food\":\"Meat\",\"numLegs\":4}}":{"{\"name\":\"Fido\",\"food\":\"Meat\",\"numLegs\":4}":{"name":"Flipper","food":"Meat","waterTemp":87.3}}}}""".asInstanceOf[JSON],js) - assertEquals(inst, sj.read[Map[Map[Map[DogPet, FishPet], Map[FishPet, DogPet]], Map[Map[FishPet, DogPet], Map[DogPet, FishPet]]]](js)) - } - - test("Map of Trait as key") { - val m1: Map[Pet, Pet] = - Map(DogPet("Fido", Food.Meat, 4) -> FishPet("Flipper", Food.Meat, 87.3)) - val m2: Map[Pet, Pet] = - Map(FishPet("Flipper", Food.Meat, 87.3) -> DogPet("Fido", Food.Meat, 4)) - val inst = Map(Map(m1 -> m2) -> Map(m2 -> m1)) - val js = sj.render(inst) - assertEquals( - """{"{\"{\\\"{\\\\\\\"_hint\\\\\\\":\\\\\\\"co.blocke.scalajack.json.mapkeys.DogPet\\\\\\\",\\\\\\\"name\\\\\\\":\\\\\\\"Fido\\\\\\\",\\\\\\\"food\\\\\\\":\\\\\\\"Meat\\\\\\\",\\\\\\\"numLegs\\\\\\\":4}\\\":{\\\"_hint\\\":\\\"co.blocke.scalajack.json.mapkeys.FishPet\\\",\\\"name\\\":\\\"Flipper\\\",\\\"food\\\":\\\"Meat\\\",\\\"waterTemp\\\":87.3}}\":{\"{\\\"_hint\\\":\\\"co.blocke.scalajack.json.mapkeys.FishPet\\\",\\\"name\\\":\\\"Flipper\\\",\\\"food\\\":\\\"Meat\\\",\\\"waterTemp\\\":87.3}\":{\"_hint\":\"co.blocke.scalajack.json.mapkeys.DogPet\",\"name\":\"Fido\",\"food\":\"Meat\",\"numLegs\":4}}}":{"{\"{\\\"_hint\\\":\\\"co.blocke.scalajack.json.mapkeys.FishPet\\\",\\\"name\\\":\\\"Flipper\\\",\\\"food\\\":\\\"Meat\\\",\\\"waterTemp\\\":87.3}\":{\"_hint\":\"co.blocke.scalajack.json.mapkeys.DogPet\",\"name\":\"Fido\",\"food\":\"Meat\",\"numLegs\":4}}":{"{\"_hint\":\"co.blocke.scalajack.json.mapkeys.DogPet\",\"name\":\"Fido\",\"food\":\"Meat\",\"numLegs\":4}":{"_hint":"co.blocke.scalajack.json.mapkeys.FishPet","name":"Flipper","food":"Meat","waterTemp":87.3}}}}""".asInstanceOf[JSON],js) - assertEquals(inst, sj.read[Map[Map[Map[Pet, Pet], Map[Pet, Pet]], Map[Map[Pet, Pet], Map[Pet, Pet]]]](js)) - } - - test("Map of Any as key") { - val m1: Map[Any, Any] = Map(123.45 -> 2) - val m2: Map[Any, Any] = Map(398328372 -> 0) - val inst = Map(Map(m1 -> m2) -> Map(m2 -> m1)) - val js = sj.render(inst) - assertEquals( - """{"{\"{\\\"123.45\\\":2}\":{\"398328372\":0}}":{"{\"398328372\":0}":{"123.45":2}}}""".asInstanceOf[JSON],js) - assertEquals(true, - sj.read[Map[Map[Map[Any, Any], Map[Any, Any]], Map[Map[Any, Any], Map[Any, Any]]]](js) - .isInstanceOf[Map[Map[Map[Any, Any], Map[Any, Any]], Map[Map[Any, Any], Map[Any, Any]]]]) - } - - test("Map of parameterized class as key") { - val m1: Map[AThing[Int, String], AThing[Int, String]] = - Map(AThing("one", 1) -> AThing("two", 2)) - val m2: Map[AThing[Int, String], AThing[Int, String]] = - Map(AThing("four", 4) -> AThing("three", 3)) - val inst = Map(Map(m1 -> m2) -> Map(m2 -> m1)) - val js = sj.render(inst) - assertEquals( - """{"{\"{\\\"{\\\\\\\"a\\\\\\\":\\\\\\\"one\\\\\\\",\\\\\\\"b\\\\\\\":1}\\\":{\\\"a\\\":\\\"two\\\",\\\"b\\\":2}}\":{\"{\\\"a\\\":\\\"four\\\",\\\"b\\\":4}\":{\"a\":\"three\",\"b\":3}}}":{"{\"{\\\"a\\\":\\\"four\\\",\\\"b\\\":4}\":{\"a\":\"three\",\"b\":3}}":{"{\"a\":\"one\",\"b\":1}":{"a":"two","b":2}}}}""".asInstanceOf[JSON],js) - assertEquals(true, - sj.read[Map[Map[Map[AThing[Int, String], AThing[Int, String]], Map[AThing[Int, String], AThing[Int, String]]], Map[Map[AThing[Int, String], AThing[Int, String]], Map[AThing[Int, String], AThing[Int, String]]]]](js) - .isInstanceOf[Map[Map[Map[AThing[Int, String], AThing[Int, String]], Map[AThing[Int, String], AThing[Int, String]]], Map[Map[AThing[Int, String], AThing[Int, String]], Map[AThing[Int, String], AThing[Int, String]]]]]) - } - - test("Map of parameterized trait as key") { - val m1: Map[Thing[String, Int], Thing[String, Int]] = - Map(AThing("one", 1) -> AThing("two", 2)) - val m2: Map[Thing[String, Int], Thing[String, Int]] = - Map(AThing("four", 4) -> AThing("three", 3)) - val inst = Map(Map(m1 -> m2) -> Map(m2 -> m1)) - val js = sj.render(inst) - assertEquals( - """{"{\"{\\\"{\\\\\\\"_hint\\\\\\\":\\\\\\\"co.blocke.scalajack.json.mapkeys.AThing\\\\\\\",\\\\\\\"a\\\\\\\":\\\\\\\"one\\\\\\\",\\\\\\\"b\\\\\\\":1}\\\":{\\\"_hint\\\":\\\"co.blocke.scalajack.json.mapkeys.AThing\\\",\\\"a\\\":\\\"two\\\",\\\"b\\\":2}}\":{\"{\\\"_hint\\\":\\\"co.blocke.scalajack.json.mapkeys.AThing\\\",\\\"a\\\":\\\"four\\\",\\\"b\\\":4}\":{\"_hint\":\"co.blocke.scalajack.json.mapkeys.AThing\",\"a\":\"three\",\"b\":3}}}":{"{\"{\\\"_hint\\\":\\\"co.blocke.scalajack.json.mapkeys.AThing\\\",\\\"a\\\":\\\"four\\\",\\\"b\\\":4}\":{\"_hint\":\"co.blocke.scalajack.json.mapkeys.AThing\",\"a\":\"three\",\"b\":3}}":{"{\"_hint\":\"co.blocke.scalajack.json.mapkeys.AThing\",\"a\":\"one\",\"b\":1}":{"_hint":"co.blocke.scalajack.json.mapkeys.AThing","a":"two","b":2}}}}""".asInstanceOf[JSON],js) - assertEquals(true, - sj.read[Map[Map[Map[Thing[String, Int], Thing[String, Int]], Map[Thing[String, Int], Thing[String, Int]]], Map[Map[Thing[String, Int], Thing[String, Int]], Map[Thing[String, Int], Thing[String, Int]]]]](js) - .isInstanceOf[Map[Map[Map[Thing[String, Int], Thing[String, Int]], Map[Thing[String, Int], Thing[String, Int]]], Map[Map[Thing[String, Int], Thing[String, Int]], Map[Thing[String, Int], Thing[String, Int]]]]] - ) - } - - test("Map of Optional as key") { - val m1: Map[Option[Int], Option[Int]] = Map(Some(3) -> None) - val m2: Map[Option[Int], Option[Int]] = - Map(None -> Some(2), Some(5) -> null) - val inst = Map(Map(m1 -> m2) -> Map(m2 -> m1)) - val js = sj.render(inst) - assertEquals("""{"{\"{}\":{\"5\":null}}":{"{\"5\":null}":{}}}""".asInstanceOf[JSON],js) - assert( - Map( - Map(Map() -> Map(Some(5) -> None)) -> Map( - Map(Some(5) -> None) -> Map() - ) - ) == sj.read[Map[Map[Map[Option[Int], Option[Int]], Map[Option[Int], Option[Int]]], Map[Map[Option[Int], Option[Int]], Map[Option[Int], Option[Int]]]]](js)) - } - - test("Map of Option as key where Option is null must fail") { - val m1: Map[Option[Int], Option[Int]] = Map(Some(3) -> None) - val m0 = Map.empty[Option[Int], Option[Int]] - val bad: Option[Int] = null - val m2 = m0 + (bad -> Some(99)) - val inst = Map(Map(m1 -> m2) -> Map(m2 -> m1)) - val msg = "Map keys cannot be null." - interceptMessage[co.blocke.scalajack.ScalaJackError](msg){ - sj.render(inst) - } - } - - test("Map of ValueClass as key") { - val m1: Map[VCChar, VCChar] = Map(VCChar('Z') -> VCChar('z')) - val m2: Map[VCChar, VCChar] = Map(VCChar('A') -> VCChar('a')) - val inst = Map(Map(m1 -> m2) -> Map(m2 -> m1)) - val js = sj.render(inst) - assertEquals( - """{"{\"{\\\"Z\\\":\\\"z\\\"}\":{\"A\":\"a\"}}":{"{\"A\":\"a\"}":{"Z":"z"}}}""".asInstanceOf[JSON],js) - assertEquals(inst, sj.read[Map[Map[Map[VCChar, VCChar], Map[VCChar, VCChar]], Map[Map[VCChar, VCChar], Map[VCChar, VCChar]]]](js)) - } \ No newline at end of file diff --git a/core/src/test/scala/co.blocke.scalajack/json/mapkeys/Model.scala b/core/src/test/scala/co.blocke.scalajack/json/mapkeys/Model.scala deleted file mode 100644 index b470e48b..00000000 --- a/core/src/test/scala/co.blocke.scalajack/json/mapkeys/Model.scala +++ /dev/null @@ -1,176 +0,0 @@ -package co.blocke.scalajack -package json.mapkeys - -import java.util.UUID -import java.lang.{ - Boolean => JBoolean, - Byte => JByte, - Character => JChar, - Double => JDouble, - Float => JFloat, - Integer => JInteger, - Long => JLong, - Number => JNumber, - Short => JShort -} -import java.math.{ BigDecimal => JBigDecimal, BigInteger => JBigInteger } -import java.time._ - -object Size extends Enumeration { - val Small, Medium, Large = Value -} - -trait Thing[A, B] { val a: A; val b: B } -case class AThing[Y, X](a: X, b: Y) extends Thing[X, Y] -trait Part[A] { val p: A } -case class APart[A](p: A) extends Part[A] - -// === Scala Primitive Keys -case class SampleBigDecimal(m: Map[BigDecimal, BigDecimal]) -case class SampleBigInt(m: Map[BigInt, BigInt]) -case class SampleBoolean(m: Map[Boolean, Boolean]) -case class SampleByte(m: Map[Byte, Byte]) -case class SampleChar(m: Map[Char, Char]) -case class SampleDouble(m: Map[Double, Double]) -case class SampleEnumeration(m: Map[Size.Value, Size.Value]) -case class SampleFloat(m: Map[Float, Float]) -case class SampleInt(m: Map[Int, Int]) -case class SampleLong(m: Map[Long, Long]) -case class SampleShort(m: Map[Short, Short]) - -// === Java Primitive Keys -case class SampleJBigDecimal(m: Map[JBigDecimal, JBigDecimal]) -case class SampleJBigInteger(m: Map[JBigInteger, JBigInteger]) -case class SampleJBoolean(m: Map[JBoolean, JBoolean]) -case class SampleJByte(m: Map[JByte, JByte]) -case class SampleJChar(m: Map[JChar, JChar]) -case class SampleJDouble(m: Map[JDouble, JDouble]) -case class SampleJFloat(m: Map[JFloat, JFloat]) -case class SampleJInteger(m: Map[JInteger, JInteger]) -case class SampleJLong(m: Map[JLong, JLong]) -case class SampleJNumber(m: Map[JNumber, JNumber]) -case class SampleJShort(m: Map[JShort, JShort]) - -// === Java Time Keys -case class SampleDuration(m: Map[Duration, Duration]) -case class SampleInstant(m: Map[Instant, Instant]) -case class SampleLocalDateTime(m: Map[LocalDateTime, LocalDateTime]) -case class SampleLocalDate(m: Map[LocalDate, LocalDate]) -case class SampleLocalTime(m: Map[LocalTime, LocalTime]) -case class SampleOffsetDateTime(m: Map[OffsetDateTime, OffsetDateTime]) -case class SampleOffsetTime(m: Map[OffsetTime, OffsetTime]) -case class SamplePeriod(m: Map[Period, Period]) -case class SampleZonedDateTime(m: Map[ZonedDateTime, ZonedDateTime]) - -// === Any primitives -case class AnyShell(m: Map[Any, Any]) - -// === Class Keys -case class SimpleClass(name: String, age: Int, isOk: Boolean, favorite: Any) -case class SampleSimple(m: Map[SimpleClass, SimpleClass]) -case class ComplexClass(id: UUID, simple: SimpleClass, allDone: Boolean) -case class SampleComplex(m: Map[ComplexClass, ComplexClass]) - -object Food extends Enumeration { - val Seeds, Meat, Pellets, Veggies = Value -} -trait Pet { - val name: String - val food: Food.Value -} -case class FishPet(name: String, food: Food.Value, waterTemp: Double) - extends Pet -case class DogPet(name: String, food: Food.Value, numLegs: Int) extends Pet -case class CompoundPet(name: String, food: Food.Value, pet: Pet) extends Pet -trait PetHolder { - val address: String - val pet: Pet -} -case class ShinyPetHolder(address: String, pet: Pet) extends PetHolder -case class SamplePet(m: Map[Pet, Pet]) -case class PolyClass(lookup: Map[String, Int], favs: List[String]) -case class SamplePolyClass(m: Map[PolyClass, PolyClass]) -case class SampleShiny(m: Map[PetHolder, PetHolder]) -case class NCKey(nc: Map[Int, Boolean], name: String) -case class SampleNCKey(m: Map[NCKey, NCKey]) - -// === Collections - Tuple -case class SampleTuple(m: Map[(Int, String, Char), (String, Boolean)]) -case class SampleTupleList( - m: Map[(List[String], List[Int]), (List[String], List[Int])] -) -case class SampleTupleMap( - m: Map[(Map[String, Int], Map[Int, String]), (Map[String, Int], Map[Int, String])] -) -case class SampleTupleTuple( - m: Map[((String, Boolean), (Int, Double)), ((String, Boolean), (Int, Double))] -) -case class SampleTupleClass( - m: Map[(SampleChar, SampleInt), (SampleChar, SampleInt)] -) -case class SampleTupleTrait(m: Map[(Pet, Pet), (Pet, Pet)]) -case class SampleTupleAny(m: Map[(Any, Any), (Any, Any)]) -case class SampleTupleOptional( - m: Map[(Option[Int], Option[String]), (Option[Boolean], Option[Food.Value])] -) -case class SampleTupleVC(m: Map[(VCChar, VCChar), (VCChar, VCChar)]) -case class SampleTupleComplex( - m: Map[(ComplexClass, ComplexClass), (ComplexClass, ComplexClass)] -) -case class SampleTuplePolyClass( - m: Map[(PolyClass, PolyClass), (PolyClass, PolyClass)] -) - -// === Value Classes -case class VCBigDecimal(vc: BigDecimal) extends AnyVal -case class SampleVCBigDecimal(m: Map[VCBigDecimal, VCBigDecimal]) -case class VCBigInt(vc: BigInt) extends AnyVal -case class SampleVCBigInt(m: Map[VCBigInt, VCBigInt]) -case class VCBoolean(vc: Boolean) extends AnyVal -case class SampleVCBoolean(m: Map[VCBoolean, VCBoolean]) -case class VCByte(vc: Byte) extends AnyVal -case class SampleVCByte(m: Map[VCByte, VCByte]) -case class VCChar(vc: Char) extends AnyVal -case class SampleVCChar(m: Map[VCChar, VCChar]) -case class VCDouble(vc: Double) extends AnyVal -case class SampleVCDouble(m: Map[VCDouble, VCDouble]) -case class VCEnumeration(vc: Food.Value) extends AnyVal -case class SampleVCEnumeration(m: Map[VCEnumeration, VCEnumeration]) -case class VCFloat(vc: Float) extends AnyVal -case class SampleVCFloat(m: Map[VCFloat, VCFloat]) -case class VCInt(vc: Int) extends AnyVal -case class SampleVCInt(m: Map[VCInt, VCInt]) -case class VCLong(vc: Long) extends AnyVal -case class SampleVCLong(m: Map[VCLong, VCLong]) -case class VCShort(vc: Short) extends AnyVal -case class SampleVCShort(m: Map[VCShort, VCShort]) -case class VCString(vc: String) extends AnyVal -case class SampleVCString(m: Map[VCString, VCString]) -case class VCUUID(vc: UUID) extends AnyVal -case class SampleVCUUID(m: Map[VCUUID, VCUUID]) -case class VCNumber(vc: Number) extends AnyVal -case class SampleVCNumber(m: Map[VCNumber, VCNumber]) -case class VCList(vc: List[Int]) extends AnyVal -case class SampleVCList(m: Map[VCList, VCList]) -case class VCMap(vc: Map[Int, Int]) extends AnyVal -case class SampleVCMap(m: Map[VCMap, VCMap]) -case class VCTuple(vc: Tuple3[Int, String, Boolean]) extends AnyVal -case class SampleVCTuple(m: Map[VCTuple, VCTuple]) - -case class VCClass(vc: ComplexClass) extends AnyVal -case class SampleVCClass(m: Map[VCClass, VCClass]) -case class VCTrait(vc: Pet) extends AnyVal -case class SampleVCTrait(m: Map[VCTrait, VCTrait]) -case class VCOption(vc: Option[String]) extends AnyVal -case class SampleVCOption(m: Map[VCOption, VCOption]) -case class VCNested(vc: List[Map[String, String]]) extends AnyVal -case class SampleVCNested(m: Map[VCNested, VCNested]) - -case class VCParamClass[A, B](vc: AThing[A, B]) extends AnyVal -case class SampleVCParamClass[A, B]( - m: Map[VCParamClass[A, B], VCParamClass[A, B]] -) -case class VCParamTrait[A, B](vc: Thing[A, B]) extends AnyVal -case class SampleVCParamTrait[A, B]( - m: Map[VCParamTrait[A, B], VCParamTrait[A, B]] -) diff --git a/core/src/test/scala/co.blocke.scalajack/json/mapkeys/ScalaPrimKeys.scala b/core/src/test/scala/co.blocke.scalajack/json/mapkeys/ScalaPrimKeys.scala deleted file mode 100644 index 7b0eeeea..00000000 --- a/core/src/test/scala/co.blocke.scalajack/json/mapkeys/ScalaPrimKeys.scala +++ /dev/null @@ -1,248 +0,0 @@ -package co.blocke.scalajack -package json.mapkeys - -import co.blocke.scala_reflection._ -import TestUtil._ -import munit._ -import munit.internal.console -import co.blocke.scalajack.json.JSON - -class ScalaPrimKeys() extends FunSuite: - - val sj = co.blocke.scalajack.ScalaJack() - - test("With Any Key") { - describe( - "-----------------------------------\n: Scala Primitive Map Key Tests :\n-----------------------------------", Console.BLUE - ) - describe("+++ Positive Tests +++") - - val inst = AnyShell( - Map( - List(1, 2, 3) -> List("a", "b", "c"), - DogPet("Fido", Food.Meat, 4) -> DogPet("Fifi", Food.Meat, 4), - Size.Small -> "ok", - 123.456 -> true, - 293845 -> "Greg", - false -> "16", - "Fred" -> "Wilma", - 16.toByte -> null - ) - ) - val js = sj.render(inst) - assert( json.JsonMatcher.jsonMatches(js, - """{"m":{"false":"16","Small":"ok","123.456":true,"{\"_hint\":\"co.blocke.scalajack.json.mapkeys.DogPet\",\"name\":\"Fido\",\"food\":\"Meat\",\"numLegs\":4}":{"_hint":"co.blocke.scalajack.json.mapkeys.DogPet","name":"Fifi","food":"Meat","numLegs":4},"Fred":"Wilma","[1,2,3]":["a","b","c"],"293845":"Greg","16":null}}""".asInstanceOf[JSON] - ) ) - val read = sj.read[AnyShell](js).m.keySet.map(z => (z, z.getClass.getName)) - assert(read.contains((16, "java.lang.Integer"))) - assert(read.contains((293845, "java.lang.Integer"))) - assert(read.contains((123.456, "java.lang.Double"))) - assert(read.contains(("Small", "java.lang.String"))) - assert(read.contains(("Fred", "java.lang.String"))) - assert(read.contains((false, "java.lang.Boolean"))) - assert(read.contains( - ( - DogPet("Fido", Food.Meat, 4), - "co.blocke.scalajack.json.mapkeys.DogPet" - ) - )) - } - - test("With BigDecimal Key") { - val inst = SampleBigDecimal( - Map( - BigDecimal(123.456) -> BigDecimal(1), - BigDecimal(789.123) -> BigDecimal(2) - ) - ) - val js = sj.render(inst) - assertEquals("""{"m":{"123.456":1,"789.123":2}}""".asInstanceOf[JSON],js) - assertEquals(inst, sj.read[SampleBigDecimal](js)) - } - - test("With BigInt Key") { - val inst = - SampleBigInt(Map(BigInt(123) -> BigInt(1), BigInt(789) -> BigInt(2))) - val js = sj.render(inst) - assertEquals("""{"m":{"123":1,"789":2}}""".asInstanceOf[JSON],js) - assertEquals(inst, sj.read[SampleBigInt](js)) - } - - test("With Boolean Key") { - val inst = SampleBoolean(Map(true -> false, false -> true)) - val js = sj.render(inst) - assertEquals("""{"m":{"true":false,"false":true}}""".asInstanceOf[JSON],js) - assertEquals(inst, sj.read[SampleBoolean](js)) - } - - test("With Byte Key") { - val inst = SampleByte(Map(16.toByte -> 2.toByte, 48.toByte -> 9.toByte)) - val js = sj.render(inst) - assertEquals("""{"m":{"16":2,"48":9}}""".asInstanceOf[JSON],js) - assertEquals(inst, sj.read[SampleByte](js)) - } - - test("With Char Key") { - val inst = SampleChar(Map('a' -> 'A', 'z' -> 'Z')) - val js = sj.render(inst) - assertEquals("""{"m":{"a":"A","z":"Z"}}""".asInstanceOf[JSON],js) - assertEquals(inst, sj.read[SampleChar](js)) - } - - test("With Double Key") { - val inst = SampleDouble(Map(12.34 -> 56.78, 90.12 -> 34.56)) - val js = sj.render(inst) - assertEquals("""{"m":{"12.34":56.78,"90.12":34.56}}""".asInstanceOf[JSON],js) - assertEquals(inst, sj.read[SampleDouble](js)) - } - - test("With Enumeration Key") { - val inst = SampleEnumeration( - Map(Size.Small -> Size.Large, Size.Large -> Size.Medium) - ) - val js = sj.render(inst) - assertEquals("""{"m":{"Small":"Large","Large":"Medium"}}""".asInstanceOf[JSON],js) - assertEquals(inst, sj.read[SampleEnumeration](js)) - } - - test("With Float Key") { - val inst = SampleFloat(Map(12.34F -> 56.78F, 90.12F -> 34.56F)) - val js = sj.render(inst) - assertEquals("""{"m":{"12.34":56.78,"90.12":34.56}}""".asInstanceOf[JSON],js) - assertEquals(inst, sj.read[SampleFloat](js)) - } - - test("With Int Key") { - val inst = SampleInt(Map(12 -> 56, 90 -> 34)) - val js = sj.render(inst) - assertEquals("""{"m":{"12":56,"90":34}}""".asInstanceOf[JSON],js) - assertEquals(inst, sj.read[SampleInt](js)) - } - - test("With Long Key") { - val inst = SampleLong(Map(12L -> 56L, 90L -> 34L)) - val js = sj.render(inst) - assertEquals("""{"m":{"12":56,"90":34}}""".asInstanceOf[JSON],js) - assertEquals(inst, sj.read[SampleLong](js)) - } - - test("With Short Key") { - val inst = - SampleShort(Map(12.toShort -> 56.toShort, 90.toShort -> 34.toShort)) - val js = sj.render(inst) - assertEquals("""{"m":{"12":56,"90":34}}""".asInstanceOf[JSON],js) - assertEquals(inst, sj.read[SampleShort](js)) - } - - test("Bad BigDecimal Key") { - describe("--- Negative Tests ---") - - val js = """{"m":{"789.123":1,"fred":2}}""".asInstanceOf[JSON] - val msg = """Expected a Number here - |fred - |^""".stripMargin - interceptMessage[co.blocke.scalajack.ScalaJackError](msg){ - sj.read[SampleBigDecimal](js) - } - } - - test("Bad BigInt Key") { - val js = """{"m":{"fred":1,"789":2}}""".asInstanceOf[JSON] - val msg = """Expected a Number here - |fred - |^""".stripMargin - interceptMessage[co.blocke.scalajack.ScalaJackError](msg){ - sj.read[SampleBigInt](js) - } - } - - test("Bad Boolean Key") { - val js = """{"m":{"true":false,"123":true}}""".asInstanceOf[JSON] - val msg = """Expected a Boolean here - |123 - |^""".stripMargin - interceptMessage[co.blocke.scalajack.ScalaJackError](msg){ - sj.read[SampleBoolean](js) - } - } - - test("Bad Byte Key") { - val js = """{"m":{"16":2,"x48":9}}""".asInstanceOf[JSON] - val msg = """Expected a Number here - |x48 - |^""".stripMargin - interceptMessage[co.blocke.scalajack.ScalaJackError](msg){ - sj.read[SampleByte](js) - } - } - - test("Bad Char Key") { // NOTE: This comprehensively tests for any null keyed Map - val js = """{"m":{null:"A","z":"Z"}}""".asInstanceOf[JSON] - val msg = """A Char typed value cannot be null - |{"m":{null:"A","z":"Z"}} - |---------^""".stripMargin - interceptMessage[co.blocke.scalajack.ScalaJackError](msg){ - sj.read[SampleChar](js) - } - } - - test("Bad Double Key") { - val js = """{"m":{"12.34":56.78,"true":34.56}}""".asInstanceOf[JSON] - val msg = """Expected a Number here - |true - |^""".stripMargin - interceptMessage[co.blocke.scalajack.ScalaJackError](msg){ - sj.read[SampleDouble](js) - } - } - - test("Bad Enumeration Key") { - val js = """{"m":{"Small":"Large","Bogus":"Medium"}}""".asInstanceOf[JSON] - val msg = - """No value found in enumeration co.blocke.scalajack.json.mapkeys.Size$ for Bogus - |{"m":{"Small":"Large","Bogus":"Medium"}} - |----------------------------^""".stripMargin - interceptMessage[co.blocke.scalajack.ScalaJackError](msg){ - sj.read[SampleEnumeration](js) - } - } - - test("Bad Float Key") { - val js = """{"m":{"12.34":56.78,"90.12.3":34.56}}""".asInstanceOf[JSON] - val msg = """Cannot parse an Float from value - |90.12.3 - |------^""".stripMargin - interceptMessage[co.blocke.scalajack.ScalaJackError](msg){ - sj.read[SampleFloat](js) - } - } - - test("Bad Int Key") { - val js = """{"m":{"12.0":56,"90":34}}""".asInstanceOf[JSON] - val msg = """Cannot parse an Int from value - |12.0 - |---^""".stripMargin - interceptMessage[co.blocke.scalajack.ScalaJackError](msg){ - sj.read[SampleInt](js) - } - } - - test("Bad Long Key") { - val js = """{"m":{"12":56,"hey":34}}""".asInstanceOf[JSON] - val msg = """Expected a Number here - |hey - |^""".stripMargin - interceptMessage[co.blocke.scalajack.ScalaJackError](msg){ - sj.read[SampleLong](js) - } - } - - test("Bad Short Key") { - val js = """{"m":{"p99999":56,"90":34}}""".asInstanceOf[JSON] - val msg = """Expected a Number here - |p99999 - |^""".stripMargin - interceptMessage[co.blocke.scalajack.ScalaJackError](msg){ - sj.read[SampleShort](js) - } - } diff --git a/core/src/test/scala/co.blocke.scalajack/json/mapkeys/TupleCollKeys.scala b/core/src/test/scala/co.blocke.scalajack/json/mapkeys/TupleCollKeys.scala deleted file mode 100644 index f3205beb..00000000 --- a/core/src/test/scala/co.blocke.scalajack/json/mapkeys/TupleCollKeys.scala +++ /dev/null @@ -1,236 +0,0 @@ -package co.blocke.scalajack -package json.mapkeys - -import co.blocke.scala_reflection._ -import TestUtil._ -import munit._ -import munit.internal.console -import co.blocke.scalajack.json.JSON -import java.util.UUID -import co.blocke.scalajack.model._ - -class TupleCollKeys() extends FunSuite: - - val sj = co.blocke.scalajack.ScalaJack() - - test("Tuple as key") { - describe( - "-------------------------\n: Tuple Map Key Tests :\n-------------------------", Console.BLUE - ) - - val a = (2, "Blather", 'Q') - val b = ("Foo", true) - val inst = SampleTuple(Map(a -> b)) - val js = sj.render(inst) - assertEquals("""{"m":{"[2,\"Blather\",\"Q\"]":["Foo",true]}}""".asInstanceOf[JSON],js) - assertEquals(inst,sj.read[SampleTuple](js)) - } - - test("Tuple as key, null tuple as value") { - val a = (2, "Blather", 'Q') - val b: (String, Boolean) = null - val inst = SampleTuple(Map(a -> b)) - val js = sj.render(inst) - assertEquals("""{"m":{"[2,\"Blather\",\"Q\"]":null}}""".asInstanceOf[JSON],js) - assertEquals(inst,sj.read[SampleTuple](js)) - } - - test("Tuple of Lists as key") { - val a = (List("one", "two", "three"), List(1, 2, 3)) - val b = (List("four", "five", "six"), List(4, 5, 6)) - val inst = SampleTupleList(Map(a -> b)) - val js = sj.render(inst) - assertEquals( - """{"m":{"[[\"one\",\"two\",\"three\"],[1,2,3]]":[["four","five","six"],[4,5,6]]}}""".asInstanceOf[JSON],js) - assertEquals(inst,sj.read[SampleTupleList](js)) - } - - test("Tuple of Maps as key") { - val a = (Map("one" -> 1), Map(2 -> "two")) - val b = (Map("three" -> 3), Map(4 -> "four")) - val inst = SampleTupleMap(Map(a -> b)) - val js = sj.render(inst) - assertEquals( - """{"m":{"[{\"one\":1},{\"2\":\"two\"}]":[{"three":3},{"4":"four"}]}}""".asInstanceOf[JSON],js) - assertEquals(inst,sj.read[SampleTupleMap](js)) - } - - test("Tuple of Tuples as key") { - val a = (("yes", true), (1, 0.25)) - val b = (("no", false), (2, 0.5)) - val inst = SampleTupleTuple(Map(a -> b)) - val js = sj.render(inst) - assertEquals( - """{"m":{"[[\"yes\",true],[1,0.25]]":[["no",false],[2,0.5]]}}""".asInstanceOf[JSON],js) - assertEquals(inst,sj.read[SampleTupleTuple](js)) - } - - test("Tuple of Case Class as key") { - val a = (SampleChar(Map('a' -> 'A')), SampleInt(Map(1 -> 2))) - val b = (SampleChar(Map('z' -> 'Z')), SampleInt(Map(99 -> 100))) - val inst = SampleTupleClass(Map(a -> b)) - val js = sj.render(inst) - assertEquals( - """{"m":{"[{\"m\":{\"a\":\"A\"}},{\"m\":{\"1\":2}}]":[{"m":{"z":"Z"}},{"m":{"99":100}}]}}""".asInstanceOf[JSON],js) - assertEquals(inst,sj.read[SampleTupleClass](js)) - } - - test("Tuple of Trait as key") { - val a = (DogPet("Fido", Food.Meat, 4), FishPet("Jaws", Food.Meat, 69.8)) - val b = - (DogPet("Chey", Food.Meat, 3), FishPet("Flipper", Food.Seeds, 80.1)) - val inst = SampleTupleTrait(Map(a -> b)) - val js = sj.render(inst) - assertEquals( - """{"m":{"[{\"_hint\":\"co.blocke.scalajack.json.mapkeys.DogPet\",\"name\":\"Fido\",\"food\":\"Meat\",\"numLegs\":4},{\"_hint\":\"co.blocke.scalajack.json.mapkeys.FishPet\",\"name\":\"Jaws\",\"food\":\"Meat\",\"waterTemp\":69.8}]":[{"_hint":"co.blocke.scalajack.json.mapkeys.DogPet","name":"Chey","food":"Meat","numLegs":3},{"_hint":"co.blocke.scalajack.json.mapkeys.FishPet","name":"Flipper","food":"Seeds","waterTemp":80.1}]}}""".asInstanceOf[JSON],js) - assertEquals(inst,sj.read[SampleTupleTrait](js)) - } - - test("Tuple of Any as key") { - val a = (DogPet("Fido", Food.Meat, 4), FishPet("Jaws", Food.Meat, 69.8)) - val b = - (DogPet("Chey", Food.Meat, 3), FishPet("Flipper", Food.Seeds, 80.1)) - val inst = SampleTupleTrait(Map(a -> b)) - val js = sj.render(inst) - assertEquals( - """{"m":{"[{\"_hint\":\"co.blocke.scalajack.json.mapkeys.DogPet\",\"name\":\"Fido\",\"food\":\"Meat\",\"numLegs\":4},{\"_hint\":\"co.blocke.scalajack.json.mapkeys.FishPet\",\"name\":\"Jaws\",\"food\":\"Meat\",\"waterTemp\":69.8}]":[{"_hint":"co.blocke.scalajack.json.mapkeys.DogPet","name":"Chey","food":"Meat","numLegs":3},{"_hint":"co.blocke.scalajack.json.mapkeys.FishPet","name":"Flipper","food":"Seeds","waterTemp":80.1}]}}""".asInstanceOf[JSON],js) - assertEquals(inst,sj.read[SampleTupleTrait](js)) - } - - test("Tuple of Optional as key") { - val a = (Some(5), Some("Fred")) - val b = (None, Some(Food.Meat)) - val inst = SampleTupleOptional(Map(a -> b)) - val js = sj.render(inst) - assertEquals("""{"m":{"[5,\"Fred\"]":[null,"Meat"]}}""".asInstanceOf[JSON],js) - assertEquals(inst,sj.read[SampleTupleOptional](js)) - } - - test("Tuple of ValueClass as key") { - val a = (VCChar('a'), VCChar('A')) - val b = (VCChar('z'), VCChar('Z')) - val inst = SampleTupleVC(Map(a -> b)) - val js = sj.render(inst) - assertEquals("""{"m":{"[\"a\",\"A\"]":["z","Z"]}}""".asInstanceOf[JSON],js) - assertEquals(inst,sj.read[SampleTupleVC](js)) - } - - test("Complex class (having members that are classes) as key") { - val a = SimpleClass("Larry", 32, isOk = true, "golf") - val b = SimpleClass("Mike", 27, isOk = false, 125) - val c1 = ComplexClass( - UUID.fromString("1e6c2b31-4dfe-4bf6-a0a0-882caaff0e9c"), - a, - allDone = true - ) - val c2 = ComplexClass( - UUID.fromString("1e6c2b31-4dfe-4bf6-a0a0-882caaff0e9d"), - b, - allDone = false - ) - val t1 = (c1, c2) - val t2 = (c2, c1) - val inst = SampleTupleComplex(Map(t1 -> t2)) - val js = sj.render(inst) - assertEquals( - """{"m":{"[{\"id\":\"1e6c2b31-4dfe-4bf6-a0a0-882caaff0e9c\",\"simple\":{\"name\":\"Larry\",\"age\":32,\"isOk\":true,\"favorite\":\"golf\"},\"allDone\":true},{\"id\":\"1e6c2b31-4dfe-4bf6-a0a0-882caaff0e9d\",\"simple\":{\"name\":\"Mike\",\"age\":27,\"isOk\":false,\"favorite\":125},\"allDone\":false}]":[{"id":"1e6c2b31-4dfe-4bf6-a0a0-882caaff0e9d","simple":{"name":"Mike","age":27,"isOk":false,"favorite":125},"allDone":false},{"id":"1e6c2b31-4dfe-4bf6-a0a0-882caaff0e9c","simple":{"name":"Larry","age":32,"isOk":true,"favorite":"golf"},"allDone":true}]}}""".asInstanceOf[JSON],js) - assertEquals(inst,sj.read[SampleTupleComplex](js)) - } - - test("Class having collections as members") { - val a = PolyClass(Map("a" -> 1, "b" -> 2), List("one", "two")) - val b = PolyClass(Map("x" -> 9, "y" -> 10), List("aye", "you")) - val t1 = (a, b) - val t2 = (b, a) - val inst = SampleTuplePolyClass(Map(t1 -> t2)) - val js = sj.render(inst) - assertEquals( - """{"m":{"[{\"lookup\":{\"a\":1,\"b\":2},\"favs\":[\"one\",\"two\"]},{\"lookup\":{\"x\":9,\"y\":10},\"favs\":[\"aye\",\"you\"]}]":[{"lookup":{"x":9,"y":10},"favs":["aye","you"]},{"lookup":{"a":1,"b":2},"favs":["one","two"]}]}}""".asInstanceOf[JSON],js) - assertEquals(inst,sj.read[SampleTuplePolyClass](js)) - } - - test("Class having collections as members (empty collections") { - val a = PolyClass(Map.empty[String, Int], List.empty[String]) - val b = PolyClass(Map.empty[String, Int], List.empty[String]) - val t1 = (a, b) - val t2 = (b, a) - val inst = Map(t1 -> t2) - val js = sj.render(inst) - assertEquals( - """{"[{\"lookup\":{},\"favs\":[]},{\"lookup\":{},\"favs\":[]}]":[{"lookup":{},"favs":[]},{"lookup":{},"favs":[]}]}""".asInstanceOf[JSON],js) - assertEquals(inst,sj.read[Map[(PolyClass, PolyClass), (PolyClass, PolyClass)]](js)) - } - - test("Custom trait hint field and value for key trait") { - val petHintMod = StringMatchHintModifier( - Map("BreathsWater" -> "co.blocke.scalajack.json.mapkeys.FishPet", "BreathsAir" -> "co.blocke.scalajack.json.mapkeys.DogPet") - ) - val sj2 = co.blocke.scalajack.ScalaJack() - .withHints(RType.of[Pet] -> "kind") - .withHintModifiers(RType.of[Pet] -> petHintMod) - val a: Pet = FishPet("Flipper", Food.Veggies, 74.33) - val b: Pet = DogPet("Fido", Food.Meat, 3) - val t1 = (a, b) - val t2 = (b, a) - val inst = Map(t1 -> t2) - val js = sj2.render(inst) - assertEquals( - """{"[{\"kind\":\"BreathsWater\",\"name\":\"Flipper\",\"food\":\"Veggies\",\"waterTemp\":74.33},{\"kind\":\"BreathsAir\",\"name\":\"Fido\",\"food\":\"Meat\",\"numLegs\":3}]":[{"kind":"BreathsAir","name":"Fido","food":"Meat","numLegs":3},{"kind":"BreathsWater","name":"Flipper","food":"Veggies","waterTemp":74.33}]}""".asInstanceOf[JSON],js) - assertEquals(inst,sj2.read[Map[(Pet, Pet), (Pet, Pet)]](js)) - } - - test("Custom trait hint field and value for key member's trait") { - val petHintMod = StringMatchHintModifier( - Map("BreathsWater" -> "co.blocke.scalajack.json.mapkeys.FishPet", "BreathsAir" -> "co.blocke.scalajack.json.mapkeys.DogPet") - ) - val sj2 = co.blocke.scalajack.ScalaJack() - .withHints(RType.of[Pet] -> "kind") - .withHintModifiers(RType.of[Pet] -> petHintMod) - - val a: PetHolder = - ShinyPetHolder("123 Main", FishPet("Flipper", Food.Veggies, 74.33)) - val b: PetHolder = - ShinyPetHolder("210 North", DogPet("Fido", Food.Meat, 3)) - val t1 = (a, b) - val t2 = (b, a) - val inst = Map(t1 -> t2) - val js = sj2.render(inst) - assertEquals( - """{"[{\"_hint\":\"co.blocke.scalajack.json.mapkeys.ShinyPetHolder\",\"address\":\"123 Main\",\"pet\":{\"kind\":\"BreathsWater\",\"name\":\"Flipper\",\"food\":\"Veggies\",\"waterTemp\":74.33}},{\"_hint\":\"co.blocke.scalajack.json.mapkeys.ShinyPetHolder\",\"address\":\"210 North\",\"pet\":{\"kind\":\"BreathsAir\",\"name\":\"Fido\",\"food\":\"Meat\",\"numLegs\":3}}]":[{"_hint":"co.blocke.scalajack.json.mapkeys.ShinyPetHolder","address":"210 North","pet":{"kind":"BreathsAir","name":"Fido","food":"Meat","numLegs":3}},{"_hint":"co.blocke.scalajack.json.mapkeys.ShinyPetHolder","address":"123 Main","pet":{"kind":"BreathsWater","name":"Flipper","food":"Veggies","waterTemp":74.33}}]}""".asInstanceOf[JSON],js) - assertEquals(inst,sj2.read[Map[(PetHolder, PetHolder), (PetHolder, PetHolder)]](js)) - } - - test("Parameterized class") { - val t1 = (AThing("wow", 4), AThing("boom", 1)) - val t2 = (AThing("yep", 3), AThing("yikes", 11)) - val inst = Map(t1 -> t2) - val js = sj.render(inst) - assertEquals( - """{"[{\"a\":\"wow\",\"b\":4},{\"a\":\"boom\",\"b\":1}]":[{"a":"yep","b":3},{"a":"yikes","b":11}]}""".asInstanceOf[JSON],js) - assertEquals(inst,sj.read[Map[(AThing[Int, String], AThing[Int, String]), (AThing[Int, String], AThing[Int, String])]](js)) - } - - test("Parameterized trait") { - type T1 = Thing[String,Int] - val t1: (Thing[String,Int], Thing[String,Int]) = - (AThing("wow", 4), AThing("boom", 1)) - val t2: (Thing[String,Int], Thing[String,Int]) = - (AThing("yep", 3), AThing("yikes", 11)) - val inst = Map(t1 -> t2) - val js = sj.render(inst) - assertEquals( - """{"[{\"_hint\":\"co.blocke.scalajack.json.mapkeys.AThing\",\"a\":\"wow\",\"b\":4},{\"_hint\":\"co.blocke.scalajack.json.mapkeys.AThing\",\"a\":\"boom\",\"b\":1}]":[{"_hint":"co.blocke.scalajack.json.mapkeys.AThing","a":"yep","b":3},{"_hint":"co.blocke.scalajack.json.mapkeys.AThing","a":"yikes","b":11}]}""".asInstanceOf[JSON],js) - assert(inst == sj.read[Map[(Thing[String,Int],Thing[String,Int]),(Thing[String,Int],Thing[String,Int])]](js)) - } - - test("Parameterized trait having parameterized trait members") { - val t1: (Thing[String, Part[Double]], Thing[String, Part[Double]]) = - (AThing("wow", APart(1.2)), AThing("boom", APart(2.3))) - val t2: (Thing[String, Part[Double]], Thing[String, Part[Double]]) = - (AThing("yep", APart(4.5)), AThing("yikes", APart(6.7))) - val inst = Map(t1 -> t2) - val js = sj.render(inst) - assertEquals( - """{"[{\"_hint\":\"co.blocke.scalajack.json.mapkeys.AThing\",\"a\":\"wow\",\"b\":{\"_hint\":\"co.blocke.scalajack.json.mapkeys.APart\",\"p\":1.2}},{\"_hint\":\"co.blocke.scalajack.json.mapkeys.AThing\",\"a\":\"boom\",\"b\":{\"_hint\":\"co.blocke.scalajack.json.mapkeys.APart\",\"p\":2.3}}]":[{"_hint":"co.blocke.scalajack.json.mapkeys.AThing","a":"yep","b":{"_hint":"co.blocke.scalajack.json.mapkeys.APart","p":4.5}},{"_hint":"co.blocke.scalajack.json.mapkeys.AThing","a":"yikes","b":{"_hint":"co.blocke.scalajack.json.mapkeys.APart","p":6.7}}]}""".asInstanceOf[JSON],js) - assert(inst == sj.read[Map[(Thing[String, Part[Double]], Thing[String, Part[Double]]), (Thing[String, Part[Double]], Thing[String, Part[Double]])]](js)) - } \ No newline at end of file diff --git a/core/src/test/scala/co.blocke.scalajack/json/mapkeys/ValueClassKeys.scala b/core/src/test/scala/co.blocke.scalajack/json/mapkeys/ValueClassKeys.scala deleted file mode 100644 index c021b807..00000000 --- a/core/src/test/scala/co.blocke.scalajack/json/mapkeys/ValueClassKeys.scala +++ /dev/null @@ -1,454 +0,0 @@ -package co.blocke.scalajack -package json.mapkeys - -import co.blocke.scala_reflection._ -import TestUtil._ -import munit._ -import munit.internal.console -import co.blocke.scalajack.json.JSON -import java.util.UUID -import co.blocke.scalajack.model._ - -class ValueClassKeys() extends FunSuite: - - val sj = co.blocke.scalajack.ScalaJack() - - test("Value class of BigDecimal") { - describe( - "------------------------------\n: ValueClass Map Key Tests :\n------------------------------", Console.BLUE - ) - describe("+++ Positive Primitive Tests +++") - - val inst = SampleVCBigDecimal( - Map( - VCBigDecimal(BigDecimal(12.34)) -> VCBigDecimal(BigDecimal(56.78)) - ) - ) - val js = sj.render(inst) - assertEquals("""{"m":{"12.34":56.78}}""".asInstanceOf[JSON],js) - assertEquals(inst, sj.read[SampleVCBigDecimal](js)) - } - - test("Value class of BigInt") { - val inst = - SampleVCBigInt(Map(VCBigInt(BigInt(12)) -> VCBigInt(BigInt(56)))) - val js = sj.render(inst) - assertEquals("""{"m":{"12":56}}""".asInstanceOf[JSON],js) - assertEquals(inst, sj.read[SampleVCBigInt](js)) - } - - test("Value class of Byte") { - val inst = SampleVCByte( - Map(VCByte(12.asInstanceOf[Byte]) -> VCByte(56.asInstanceOf[Byte])) - ) - val js = sj.render(inst) - assertEquals("""{"m":{"12":56}}""".asInstanceOf[JSON],js) - assertEquals(inst, sj.read[SampleVCByte](js)) - } - - test("Value class of Boolean") { - val inst = SampleVCBoolean(Map(VCBoolean(true) -> VCBoolean(false))) - val js = sj.render(inst) - assertEquals("""{"m":{"true":false}}""".asInstanceOf[JSON],js) - assertEquals(inst, sj.read[SampleVCBoolean](js)) - } - - test("Value class of Char") { - val inst = SampleVCChar(Map(VCChar('a') -> VCChar('A'))) - val js = sj.render(inst) - assertEquals("""{"m":{"a":"A"}}""".asInstanceOf[JSON],js) - assertEquals(inst, sj.read[SampleVCChar](js)) - } - - test("Value class of Double") { - val inst = SampleVCDouble(Map(VCDouble(12.34) -> VCDouble(56.78))) - val js = sj.render(inst) - assertEquals("""{"m":{"12.34":56.78}}""".asInstanceOf[JSON],js) - assertEquals(inst, sj.read[SampleVCDouble](js)) - } - - test("Value class of Enumeration") { - val inst = SampleVCEnumeration( - Map(VCEnumeration(Food.Veggies) -> VCEnumeration(Food.Meat)) - ) - val js = sj.render(inst) - assertEquals("""{"m":{"Veggies":"Meat"}}""".asInstanceOf[JSON],js) - assertEquals(inst, sj.read[SampleVCEnumeration](js)) - } - - test("Value class of Float") { - val inst = SampleVCFloat(Map(VCFloat(12.34F) -> VCFloat(56.2F))) - val js = sj.render(inst) - assertEquals("""{"m":{"12.34":56.2}}""".asInstanceOf[JSON],js) - assertEquals(inst, sj.read[SampleVCFloat](js)) - } - - test("Value class of Int") { - val inst = SampleVCInt(Map(VCInt(12) -> VCInt(56))) - val js = sj.render(inst) - assertEquals("""{"m":{"12":56}}""".asInstanceOf[JSON],js) - assertEquals(inst, sj.read[SampleVCInt](js)) - } - - test("Value class of Long") { - val inst = SampleVCLong(Map(VCLong(12L) -> VCLong(56L))) - val js = sj.render(inst) - assertEquals("""{"m":{"12":56}}""".asInstanceOf[JSON],js) - assertEquals(inst, sj.read[SampleVCLong](js)) - } - - test("Value class of Short") { - val inst = SampleVCShort( - Map( - VCShort(12.asInstanceOf[Short]) -> VCShort(56.asInstanceOf[Short]) - ) - ) - val js = sj.render(inst) - assertEquals("""{"m":{"12":56}}""".asInstanceOf[JSON],js) - assertEquals(inst, sj.read[SampleVCShort](js)) - } - - test("Value class of String") { - val inst = SampleVCString(Map(VCString("A") -> VCString("B"))) - val js = sj.render(inst) - assertEquals("""{"m":{"A":"B"}}""".asInstanceOf[JSON],js) - assertEquals(inst, sj.read[SampleVCString](js)) - } - - test("Value class of UUID") { - val inst = SampleVCUUID( - Map( - VCUUID(UUID.fromString("54cab778-7b9e-4b07-9d37-87b97a011e56")) -> VCUUID( - UUID.fromString("54cab778-7b9e-4b07-9d37-87b97a011e55") - ) - ) - ) - val js = sj.render(inst) - assertEquals( - """{"m":{"54cab778-7b9e-4b07-9d37-87b97a011e56":"54cab778-7b9e-4b07-9d37-87b97a011e55"}}""".asInstanceOf[JSON],js) - assertEquals(inst, sj.read[SampleVCUUID](js)) - } - - test("Value class of List") { - describe("+++ Positive Collection Tests +++") - - val inst = - SampleVCList(Map(VCList(List(1, 2, 3)) -> VCList(List(4, 5, 6)))) - val js = sj.render(inst) - assertEquals("""{"m":{"[1,2,3]":[4,5,6]}}""".asInstanceOf[JSON],js) - assertEquals(inst, sj.read[SampleVCList](js)) - } - - test("Value class of List (empty List)") { - val inst = - SampleVCList(Map(VCList(List.empty[Int]) -> VCList(List.empty[Int]))) - val js = sj.render(inst) - assertEquals("""{"m":{"[]":[]}}""".asInstanceOf[JSON],js) - assertEquals(inst, sj.read[SampleVCList](js)) - } - - test("Value class of Map") { - val inst = SampleVCMap(Map(VCMap(Map(1 -> 2)) -> VCMap(Map(3 -> 4)))) - val js = sj.render(inst) - assertEquals("""{"m":{"{\"1\":2}":{"3":4}}}""".asInstanceOf[JSON],js) - assertEquals(inst, sj.read[SampleVCMap](js)) - } - - test("Value class of Map (empty Map") { - val inst = SampleVCMap( - Map(VCMap(Map.empty[Int, Int]) -> VCMap(Map.empty[Int, Int])) - ) - val js = sj.render(inst) - assertEquals("""{"m":{"{}":{}}}""".asInstanceOf[JSON],js) - assertEquals(inst, sj.read[SampleVCMap](js)) - } - - test("Value class of Tupple") { - val inst = SampleVCTuple( - Map(VCTuple((1, "one", true)) -> VCTuple((2, "two", false))) - ) - val js = sj.render(inst) - assertEquals("""{"m":{"[1,\"one\",true]":[2,"two",false]}}""".asInstanceOf[JSON],js) - assertEquals(inst, sj.read[SampleVCTuple](js)) - } - - test("Value class of Case Class") { - describe("+++ Positive Complex Tests +++") - - val a = SimpleClass("Larry", 32, isOk = true, "golf") - val b = SimpleClass("Mike", 27, isOk = false, 125) - val c1 = ComplexClass( - UUID.fromString("1e6c2b31-4dfe-4bf6-a0a0-882caaff0e9c"), - a, - allDone = true - ) - val c2 = ComplexClass( - UUID.fromString("1e6c2b31-4dfe-4bf6-a0a0-882caaff0e9d"), - b, - allDone = false - ) - val inst = SampleVCClass(Map(VCClass(c1) -> VCClass(c2))) - val js = sj.render(inst) - assertEquals( - """{"m":{"{\"id\":\"1e6c2b31-4dfe-4bf6-a0a0-882caaff0e9c\",\"simple\":{\"name\":\"Larry\",\"age\":32,\"isOk\":true,\"favorite\":\"golf\"},\"allDone\":true}":{"id":"1e6c2b31-4dfe-4bf6-a0a0-882caaff0e9d","simple":{"name":"Mike","age":27,"isOk":false,"favorite":125},"allDone":false}}}""".asInstanceOf[JSON],js) - assertEquals(inst, sj.read[SampleVCClass](js)) - } - - test("Value class of Trait") { - val a: Pet = FishPet("Flipper", Food.Veggies, 74.33) - val b: Pet = DogPet("Fido", Food.Meat, 3) - val inst = SampleVCTrait(Map(VCTrait(a) -> VCTrait(b))) - val js = sj.render(inst) - assertEquals( - """{"m":{"{\"_hint\":\"co.blocke.scalajack.json.mapkeys.FishPet\",\"name\":\"Flipper\",\"food\":\"Veggies\",\"waterTemp\":74.33}":{"_hint":"co.blocke.scalajack.json.mapkeys.DogPet","name":"Fido","food":"Meat","numLegs":3}}}""".asInstanceOf[JSON],js) - assertEquals(inst, sj.read[SampleVCTrait](js)) - } - - test("Value class of Parameterized Case Class") { - val a = AThing(5, "wow") - val b = AThing(6, "zoom") - val inst = SampleVCParamClass(Map(VCParamClass(a) -> VCParamClass(b))) - val js = sj.render(inst) - assertEquals("""{"m":{"{\"a\":5,\"b\":\"wow\"}":{"a":6,"b":"zoom"}}}""".asInstanceOf[JSON],js) - assertEquals(inst, sj.read[SampleVCParamClass[String, Int]](js)) - } - - test("Value class of Parameterized Trait") { - val a: Thing[Int, String] = AThing(5, "wow") - val b: Thing[Int, String] = AThing(6, "zoom") - val inst = SampleVCParamTrait(Map(VCParamTrait(a) -> VCParamTrait(b))) - val js = sj.render(inst) - assertEquals( - """{"m":{"{\"_hint\":\"co.blocke.scalajack.json.mapkeys.AThing\",\"a\":5,\"b\":\"wow\"}":{"_hint":"co.blocke.scalajack.json.mapkeys.AThing","a":6,"b":"zoom"}}}""".asInstanceOf[JSON],js) - assertEquals(inst, sj.read[SampleVCParamTrait[Int, String]](js)) - } - - test("Value class of Option") { - val inst = - SampleVCOption(Map(VCOption(Some("here")) -> VCOption(Some("there")))) - val js = sj.render(inst) - assertEquals("""{"m":{"here":"there"}}""".asInstanceOf[JSON],js) - assertEquals(inst, sj.read[SampleVCOption](js)) - } - - test("Value class of nested collection") { - val a = VCNested(List(Map("a" -> "b"), Map("c" -> "d"))) - val b = VCNested(List(Map("t" -> "u"), Map("x" -> "y"))) - val inst = SampleVCNested(Map(a -> b)) - val js = sj.render(inst) - assertEquals( - """{"m":{"[{\"a\":\"b\"},{\"c\":\"d\"}]":[{"t":"u"},{"x":"y"}]}}""".asInstanceOf[JSON],js) - assertEquals(inst, sj.read[SampleVCNested](js)) - } - - test("Bad BigDecimal Key") { - describe("--- Negative Primitive Tests ---") - - val js = """{"m":{"true":56.78}}""".asInstanceOf[JSON] - val msg = """Expected a Number here - |true - |^""".stripMargin - interceptMessage[co.blocke.scalajack.ScalaJackError](msg){ - sj.read[SampleVCBigDecimal](js) - } - } - - test("Bad BigInt Key") { - val js = """{"m":{"12.5":56}}""".asInstanceOf[JSON] - interceptMessage[java.lang.NumberFormatException]("For input string: \"12.5\""){ - sj.read[SampleVCBigInt](js) - } - } - - test("Bad Boolean Key") { - val js = """{"m":{"1":false}}""".asInstanceOf[JSON] - val msg = """Expected a Boolean here - |1 - |^""".stripMargin - interceptMessage[co.blocke.scalajack.ScalaJackError](msg){ - sj.read[SampleVCBoolean](js) - } - } - - test("Bad Char Key") { - val js = """{"m":{null:"A"}}""".asInstanceOf[JSON] - val msg = """A Char typed value cannot be null - |{"m":{null:"A"}} - |---------^""".stripMargin - interceptMessage[co.blocke.scalajack.ScalaJackError](msg){ - sj.read[SampleVCChar](js) - } - } - - test("Bad Double Key") { - val js = """{"m":{"false":56.78}}""".asInstanceOf[JSON] - val msg = """Expected a Number here - |false - |^""".stripMargin - interceptMessage[co.blocke.scalajack.ScalaJackError](msg){ - sj.read[SampleVCDouble](js) - } - } - - test("Bad Enumeration Key") { - val js = """{"m":{"Bogus":"Meat"}}""".asInstanceOf[JSON] - val msg = - """No value found in enumeration co.blocke.scalajack.json.mapkeys.Food$ for Bogus - |{"m":{"Bogus":"Meat"}} - |------------^""".stripMargin - interceptMessage[co.blocke.scalajack.ScalaJackError](msg){ - sj.read[SampleVCEnumeration](js) - } - } - - test("Bad Float Key") { - val js = """{"m":{"hey":56.2}}""".asInstanceOf[JSON] - val msg = """Expected a Number here - |hey - |^""".stripMargin - interceptMessage[co.blocke.scalajack.ScalaJackError](msg){ - sj.read[SampleVCFloat](js) - } - } - - test("Bad Int Key") { - val js = """{"m":{"12.5":56}}""".asInstanceOf[JSON] - val msg = """Cannot parse an Int from value - |12.5 - |---^""".stripMargin - interceptMessage[co.blocke.scalajack.ScalaJackError](msg){ - sj.read[SampleVCInt](js) - } - } - - test("Bad Long Key") { - val js = """{"m":{"12.5":56}}""".asInstanceOf[JSON] - val msg = """Cannot parse an Long from value - |12.5 - |---^""".stripMargin - interceptMessage[co.blocke.scalajack.ScalaJackError](msg){ - sj.read[SampleVCLong](js) - } - } - - test("Bad Short Key") { - val js = """{"m":{"12.5":56}}""".asInstanceOf[JSON] - val msg = """Cannot parse an Short from value - |12.5 - |---^""".stripMargin - interceptMessage[co.blocke.scalajack.ScalaJackError](msg){ - sj.read[SampleVCShort](js) - } - } - - test("Bad String Key") { - val js = """{"m":{true:"B"}}""".asInstanceOf[JSON] - val msg = """Expected a String here - |{"m":{true:"B"}} - |------^""".stripMargin - interceptMessage[co.blocke.scalajack.ScalaJackError](msg){ - sj.read[SampleVCString](js) - } - } - - test("Bad UUID Key") { - val js = """{"m":{"bogus":"54cab778-7b9e-4b07-9d37-87b97a011e55"}}""".asInstanceOf[JSON] - val msg = """Failed to create UUID value from parsed text bogus - |{"m":{"bogus":"54cab778-7b9e-4b07-9d37-87b97a011e55"}} - |------------^""".stripMargin - interceptMessage[co.blocke.scalajack.ScalaJackError](msg){ - sj.read[SampleVCUUID](js) - } - } - - test("Bad List Key") { - describe("--- Negative Collection Tests ---") - - val js = """{"m":{"[1,2,\"a\"]":[4,5,6]}}""".asInstanceOf[JSON] - val msg = """Expected a Number here - |[1,2,"a"] - |-----^""".stripMargin - interceptMessage[co.blocke.scalajack.ScalaJackError](msg){ - sj.read[SampleVCList](js) - } - } - - test("Bad Map Key") { - val js = """{"m":{"{[true]:2}":{"3":4}}}""".asInstanceOf[JSON] - val msg = """Expected a String here - |{[true]:2} - |-^""".stripMargin - interceptMessage[co.blocke.scalajack.ScalaJackError](msg){ - sj.read[SampleVCMap](js) - } - } - - test("Bad Tuple Key") { - val js = """{"m":{"[1,\"one\",true,1]":[2,"two",false]}}""".asInstanceOf[JSON] - val msg = """Expected end of tuple here - |[1,"one",true,1] - |-------------^""".stripMargin - interceptMessage[co.blocke.scalajack.ScalaJackError](msg){ - sj.read[SampleVCTuple](js) - } - } - - test("Bad Case Class Key") { - describe("--- Negative Complex Tests ---") - - val js = - """{"m":{"{\"id\":\"1e6c2b31-4dfe-4bf6-a0a0-882caaff0e9c\",\"simple\":{\"bogus\":\"Larry\",\"age\":32,\"isOk\":true,\"favorite\":\"golf\"},\"allDone\":true}":{"id":"1e6c2b31-4dfe-4bf6-a0a0-882caaff0e9d","simple":{"name":"Mike","age":27,"isOk":false,"favorite":125},"allDone":false}}}""".asInstanceOf[JSON] - val msg = - """Class co.blocke.scalajack.json.mapkeys.SimpleClass missing required fields: name - |...s":"Larry","age":32,"isOk":true,"favorite":"golf"},"allDone":true} - |----------------------------------------------------^""".stripMargin - interceptMessage[co.blocke.scalajack.ScalaJackError](msg){ - sj.read[SampleVCClass](js) - } - } - - test("Bad Trait Key") { - val js = - """{"m":{"{\"_hint\":\"co.blocke.scalajack.json.mapkeys.Bogus\",\"name\":\"Flipper\",\"food\":\"Veggies\",\"waterTemp\":74.33}":{"_hint":"co.blocke.scalajack.json.mapkeys.DogPet","name":"Fido","food":"Meat","numLegs":3}}}""".asInstanceOf[JSON] - interceptMessage[java.lang.ClassNotFoundException]("co.blocke.scalajack.json.mapkeys.Bogus"){ - sj.read[SampleVCTrait](js) - } - } - - test("Bad Parameterized Case Class Key") { - val js = """{"m":{"{\"a\":5.5,\"b\":\"wow\"}":{"a":6,"b":"zoom"}}}""".asInstanceOf[JSON] - val msg = """Cannot parse an Int from value - |{"a":5.5,"b":"wow"} - |-------^""".stripMargin - interceptMessage[co.blocke.scalajack.ScalaJackError](msg){ - sj.read[SampleVCParamClass[String, Int]](js) - } - } - - test("Bad Parameterized Trait Key") { - val js = - """{"m":{"{\"_hint\":\"co.blocke.scalajack.json.mapkeys.ZThing\",\"a\":5,\"b\":\"wow\"}":{"_hint":"co.blocke.scalajack.mapkeys.AThing","a":6,"b":"zoom"}}}""".asInstanceOf[JSON] - interceptMessage[java.lang.ClassNotFoundException]("co.blocke.scalajack.json.mapkeys.ZThing"){ - sj.read[SampleVCParamTrait[Int, String]](js) - } - } - - test("Bad Option Key") { - val js = """{"m":{true:"there"}}""".asInstanceOf[JSON] - val msg = """Expected a String here - |{"m":{true:"there"}} - |------^""".stripMargin - interceptMessage[co.blocke.scalajack.ScalaJackError](msg){ - sj.read[SampleVCOption](js) - } - } - - test("Bad Nested Collection Key") { - val js = """{"m":{"[{\"a\":\"b\"},{\"c\":9}]":[{"t":"u"},{"x":"y"}]}}""".asInstanceOf[JSON] - val msg = """Expected a String here - |[{"a":"b"},{"c":9}] - |----------------^""".stripMargin - interceptMessage[co.blocke.scalajack.ScalaJackError](msg){ - sj.read[SampleVCNested](js) - } - } \ No newline at end of file diff --git a/core/src/test/scala/co.blocke.scalajack/json/parameters/ClassParams.scala b/core/src/test/scala/co.blocke.scalajack/json/parameters/ClassParams.scala deleted file mode 100644 index a23d4b69..00000000 --- a/core/src/test/scala/co.blocke.scalajack/json/parameters/ClassParams.scala +++ /dev/null @@ -1,103 +0,0 @@ -package co.blocke.scalajack -package json.parameters - -import TestUtil._ -import munit._ -import munit.internal.console -import co.blocke.scalajack.json.JSON -import co.blocke.scala_reflection._ - -class ClassParams() extends FunSuite: - - val sj = co.blocke.scalajack.ScalaJack() - - test("Simple parameters - Foo[A](x:A) where A -> simple type") { - describe("---------------------------------\n: Class Paramterization Tests :\n---------------------------------", Console.BLUE) - describe("Basic Parameterized Case Class") - - val inst = Foo1(false, 19) - val js = sj.render(inst) - assertEquals("""{"x":false,"b":19}""".asInstanceOf[JSON],js) - assertEquals(inst,sj.read[Foo1[Boolean]](js)) - } - - test("Non-parameter case class as a parameter - Foo[A](x:A) where A -> Bar (case clas)") { - val inst = Foo1(Bar1("Fred"), 19) - val js = sj.render(inst) - assertEquals("""{"x":{"name":"Fred"},"b":19}""".asInstanceOf[JSON],js) - assertEquals(inst,sj.read[Foo1[Bar1]](js)) - } - - test("Parameterized case class as parameter - Foo[A](x:A) where A -> Bar[Int]") { - describe("Advanced Parameterized Case Class") - - val inst = Foo1(Bar2(123L), 19) - val js = sj.render(inst) - assertEquals("""{"x":{"id":123},"b":19}""".asInstanceOf[JSON],js) - assertEquals(inst,sj.read[Foo1[Bar2[Long]]](js)) - } - - test("Value class as parameter - Foo[A](x:A) where A -> value class") { - val inst = Foo1(VC1("Wow"), 19) - val js = sj.render(inst) - assertEquals("""{"x":"Wow","b":19}""".asInstanceOf[JSON],js) - assertEquals(inst,sj.read[Foo1[VC1]](js)) - } - - test("Parameterized case class as a parameter - Foo[A](x:Bar[A])") { - val inst = Foo2(Bar2(123L), 19) - val js = sj.render(inst) - assertEquals("""{"x":{"id":123},"b":19}""".asInstanceOf[JSON],js) - assertEquals(inst,sj.read[Foo2[Long]](js)) - } - - test("Parameterized case class with parameterized another member - Foo[A](x:Bar[A], y:A)") { - val inst = Foo3(Bar2(List(1, 2, 3)), List(4, 5, 6)) - val js = sj.render(inst) - assertEquals("""{"x":{"id":[1,2,3]},"b":[4,5,6]}""".asInstanceOf[JSON],js) - assertEquals(inst,sj.read[Foo3[List[Int]]](js)) - } - - test("Class with two parameters, one given one not - Foo[A](x:List[Bar[A, Boolean]])") { - val inst = Foo4( - List( - Bar3(Map(4 -> "yes", 5 -> "no"), true), - Bar3(Map(8 -> "yellow", 9 -> "red"), false) - ), - Map(1 -> "wow", 2 -> "yup") - ) - val js = sj.render(inst) - assertEquals( - """{"x":[{"id":{"4":"yes","5":"no"},"isIt":true},{"id":{"8":"yellow","9":"red"},"isIt":false}],"b":{"1":"wow","2":"yup"}}""".asInstanceOf[JSON],js) - assertEquals(inst,sj.read[Foo4[Map[Int, String]]](js)) - } - - test("Multiple parameters, in order - Foo[A,B](x:Bar[A,B], y:A)") { - describe("Very Advanced Parameterized Case Class") - - val inst = Foo5( - List( - Bar3(Map(4 -> "yes", 5 -> "no"), true), - Bar3(Map(8 -> "yellow", 9 -> "red"), false) - ), - Map(1 -> "wow", 2 -> "yup") - ) - val js = sj.render(inst) - assertEquals( - """{"x":[{"id":{"4":"yes","5":"no"},"isIt":true},{"id":{"8":"yellow","9":"red"},"isIt":false}],"b":{"1":"wow","2":"yup"}}""".asInstanceOf[JSON],js) - assertEquals(inst,sj.read[Foo5[Map[Int, String], Boolean]](js)) - } - - test("Multiple parameters, out of order - Foo[A,B,C,D](x:Bar[C,D,A], y:B)") { - val inst = Foo6(Bar4(5, 2.5, 'H'), "wow") - val js = sj.render(inst) - assertEquals("""{"x":{"id":5,"thing1":2.5,"thing2":"H"},"y":"wow"}""".asInstanceOf[JSON],js) - assertEquals(inst,sj.read[Foo6[Double, String, Int, Char]](js)) - } - - test("Nested multiple parameters, out of order - Foo[A,B,C,D](x:Bar[C,Blah[D,A]], y:B)") { - val inst = Foo7(Bar5(5, Blah(2.5, 'H')), "wow") - val js = sj.render(inst) - assertEquals("""{"x":{"id":5,"blah":{"t":2.5,"u":"H"}},"y":"wow"}""".asInstanceOf[JSON],js) - assertEquals(inst,sj.read[Foo7[Double, String, Char, Int]](js)) - } \ No newline at end of file diff --git a/core/src/test/scala/co.blocke.scalajack/json/parameters/Model.scala b/core/src/test/scala/co.blocke.scalajack/json/parameters/Model.scala deleted file mode 100644 index e90fa9ad..00000000 --- a/core/src/test/scala/co.blocke.scalajack/json/parameters/Model.scala +++ /dev/null @@ -1,60 +0,0 @@ -package co.blocke.scalajack -package json.parameters - -//--- Basic Parameterized Case Class -case class Foo1[A](x: A, b: Int) -case class Bar1(name: String) - -//--- Advanced Parameterized Case Class -case class Bar2[X](id: X) -case class VC1(s: String) extends AnyVal -case class Foo2[A](x: Bar2[A], b: Int) -case class Foo3[A](x: Bar2[A], b: A) -case class Bar3[X, Y](id: X, isIt: Y) -case class Foo4[A](x: List[Bar3[A, Boolean]], b: A) - -//--- Very Advanced Parameterized Case Class -case class Foo5[A, B](x: List[Bar3[A, B]], b: A) -case class Foo6[A, B, C, D](x: Bar4[C, D, A], y: B) -case class Bar4[X, Y, Z](id: X, thing1: Z, thing2: Y) -case class Blah[T, U](t: T, u: U) -case class Foo7[A, B, C, D](x: Bar5[C, D, A], y: B) -case class Bar5[X, Y, Z](id: Y, blah: Blah[Z, X]) - -//--- Basic Parameterized Trait -trait T1[X] { val x: X } -case class TFoo1[A](x: A, b: Int) extends T1[A] -trait T2 { val name: String } -case class TBar1(name: String) extends T2 - -//--- Advanced Parameterized Trait -trait T3[X] { val thing: X } -case class TBar2(thing: Boolean) extends T3[Boolean] -case class TBar3[T](thing: T) extends T3[T] -trait T4[X] { val x: TBar3[X] } -case class TFoo2[A](x: TBar3[A], b: A) extends T4[A] -trait T5[X, Y] { val thing1: X; val thing2: Y } -case class TBar4[T](thing1: T, thing2: String) extends T5[T, String] -trait T6[X] { val x: List[T5[X, String]] } -case class TFoo3[A](x: List[T5[A, String]]) extends T6[A] - -//--- Very Advanced Parameterized Trait -trait T7[X, Y] { val x: T5[X, Y]; val b: X } -case class TBar5[T, U](thing1: T, thing2: U) extends T5[T, U] -case class TFoo4[A, B](x: T5[A, B], b: A) extends T7[A, B] - -trait T8[W, X, Y, Z] { val x: T9[Y, Z, W]; val y: X } -trait T9[T, U, V] { val pi: T; val po: U; val pu: V } -case class TBar6[A, B, C](pi: A, po: B, pu: C) extends T9[A, B, C] -case class TFoo5[A, B, C, D](x: T9[C, D, A], y: B) extends T8[A, B, C, D] - -// Foo[A,B,C,D](x:Bar[C,Blah[D,A]], y:B) -trait T10[X, Y] { val x: X; val y: Y } -trait T11[W, Z] { val w: W; val z: Z } -case class TBlah1[A, B](w: A, z: B) extends T11[A, B] -case class TBar7[A, B](thing1: A, thing2: B) extends T5[A, B] -case class TFoo6[A, B, C, D](x: T11[C, T5[D, A]], y: B) - extends T10[T11[C, T5[D, A]], B] - -trait Pet[T]{ val name: String; val other: T } -case class Dog[X](name: String, numLegs: Int, other: X) extends Pet[X] \ No newline at end of file diff --git a/core/src/test/scala/co.blocke.scalajack/json/parameters/TraitParams.scala b/core/src/test/scala/co.blocke.scalajack/json/parameters/TraitParams.scala deleted file mode 100644 index a8889379..00000000 --- a/core/src/test/scala/co.blocke.scalajack/json/parameters/TraitParams.scala +++ /dev/null @@ -1,143 +0,0 @@ -package co.blocke.scalajack -package json.parameters - -import TestUtil._ -import munit._ -import munit.internal.console -import co.blocke.scalajack.json.JSON -import co.blocke.scala_reflection._ - -class TraitParams() extends FunSuite: - - val sj = co.blocke.scalajack.ScalaJack() - - test("Simple parameters - Foo[A](x:A) where A -> simple type") { - describe("---------------------------------\n: Trait Paramterization Tests :\n---------------------------------", Console.BLUE) - describe("Basic Parameterized Trait") - - val inst: T1[Boolean] = TFoo1(false, 19) - val js = sj.render[T1[Boolean]](inst) - assertEquals("""{"_hint":"co.blocke.scalajack.json.parameters.TFoo1","x":false,"b":19}""".asInstanceOf[JSON],js) - assertEquals(inst,sj.read[T1[Boolean]](js)) - } - - test("Non-parameter trait as a parameter - Foo[A](x:A) where A -> Bar (case class)") { - val inst: T1[T2] = TFoo1(TBar1("Fred"), 19) - val js = sj.render[T1[T2]](inst) - assertEquals( - """{"_hint":"co.blocke.scalajack.json.parameters.TFoo1","x":{"_hint":"co.blocke.scalajack.json.parameters.TBar1","name":"Fred"},"b":19}""".asInstanceOf[JSON],js) - assert(inst == sj.read[TFoo1[TBar1]](js)) - } - - /* - test("Parameterized trait as parameter - Foo[A](x:A) where A -> Bar[Int]") { - describe("Advanced Parameterized trait") - - val inst: T1[TBar2] = TFoo1(TBar2(true), 19) - val js = sj.render(inst) - assertEquals( - """{"_hint":"co.blocke.scalajack.json.parameters.TFoo1","x":{"thing":true},"b":19}""".asInstanceOf[JSON],js) - assert(inst == sj.read[T1[TBar2]](js)) - } - - test("Value class as parameter - Foo[A](x:A) where A -> value class") { - val inst: T1[VC1] = TFoo1(VC1("Wow"), 19) - val js = sj.render(inst) - assertEquals("""{"_hint":"co.blocke.scalajack.json.parameters.TFoo1","x":"Wow","b":19}""".asInstanceOf[JSON],js) - assert(inst == sj.read[T1[VC1]](js)) - } - - test("Parameterized trait as a parameter - Foo[A](x:Bar[A])") { - val inst: T1[T3[Boolean]] = TFoo1(TBar3(false), 19) - val js = sj.render(inst) - assertEquals( - """{"_hint":"co.blocke.scalajack.json.parameters.TFoo1","x":{"_hint":"co.blocke.scalajack.json.parameters.TBar3","thing":false},"b":19}""".asInstanceOf[JSON],js) - assert(inst == sj.read[T1[T3[Boolean]]](js)) - } - - test("Parameterized trait with parameterized another member - Foo[A](x:Bar[A], y:A)") { - val inst: T4[Boolean] = TFoo2(TBar3(false), true) - val js = sj.render(inst) - assertEquals( - """{"_hint":"co.blocke.scalajack.json.parameters.TFoo2","x":{"thing":false},"b":true}""".asInstanceOf[JSON],js) - assert(inst == sj.read[T4[Boolean]](js)) - } - - test("Trait with two parameters, one given one not - Foo[A](x:List[Bar[A, Boolean]])") { - val inst: T6[Int] = TFoo3(List(TBar4(5, "five"), TBar4(6, "six"))) - val js = sj.render(inst) - assertEquals( - """{"_hint":"co.blocke.scalajack.json.parameters.TFoo3","x":[{"_hint":"co.blocke.scalajack.json.parameters.TBar4","thing1":5,"thing2":"five"},{"_hint":"co.blocke.scalajack.json.parameters.TBar4","thing1":6,"thing2":"six"}]}""".asInstanceOf[JSON],js) - assert(inst == sj.read[T6[Int]](js)) - } - - test("Multiple parameters, in order - Foo[A,B](x:Bar[A,B], y:A)") { - describe("Very Advanced Parameterized Trait") - - val inst: T7[Long, String] = TFoo4(TBar5(123L, "wow"), 456L) - val js = sj.render(inst) - assertEquals( - """{"_hint":"co.blocke.scalajack.json.parameters.TFoo4","x":{"_hint":"co.blocke.scalajack.json.parameters.TBar5","thing1":123,"thing2":"wow"},"b":456}""".asInstanceOf[JSON],js) - assert(inst == sj.read[T7[Long, String]](js)) - } - - test("Multiple parameters, out of order - Foo[A,B,C,D](x:Bar[C,D,A], y:B)") { - val inst: T8[Char, String, Int, Double] = - TFoo5(TBar6(5, 2.5, 'H'), "wow") - val js = sj.render(inst) - assertEquals( - """{"_hint":"co.blocke.scalajack.json.parameters.TFoo5","x":{"_hint":"co.blocke.scalajack.json.parameters.TBar6","pi":5,"po":2.5,"pu":"H"},"y":"wow"}""".asInstanceOf[JSON],js) - assert(inst == sj.read[T8[Char, String, Int, Double]](js)) - } - - test("Multiple parameters, out of order - Foo[A,B,C,D](x:Bar[C,D,A], y:B)") { - val inst: T8[Char, String, Int, Double] = - TFoo5(TBar6(5, 2.5, 'H'), "wow") - val js = sj.render(inst) - assertEquals( - """{"_hint":"co.blocke.scalajack.json.parameters.TFoo5","x":{"_hint":"co.blocke.scalajack.json.parameters.TBar6","pi":5,"po":2.5,"pu":"H"},"y":"wow"}""".asInstanceOf[JSON],js) - assert(inst == sj.read[T8[Char, String, Int, Double]](js)) - } - - test("Nested multiple parameters, out of order - Foo[A,B,C,D](x:Bar[C,Blah[D,A]], y:B)") { - val rt = RType.of[T10[T11[Int, T5[Double, Char]], String]] - val inst: T10[T11[Int, T5[Double, Char]], String] = TFoo6(TBlah1(5, TBar7(1.2, 'Z')), "wow") - val js = sj.render(inst) - assertEquals( - """{"_hint":"co.blocke.scalajack.json.parameters.TFoo6","x":{"_hint":"co.blocke.scalajack.json.parameters.TBlah1","w":5,"z":{"_hint":"co.blocke.scalajack.json.parameters.TBar7","thing1":1.2,"thing2":"Z"}},"y":"wow"}""".asInstanceOf[JSON],js) - val z = sj.read[T10[T11[Int, T5[Double, Char]], String]](js) - assertEquals(inst, sj.read[T10[T11[Int, T5[Double, Char]], String]](js)) - } - */ - - /* Performance tests - var rtx: RType = null - var jsx: JSON = null.asInstanceOf[JSON] - var zx: T10[T11[Int, T5[Double, Char]], String] = null - test("a") { - rtx = Reflector.reflectOn[T10[T11[Int, T5[Double, Char]], String]] - } - test("b") { - val inst: T10[T11[Int, T5[Double, Char]], String] = TFoo6(TBlah1(5, TBar7(1.2, 'Z')), "wow") - jsx = sj.render(inst) - } - test("c") { - zx = sj.read[T10[T11[Int, T5[Double, Char]], String]](jsx) - } - test("a.2") { - rtx = Reflector.reflectOn[T10[T11[Int, T5[Double, Char]], String]] - } - test("b.2") { - val inst: T10[T11[Int, T5[Double, Char]], String] = TFoo6(TBlah1(5, TBar7(1.2, 'Z')), "wow") - jsx = sj.render(inst) - } - test("c.2") { - zx = sj.read[T10[T11[Int, T5[Double, Char]], String]](jsx) - } - test("Z") { - val inst: Pet[Boolean] = Dog("Fido",4,true) - val js = sj.render(inst) - sj.read[Pet[Boolean]](js) - } - */ - diff --git a/core/src/test/scala/co.blocke.scalajack/json/plainclass/Inheritance.scala b/core/src/test/scala/co.blocke.scalajack/json/plainclass/Inheritance.scala deleted file mode 100644 index 5aacc68b..00000000 --- a/core/src/test/scala/co.blocke.scalajack/json/plainclass/Inheritance.scala +++ /dev/null @@ -1,141 +0,0 @@ -package co.blocke.scalajack -package json -package plainclass - -import co.blocke.scalajack.model.ClassNameHintModifier -import co.blocke.scala_reflection._ -import TestUtil._ -import munit._ -import munit.internal.console -import co.blocke.scalajack.json.JSON -import JsonMatcher._ - - -class Inheritance() extends FunSuite: - - val sj = co.blocke.scalajack.ScalaJack() - - test("Simple class inheritance must work (all fields present)") { - describe( - "-------------------------------------\n: Inheritance Tests (Plain Class) :\n-------------------------------------", Console.BLUE - ) - describe("Scala Plain") - - val js = - """{"uno":"foo","foobar":99,"dontForget":12,"three":false,"quatro":12.34,"foo":25,"extra":"bar"}""".asInstanceOf[JSON] - val simple = sj.read[InheritSimpleChild](js) - assertEquals(simple.one, "foo") - assertEquals(simple.extra, "bar") - assertEquals(simple.foo, 25) - assertEquals(simple.two, 99) - assertEquals(simple.three, false) - assertEquals(simple.four, 12.34) - // Need this matching because JSON order is often different - assert(JsonMatcher.jsonMatches(js, sj.render(simple))) - } - - test("MapName, and Ignore annotations must be inherited properly") { - val adapter = sj.taCache - .typeAdapterOf[InheritSimpleChild] - .asInstanceOf[co.blocke.scalajack.typeadapter.classes.NonCaseClassTypeAdapter[_]] - assertEquals( adapter.dbKeys.map(f => (f.name, f.dbKeyIndex)), - List( - ("uno", Some(0)), - ("foobar", Some(1)), - ("quatro", Some(2)), - ("foo", Some(99)) - ) - ) - val inst = new InheritSimpleChild("thing1", "thing2") - val js = sj.render(inst) - // Need this matching because JSON order is often different - assert(JsonMatcher.jsonMatches(js, """{"extra":"thing1","uno":"thing2","foo":39,"dontForget":9,"quatro":0.1,"three":true,"foobar":5}""".asInstanceOf[JSON])) - } - - test("With type parameter") { - val js = """{"thing":5, "item": 15, "cosa": 99}""".asInstanceOf[JSON] - val inst = sj.read[ParamChild[Int]](js) - assertEquals(inst.thing, 5) - assertEquals(inst.item, 15) - assertEquals(inst.cosa, 99) - assert(JsonMatcher.jsonMatches(sj.render(inst), """{"thing":5,"cosa":99,"item":15}""".asInstanceOf[JSON])) - } - - test("With type member (as part of a trait)") { - val inst = new WrapTrait[TraitBase]() - val flower = new Flower(5, 6) - inst.rose = flower - val js = sj.render(inst) - assertEquals(js, - """{"flower":"co.blocke.scalajack.json.plainclass.Flower","rose":{"thing":5,"other":6}}""".asInstanceOf[JSON] - ) - val inst2 = sj.read[WrapTrait[TraitBase]](js) - assert(inst2.rose.thing == flower.thing && inst2.rose.other == flower.other) - } - - test("Must catch missing/required var") { - describe("Scala Plain Negative") - - val js = - """{"extra":"bar","foo":25,"dontForget":12,"uno":"something","quatro":12.34}""".asInstanceOf[JSON] - val msg = """Class co.blocke.scalajack.json.plainclass.InheritSimpleChild missing required fields: foobar - |...,"dontForget":12,"uno":"something","quatro":12.34} - |----------------------------------------------------^""".stripMargin - interceptMessage[co.blocke.scalajack.ScalaJackError](msg){ - sj.read[InheritSimpleChild](js) - } - } - - test("Must catch missing/required constructor field (with newline)") { - val js = - """{"extra":"bar","foo":25,"dontForget":12,"quatro":12.34,"foobar":99}""".asInstanceOf[JSON] - val msg = - """Class co.blocke.scalajack.json.plainclass.InheritSimpleChild missing required fields: uno - |...oo":25,"dontForget":12,"quatro":12.34,"foobar":99} - |----------------------------------------------------^""".stripMargin - interceptMessage[co.blocke.scalajack.ScalaJackError](msg){ - sj.read[InheritSimpleChild](js) - } - } - - test("Must catch missing/required getter/setter field") { - val js = - """{"extra":"bar","foo":25,"uno":"something","quatro":12.34,"foobar":99}""".asInstanceOf[JSON] - val msg = - """Class co.blocke.scalajack.json.plainclass.InheritSimpleChild missing required fields: dontForget - |...":25,"uno":"something","quatro":12.34,"foobar":99} - |----------------------------------------------------^""".stripMargin - interceptMessage[co.blocke.scalajack.ScalaJackError](msg){ - sj.read[InheritSimpleChild](js) - } - } - - // NOTE: We can't test this because the exception is tossed at compile-time. (It will indeed throw this exception, however.) - //---------- - // test("Must fail non-val constructor field") { - // val f = new Fail4(1, 2) - // interceptMessage[co.blocke.scala_reflection.ReflectException]("""Class [co.blocke.scalajack.json.plainclass.Fail4]: Non-case class constructor arguments must all be 'val'"""){ - // sj.render(f) - // } - // } - - test( - "Simple class inheritance must work (all fields present) including MapName and Ignore" - ) { - describe("Java Plain") - - val js = """{"three":3,"dos":1}""".asInstanceOf[JSON] - val simple = sj.read[JavaSimpleChild](js) - assertEquals(simple.getTwo, 1) - assertEquals(simple.getThree, 3) - assertEquals(simple.getBogus, -1) - assertEquals(sj.render(simple), js) - } - - test("Optional annotation must be inherited properly") { - val js = """{"dos":1}""".asInstanceOf[JSON] - val simple = sj.read[JavaSimpleChild](js) - assertEquals(simple.getTwo, 1) - assertEquals(simple.getThree, -10) - assertEquals(sj.render(simple), """{"three":-10,"dos":1}""".asInstanceOf[JSON]) - } \ No newline at end of file diff --git a/core/src/test/scala/co.blocke.scalajack/json/plainclass/Misc.scala b/core/src/test/scala/co.blocke.scalajack/json/plainclass/Misc.scala deleted file mode 100644 index d62aeb39..00000000 --- a/core/src/test/scala/co.blocke.scalajack/json/plainclass/Misc.scala +++ /dev/null @@ -1,81 +0,0 @@ -package co.blocke.scalajack -package json.plainclass - -import co.blocke.scalajack.model.ClassNameHintModifier -import co.blocke.scala_reflection._ -import TestUtil._ -import munit._ -import munit.internal.console -import co.blocke.scalajack.json.JSON - - -trait Distance extends Any -case class Meter(val value: Double) extends AnyVal with Distance - - -class Misc() extends FunSuite: - - val sj = co.blocke.scalajack.ScalaJack() - - test("Read/write null into object") { - describe("------------------------\n: Misc Tests (Plain) :\n------------------------", Console.BLUE) - - assert(null == sj.read[PlayerMix]("null".asInstanceOf[JSON]) ) - assert("null".asInstanceOf[JSON] == sj.render[PlayerMix](null) ) - } - - test("Handles type members with modifier") { - val prependHintMod = ClassNameHintModifier( - (hint: String) => "co.blocke.scalajack.json.plainclass." + hint, - (cname: String) => cname.split('.').last - ) - val sj2 = co.blocke.scalajack.ScalaJack().withTypeValueModifier(prependHintMod) - val js = """{"flower":"Flower","rose":{"thing":5,"other":6}}""".asInstanceOf[JSON] - val inst = sj2.read[WrapTrait[TraitBase]](js) - assert(inst.rose.isInstanceOf[Flower]) - assertEquals(sj2.render(inst), js) - } - - test("Fails if no hint for type member") { - val js = """{"rose":{"thing":5,"other":6}}""".asInstanceOf[JSON] - val msg = - """Did not find required type member(s): flower - |{"rose":{"thing":5,"other":6}} - |^""".stripMargin - interceptMessage[co.blocke.scalajack.ScalaJackError](msg){ - sj.read[WrapTrait[TraitBase]](js) - } - } - - test("Must accept missing default constructor values") { - val js = """{"foobar":3, "quatro":4, "dontForget":1}""".asInstanceOf[JSON] - val inst = sj.read[InheritSimpleBase](js) - assertEquals(inst.one, "blather") - } - - test("Must accept missing optional constructor values") { - val js = """{"b":3}""".asInstanceOf[JSON] - val inst = sj.read[OptConst](js) - assertEquals(inst.a, None) - assertEquals(inst.b, Some(3)) - } - - test("Must ignore unneeded type members") { - val inst = new UnneededType[String]() - inst.a = 9 - assertEquals(sj.render(inst), """{"a":9}""".asInstanceOf[JSON]) - } - - test("Must require Java classes to have an empty constructor") { - val inst = new Unsupported("Foo") - interceptMessage[co.blocke.scalajack.ScalaJackError]("""ScalaJack does not support Java classes with a non-empty constructor."""){ - sj.render(inst) - } - } - - test("Must handle Change on Java setter") { - val js = """{"dos":9}""".asInstanceOf[JSON] - val inst = sj.read[OnSetter](js) - assertEquals(inst.getTwo, 9) - assertEquals(sj.render(inst), js) - } diff --git a/core/src/test/scala/co.blocke.scalajack/json/plainclass/Model.scala b/core/src/test/scala/co.blocke.scalajack/json/plainclass/Model.scala deleted file mode 100644 index 0300e78a..00000000 --- a/core/src/test/scala/co.blocke.scalajack/json/plainclass/Model.scala +++ /dev/null @@ -1,132 +0,0 @@ -package co.blocke.scalajack -package json.plainclass - -import co.blocke.scalajack.SJCapture -import co.blocke.scala_reflection._ -import co.blocke.scalajack._ - -import scala.util._ - - -class InheritSimpleBase( - @DBKey(index = 50)@Change(name = "bogus") val one:String= "blather" -) { - // Public data member - @DBKey(index = 1) @Change(name = "foobar") var two: Int = 5 - @Optional var three: Boolean = true - - // Private var or val - val notOne: Int = 2 - - @Ignore var dontseeme: Int = 90 - - // Scala-style getter/setter - private var _four: Double = 0.1 - @DBKey(index = 2) def four: Double = _four - @Change(name = "quatro") def four_=(a: Double): Unit = _four = a - - private var _dontForget: Int = 9 - def dontForget: Int = _dontForget - def dontForget_=(a: Int): Unit = _dontForget = a - - private var _unused: Double = 0.1 - @Ignore def unused: Double = _unused - def unused_=(a: Double): Unit = _unused = a -} - -class InheritSimpleChild( - val extra: String, - @DBKey @Change(name = "uno") override val one:String) - extends InheritSimpleBase(one) { - @DBKey(index = 99) var foo: Int = 39 - @Ignore var bogus: String = "" - - private var _nada: Double = 0.1 - def nada: Double = _nada - @Ignore def nada_=(a: Double): Unit = _nada = a -} - -// --- - -class ParamBase[T](val thing: T) { - var item: T = null.asInstanceOf[T] - - private var _cosa: T = null.asInstanceOf[T] - def cosa: T = _cosa - def cosa_=(a: T): Unit = _cosa = a -} - -class ParamChild[T](override val thing: T) extends ParamBase[T](thing) - -// --- - -trait TraitBase { - val thing: Int - val other: Int -} - -class Flower(val thing: Int, val other: Int) extends TraitBase - -class WrapTrait[T <: TraitBase]() { - type flower = T - var rose: T = null.asInstanceOf[T] - // IMPORTANT! rose must be of type T, not "flower". flower is the label for the external type in JSON -} - -// --- - -// class Fail4(val a: Int, b: Int) - -// -- - -class OptConst(val a: Option[Int]) { - var b: Option[Int] = Some(3) -} - -class UnneededType[T]() { - type item = T - - val m: T = null.asInstanceOf[item] - var a: Int = 5 -} - -//------------------------------------------------------ -case class VCDouble(vc: Double) extends AnyVal -class PlayerMix() { - def someConfusingThing() = true - var name: String = "" // public var member - var maybe: Option[Int] = Some(1) // optional member - - @Ignore var bogus: String = "" - - private var _age: VCDouble = VCDouble(0.0) - def age: VCDouble = _age // getter/setter member - def age_=(a: VCDouble): Unit = _age = a -} - -class BigPlayer() extends PlayerMix { - var more: Int = 0 -} - -// class NotAllVals(val a: Int, b: Int, val c: Int) - -class Embed() { - var stuff: List[String] = List.empty[String] - var num: Int = 0 -} -class Boom() { - var name: String = "" - var other: Try[Embed] = Success(null) -} - -class Cap() extends SJCapture { - var name: String = "" -} - -case class CaseCap(name: String) extends SJCapture - - -case class One(vc: List[VCDouble]) -class Two(val vc: VCDouble) { - var vcx: VCDouble = VCDouble(54.32) -} \ No newline at end of file diff --git a/core/src/test/scala/co.blocke.scalajack/json/plainclass/TryAndCapture.scala b/core/src/test/scala/co.blocke.scalajack/json/plainclass/TryAndCapture.scala deleted file mode 100644 index 92a97c0c..00000000 --- a/core/src/test/scala/co.blocke.scalajack/json/plainclass/TryAndCapture.scala +++ /dev/null @@ -1,80 +0,0 @@ -package co.blocke.scalajack -package json -package plainclass - -import co.blocke.scala_reflection._ -import TestUtil._ -import munit._ -import munit.internal.console -import co.blocke.scalajack.json.JSON -import scala.util.{Try, Success, Failure} -import JsonMatcher._ - - -class TryAndCapture() extends FunSuite: - - val sj = co.blocke.scalajack.ScalaJack() - - test("Try sucess") { - describe("-----------------------------------------\n: Try and Capture Tests (Plain class) :\n-----------------------------------------", Console.BLUE) - describe("Try:") - - val js = """{"name":"Greg","other":{"stuff":["a","b","c"],"num":2}}""".asInstanceOf[JSON] - val obj = sj.read[Boom](js) - assert( - obj.name == "Greg" && obj.other - .asInstanceOf[Success[Embed]] - .get - .num == 2 - ) - assert( - jsonMatches("""{"other":{"num":2,"stuff":["a","b","c"]},"name":"Greg"}""".asInstanceOf[JSON], sj.render(obj) )) - } - - test("Try failure") { - val js = """{"name":"Greg","other":[1,2,3]}""".asInstanceOf[JSON] - val obj = sj.read[Boom](js) - assertEquals("""Expected start of object here - |{"name":"Greg","other":[1,2,3]} - |-----------------------^""".stripMargin, - obj.other.asInstanceOf[Failure[_]].exception.getMessage) - assert(jsonMatches("""{"other":[1,2,3],"name":"Greg"}""".asInstanceOf[JSON], sj.render(obj) )) - } - - test("Try failure 2") { - val js = """{"name":"Greg","other": -12.45 ,"num":2}""".asInstanceOf[JSON] - val obj = sj.read[Boom](js) - assertEquals("""Expected start of object here - |{"name":"Greg","other": -12.45 ,"num":2} - |-------------------------^""".stripMargin, - obj.other.asInstanceOf[Failure[_]].exception.getMessage) - assert(jsonMatches("""{"other":-12.45,"name":"Greg"}""".asInstanceOf[JSON], sj.render(obj) )) - } - - test("Plain-class capture can write semantically equivalent JSON") { - describe("Capture:") - val js = - """{"name":"Greg", "foo":[1,2,"t" ], "zing": {"dot":{"age":25,"food":"Pizza"}}, "blather":"wow", "boo": -29384.34, "maybe": false }""".asInstanceOf[JSON] - val h = sj.read[Cap](js) - assertEquals(h.name,"Greg") - val js2 = sj.render(h) - assert( jsonMatches(js2,js) ) - } - - test("Case class capture can write semantically equivalent JSON") { - val js = - """{"name":"Greg", "foo":[1,2,"t" ], "zing" : {"_hint":"a.b.com.Hey", "dot":{"age":25,"food":"Pizza"}}, "blather":"wow", "boo": -29384.34, "maybe": false }""".asInstanceOf[JSON] - val h = sj.read[CaseCap](js) - assertEquals(h.name, "Greg") - val js2 = sj.render(h) - assert( jsonMatches(js2,js) ) - } - - test("Java class capture can write semantically equivalent JSON") { - val js = - """{"name":"Greg", "foo":[1,2,"t" ], "zing" : {"_hint":"a.b.com.Hey", "dot":{"age":25,"food":"Pizza"}}, "blather":"wow", "boo": -29384.34, "maybe": false }""".asInstanceOf[JSON] - val h = sj.read[JavaCap](js) - assertEquals(h.getName ,"Greg") - val js2 = sj.render(h) - assert( jsonMatches(js2,js) ) - } diff --git a/core/src/test/scala/co.blocke.scalajack/json/plainclass/ValueClass.scala b/core/src/test/scala/co.blocke.scalajack/json/plainclass/ValueClass.scala deleted file mode 100644 index f9e8ee7e..00000000 --- a/core/src/test/scala/co.blocke.scalajack/json/plainclass/ValueClass.scala +++ /dev/null @@ -1,31 +0,0 @@ -package co.blocke.scalajack -package json -package plainclass - -import co.blocke.scalajack.model.ClassNameHintModifier -import co.blocke.scala_reflection._ -import TestUtil._ -import munit._ -import munit.internal.console -import co.blocke.scalajack.json.JSON -import JsonMatcher._ - - -class ValueClass() extends FunSuite: - - val sj = co.blocke.scalajack.ScalaJack() - - test("Value class of Double") { - describe( - "----------------------------------------------\n: ValueClass DelimSpec Tests (Plain Class) :\n----------------------------------------------", Console.BLUE - ) - - val p1 = new PlayerMix() - p1.name = "Mike" - p1.age = VCDouble(BigDecimal("1.23").toDouble) - val js = sj.render(p1) - val r = sj.read[PlayerMix](js) - assert(jsonMatches("""{"age":1.23,"maybe":1,"name":"Mike"}""".asInstanceOf[JSON], js )) - assertEquals(p1.name, r.name) - assertEquals(p1.age, r.age) - } diff --git a/core/src/test/scala/co.blocke.scalajack/json/primitives/AnyPrim.scala b/core/src/test/scala/co.blocke.scalajack/json/primitives/AnyPrim.scala deleted file mode 100644 index 467e2a32..00000000 --- a/core/src/test/scala/co.blocke.scalajack/json/primitives/AnyPrim.scala +++ /dev/null @@ -1,159 +0,0 @@ -package co.blocke.scalajack -package json.primitives - -import TestUtil._ -import munit._ -import munit.internal.console -import co.blocke.scalajack.json.JSON -import co.blocke.scala_reflection._ - -class AnyPrim() extends FunSuite: - - val sj = co.blocke.scalajack.ScalaJack() - - test("null works") { - describe("-------------------------\n: Any Primitive Tests :\n-------------------------", Console.BLUE) - - val shell = AnyShell(null) - val js = sj.render(shell) - assertEquals("""{"a":null}""".asInstanceOf[JSON],js) - assert(sj.read[AnyShell](js).a == null) - } - - test("BigDecimal works") { - val payload = BigDecimal("12345678901234567890.12345678901234567890") - val js = sj.render(AnyShell(payload)) - assertEquals("""{"a":12345678901234567890.12345678901234567890}""".asInstanceOf[JSON], js) - assert({ - val parsed = sj.read[AnyShell](js).a - (parsed == payload) && (parsed.getClass == payload.getClass) - }) - } - - test("BigInt works") { - val payload = BigInt("12345678901234567890") - val js = sj.render(AnyShell(payload)) - assertEquals("""{"a":12345678901234567890}""".asInstanceOf[JSON], js) - assert({ - val parsed = sj.read[AnyShell](js).a - (parsed == payload) && parsed.isInstanceOf[BigInt] - }) - } - - test("Boolean works") { - val payload = true - val js = sj.render(AnyShell(payload)) - assertEquals("""{"a":true}""".asInstanceOf[JSON], js) - assert({ - val parsed = sj.read[AnyShell](js).a - (parsed == payload) && parsed.isInstanceOf[Boolean] - }) - } - - test("Byte works") { - val payload: Byte = 16 - val js = sj.render(AnyShell(payload)) - assertEquals("""{"a":16}""".asInstanceOf[JSON], js) - assert({ - val parsed = sj.read[AnyShell](js).a - (parsed == payload) && parsed.isInstanceOf[Integer] // byte becomes Integer - }) - } - - test("Char works") { - val payload: Char = 'Z' - val js = sj.render(AnyShell(payload)) - assertEquals("""{"a":"Z"}""".asInstanceOf[JSON], js) - assert({ - val parsed = sj.read[AnyShell](js).a - (parsed == payload.toString) && parsed.isInstanceOf[String] // Char becomes String - }) - } - - test("Double works") { - val payload: Double = 1234.5678 - val js = sj.render(AnyShell(payload)) - assertEquals("""{"a":1234.5678}""".asInstanceOf[JSON], js) - assert({ - val parsed = sj.read[AnyShell](js).a - (parsed == payload) && parsed.isInstanceOf[Double] - }) - } - - test("Enumeration works") { - val payload: Size.Value = Size.Small - val js = sj.render(AnyShell(payload)) - assertEquals("""{"a":"Small"}""".asInstanceOf[JSON], js) - assert({ - val parsed = sj.read[AnyShell](js).a - (parsed == payload.toString) && parsed.isInstanceOf[String] // enum value becomes String - }) - } - - test("Enum works") { - val payload = Color.Blue - val js = sj.render(AnyShell(payload)) - assertEquals("""{"a":"Blue"}""".asInstanceOf[JSON], js) - assert({ - val parsed = sj.read[AnyShell](js).a - (parsed == payload.toString) && parsed.isInstanceOf[String] // enum value becomes String - }) - } - - test("Float works") { - val payload: Float = 1234.5678F - val js = sj.render(AnyShell(payload)) - assertEquals("""{"a":1234.5677}""".asInstanceOf[JSON], js) - assert({ - val parsed = sj.read[AnyShell](js).a - (parsed.toString == payload.toString) && parsed.isInstanceOf[Double] // float becomes Double - }) - } - - test("Int works") { - val payload: Int = 1234567 - val js = sj.render(AnyShell(payload)) - assertEquals("""{"a":1234567}""".asInstanceOf[JSON], js) - assert({ - val parsed = sj.read[AnyShell](js).a - (parsed == payload) && parsed.isInstanceOf[Int] - }) - } - - test("Long works") { - val payload: Long = 123456789012345L - val js = sj.render(AnyShell(payload)) - assertEquals("""{"a":123456789012345}""".asInstanceOf[JSON], js) - assert({ - val parsed = sj.read[AnyShell](js).a - (parsed == payload) && parsed.isInstanceOf[Long] // (Note this could become Integer if smaller number parsed) - }) - - val payload2: Long = 123L - val js2 = sj.render(AnyShell(payload2)) - assertEquals("""{"a":123}""".asInstanceOf[JSON], js2) - assert({ - val parsed = sj.read[AnyShell](js2).a - (parsed == payload2) && parsed.isInstanceOf[Int] // Long became Int due to smaller size - }) - } - - test("Short works") { - val payload: Short = 16234 - val js = sj.render(AnyShell(payload)) - assertEquals("""{"a":16234}""".asInstanceOf[JSON], js) - assert({ - val parsed = sj.read[AnyShell](js).a - (parsed == payload) && parsed.isInstanceOf[Int] // short becomes Int - }) - } - - test("String works") { - val payload = "something" - val js = sj.render(AnyShell(payload)) - assertEquals("""{"a":"something"}""".asInstanceOf[JSON], js) - assert({ - val parsed = sj.read[AnyShell](js).a - (parsed == payload) && (parsed.getClass == payload.getClass) - }) - } \ No newline at end of file diff --git a/core/src/test/scala/co.blocke.scalajack/json/primitives/Enums.scala b/core/src/test/scala/co.blocke.scalajack/json/primitives/Enums.scala deleted file mode 100644 index dae76066..00000000 --- a/core/src/test/scala/co.blocke.scalajack/json/primitives/Enums.scala +++ /dev/null @@ -1,137 +0,0 @@ -package co.blocke.scalajack -package json.primitives - -import co.blocke.scala_reflection._ -import scala.math.BigDecimal -import java.util.UUID -import TestUtil._ -import munit._ -import munit.internal.console -import co.blocke.scalajack.json.JSON - -class Enums() extends FunSuite: - - val sj = co.blocke.scalajack.ScalaJack() - val sj2 = sj.enumsAsInts() - - test("Enumeration (Scala 2.x) must work (not nullable)") { - describe("-----------------\n: Scala Enums :\n-----------------", Console.BLUE) - describe("+++ Positive Tests +++") - - import SizeWithType._ - val inst = SampleEnum(Size.Small, Size.Medium, Size.Large, null, Size.Medium, Little) - val js = sj.render(inst) - assertEquals( - """{"e1":"Small","e2":"Medium","e3":"Large","e4":null,"e5":"Medium","e6":"Little"}""".asInstanceOf[JSON], - js) - // mutate e5 into an ordinal... - val js2 = js.asInstanceOf[String].replaceAll(""""e5":"Medium"""", """"e5":1""").asInstanceOf[JSON] - assertEquals(inst, sj.read[SampleEnum](js2)) - } - - test("Ordinal Enumeration (Scala 2.x) must work (not nullable)") { - import SizeWithType._ - val inst = SampleEnum(Size.Small, Size.Medium, Size.Large, null, Size.Medium, Little) - val js = sj2.render(inst) - assertEquals( - """{"e1":0,"e2":1,"e3":2,"e4":null,"e5":1,"e6":0}""".asInstanceOf[JSON], - js) - // mutate e5 into an ordinal... - val js2 = js.asInstanceOf[String].replaceAll(""""e5":"Medium"""", """"e5":1""").asInstanceOf[JSON] - assertEquals(inst, sj.read[SampleEnum](js2)) - } - - test("Enum (Scala 3.x) must work (not nullable)") { - val inst = TVColors(null, Color.Red) - val js = sj.render(inst) - assertEquals( - """{"color1":null,"color2":"Red"}""".asInstanceOf[JSON], - js) - val inst2 = sj.read[TVColors](js) - assertEquals(inst, inst2) - } - - test("Ordinal Enum (Scala 3.x) must work (not nullable)") { - val inst = TVColors(null, Color.Red) - val js = sj2.render(inst) - assertEquals( - """{"color1":null,"color2":0}""".asInstanceOf[JSON], - js) - assertEquals(inst, sj2.read[TVColors](js)) - } - - test("""Sealed trait "enums" must work""") { - val inst = Ride(Car(4,"Red")) - val js = sj.render(inst) - assertEquals("""{"wheels":{"numberOfWheels":4,"color":"Red"}}""".asInstanceOf[JSON],js) - assertEquals(inst, sj.read[Ride](js)) - } - - test("""Case object "enums" must work""") { - val inst = Favorite(Chocolate) - val js = sj.render(inst) - assertEquals("""{"flavor":"Chocolate"}""".asInstanceOf[JSON],js) - assertEquals(inst, sj.read[Favorite](js)) - } - - test("Enumeration (Scala 2.x) must break") { - describe("--- Negative Tests ---") - val js = - """{"e1":"Small","e2":"Bogus","e3":"Large","e4":null,"e5":"Medium","e6":"Little"}""".asInstanceOf[JSON] - val msg = - """No value found in enumeration co.blocke.scalajack.json.primitives.Size$ for Bogus - |{"e1":"Small","e2":"Bogus","e3":"Large","e4":null,"e5":"Medium","e6":"Little"} - |-------------------------^""".stripMargin - interceptMessage[co.blocke.scalajack.ScalaJackError](msg){ - sj.read[SampleEnum](js) - } - val js2 = - """{"e1":"Small","e2":"Medium","e3":"Large","e4":null,"e5":9,"e6":"Little}""".asInstanceOf[JSON] - val msg2 = - """No value found in enumeration co.blocke.scalajack.json.primitives.Size$ for 9 - |...Small","e2":"Medium","e3":"Large","e4":null,"e5":9,"e6":"Little} - |----------------------------------------------------^""".stripMargin - interceptMessage[co.blocke.scalajack.ScalaJackError](msg2){ - sj.read[SampleEnum](js2) - } - val js3 = - """{"e1":"Small","e2":"Medium","e3":"Large","e4":null,"e5":false,"e6":"Little}""".asInstanceOf[JSON] - val msg3 = """Expected a Number or String here - |...Small","e2":"Medium","e3":"Large","e4":null,"e5":false,"e6":"Little} - |----------------------------------------------------^""".stripMargin - interceptMessage[co.blocke.scalajack.ScalaJackError](msg3){ - sj.read[SampleEnum](js3) - } - } - - test("Enum (Scala 3.x) must break") { - val js = """{"color1":null,"color2":"Bogus"}""".asInstanceOf[JSON] - val msg = """No value found in enumeration co.blocke.scalajack.json.primitives.Color for Bogus - |{"color1":null,"color2":"Bogus"} - |------------------------------^""".stripMargin - interceptMessage[co.blocke.scalajack.ScalaJackError](msg){ - sj.read[TVColors](js) - } - } - - //-------------------------------------- - - test("Java Enumeration (not nullable) must work") { - describe("----------------------\n: Java Enumeration :\n----------------------", Console.BLUE) - describe("+++ Positive Tests +++") - - val inst = new JavaEnum() - inst.setTemp(Temperature.Hot) - val js = sj.render(inst) - assertEquals("""{"temp":"Hot"}""".asInstanceOf[JSON], js) - assertEquals(inst.getTemp, sj.read[JavaEnum](js).getTemp) - } - - test("Java Enumeration (not nullable) must fail") { - describe("--- Negative Tests ---") - - val js = """{"temp":"Bogus"}""".asInstanceOf[JSON] - interceptMessage[java.lang.IllegalArgumentException]("No enum constant co.blocke.scalajack.Temperature.Bogus"){ - sj.read[JavaEnum](js) - } - } diff --git a/core/src/test/scala/co.blocke.scalajack/json/primitives/JavaPrim.scala b/core/src/test/scala/co.blocke.scalajack/json/primitives/JavaPrim.scala deleted file mode 100644 index 3f1a8b24..00000000 --- a/core/src/test/scala/co.blocke.scalajack/json/primitives/JavaPrim.scala +++ /dev/null @@ -1,326 +0,0 @@ -package co.blocke.scalajack -package json.primitives - -import TestUtil._ -import munit._ -import munit.internal.console -import co.blocke.scalajack.json.JSON -import co.blocke.scala_reflection._ - -import java.lang.{ - Boolean => JBoolean, - Byte => JByte, - Double => JDouble, - Float => JFloat, - Integer => JInt, - Long => JLong, - Short => JShort -} -import java.math.{ BigDecimal => JBigDecimal, BigInteger => JBigInteger } - -class JavaPrim() extends FunSuite: - - val sj = co.blocke.scalajack.ScalaJack() - - test("BigDecimal must work") { - describe("--------------------------\n: Java Primitive Tests :\n--------------------------", Console.BLUE) - describe("+++ Positive Tests +++") - - val inst = SampleJBigDecimal( - JBigDecimal.ZERO, - JBigDecimal.ONE, - JBigDecimal.TEN, - new JBigDecimal( - "0.1499999999999999944488848768742172978818416595458984375" - ), - null - ) - val js = sj.render(inst) - assertEquals( - """{"bd1":0,"bd2":1,"bd3":10,"bd4":0.1499999999999999944488848768742172978818416595458984375,"bd5":null}""".asInstanceOf[JSON], - js) - assertEquals(inst, sj.read[SampleJBigDecimal](js)) - } - - test("BigInteger must work") { - val inst = SampleJBigInteger( - JBigInteger.ZERO, - JBigInteger.ONE, - JBigInteger.TEN, - new JBigInteger("-90182736451928374653345"), - new JBigInteger("90182736451928374653345"), - new JBigInteger("0"), - null - ) - val js = sj.render(inst) - assertEquals( - """{"bi1":0,"bi2":1,"bi3":10,"bi4":-90182736451928374653345,"bi5":90182736451928374653345,"bi6":0,"bi7":null}""".asInstanceOf[JSON], - js) - assertEquals(inst, sj.read[SampleJBigInteger](js)) - } - - test("Boolean must work") { - val inst = - SampleJBoolean(JBoolean.TRUE, JBoolean.FALSE, true, false, null) - val js = sj.render(inst) - assertEquals( - """{"bool1":true,"bool2":false,"bool3":true,"bool4":false,"bool5":null}""".asInstanceOf[JSON], - js) - assertEquals(inst, sj.read[SampleJBoolean](js)) - } - - test("Byte must work") { - val inst = SampleJByte( - JByte.MAX_VALUE, - JByte.MIN_VALUE, - 0.asInstanceOf[Byte], - 64.asInstanceOf[Byte], - null - ) - val js = sj.render(inst) - assertEquals("""{"b1":127,"b2":-128,"b3":0,"b4":64,"b5":null}""".asInstanceOf[JSON], js ) - assertEquals(inst, sj.read[SampleJByte](js)) - } - - test("Char must work") { - val inst = SampleJChar('Z', '\u20A0', null) - val js = sj.render(inst) - assertEquals(("""{"c1":"Z","c2":"\""" + """u20a0","c3":null}""").asInstanceOf[JSON], js ) - assertEquals(inst, sj.read[SampleJChar](js)) - } - - test("Double must work") { - val inst = SampleJDouble( - JDouble.MAX_VALUE, - JDouble.MIN_VALUE, - 0.0, - -123.4567, - null - ) - val js = sj.render(inst) - assertEquals( - """{"d1":1.7976931348623157E308,"d2":4.9E-324,"d3":0.0,"d4":-123.4567,"d5":null}""".asInstanceOf[JSON], - js) - assertEquals(inst, sj.read[SampleJDouble](js)) - } - - test("Float must work") { - val inst = SampleJFloat( - JFloat.MAX_VALUE, - JFloat.MIN_VALUE, - 0.0F, - -123.4567F, - null - ) - val js = sj.render(inst) - assertEquals( - """{"f1":3.4028235E38,"f2":1.4E-45,"f3":0.0,"f4":-123.4567,"f5":null}""".asInstanceOf[JSON], - js) - assertEquals(inst, sj.read[SampleJFloat](js)) - } - - test("Int must work") { - val inst = SampleJInt(JInt.MAX_VALUE, JInt.MIN_VALUE, 0, 123, null) - val js = sj.render(inst) - assertEquals( - """{"i1":2147483647,"i2":-2147483648,"i3":0,"i4":123,"i5":null}""".asInstanceOf[JSON], - js) - assertEquals(inst, sj.read[SampleJInt](js)) - } - - test("Long must work") { - val inst = SampleJLong(JLong.MAX_VALUE, JLong.MIN_VALUE, 0L, 123L, null) - val js = sj.render(inst) - assertEquals( - """{"l1":9223372036854775807,"l2":-9223372036854775808,"l3":0,"l4":123,"l5":null}""".asInstanceOf[JSON], - js) - assertEquals(inst, sj.read[SampleJLong](js)) - } - - test("Number must work") { - val inst = SampleJNumber( - JByte.valueOf("-128"), - JByte.valueOf("127"), - JShort.valueOf("-32768"), - JShort.valueOf("32767"), - JInt.valueOf("-2147483648"), - JInt.valueOf("2147483647"), - JLong.valueOf("-9223372036854775808"), - JLong.valueOf("9223372036854755807"), - null, //new JBigInteger("9923372036854755810"), - JByte.valueOf("0"), - JFloat.valueOf("3.4e-038"), - JFloat.valueOf("3.4e+038"), - JDouble.valueOf("1.7e-308"), - JDouble.valueOf("1.7e+308"), - null, //new JBigDecimal("1.8e+308"), - JFloat.valueOf("0.0"), - null - ) - val js = sj.render(inst) - assertEquals( - """{"n1":-128,"n2":127,"n3":-32768,"n4":32767,"n5":-2147483648,"n6":2147483647,"n7":-9223372036854775808,"n8":9223372036854755807,"n9":null,"n10":0,"n11":3.4E-38,"n12":3.4E38,"n13":1.7E-308,"n14":1.7E308,"n15":null,"n16":0.0,"n17":null}""".asInstanceOf[JSON], - js) - assertEquals(inst, sj.read[SampleJNumber](js)) - } - - test("Short must work") { - val inst = SampleJShort( - JShort.MAX_VALUE, - JShort.MIN_VALUE, - 0.asInstanceOf[Short], - 123.asInstanceOf[Short], - null - ) - val js = sj.render(inst) - assertEquals("""{"s1":32767,"s2":-32768,"s3":0,"s4":123,"s5":null}""".asInstanceOf[JSON], js) - assertEquals(inst, sj.read[SampleJShort](js)) - } - - - //-------------------------------------------------------- - - - test("BigDecimal must break") { - describe("--- Negative Tests ---") - val js = - """{"bd1":0,"bd2":1,"bd3":10,"bd4":"0.1499999999999999944488848768742172978818416595458984375","bd5":null}""".asInstanceOf[JSON] - val msg = - """Expected a Number here - |{"bd1":0,"bd2":1,"bd3":10,"bd4":"0.149999999999999994448884876874217297881841... - |--------------------------------^""".stripMargin - interceptMessage[co.blocke.scalajack.ScalaJackError](msg){ - sj.read[SampleJBigDecimal](js) - } - } - - test("BigInt must break") { - val js = - """{"bi1":"0","bi2":1,"bi3":10,"bi4":-90182736451928374653345,"bi5":90182736451928374653345,"bi6":0,"bi7":null}""".asInstanceOf[JSON] - val msg = - """Expected a Number here - |{"bi1":"0","bi2":1,"bi3":10,"bi4":-90182736451928374653345,"bi5":901827364519... - |-------^""".stripMargin - interceptMessage[co.blocke.scalajack.ScalaJackError](msg){ - sj.read[SampleJBigInteger](js) - } - } - - test("Boolean must break") { - val js = """{"bool1":true,"bool2":false,"bool3":true,"bool4":"false","bool5":null}""".asInstanceOf[JSON] - val msg = - """Expected a Boolean here - |{"bool1":true,"bool2":false,"bool3":true,"bool4":"false","bool5":null} - |-------------------------------------------------^""".stripMargin - interceptMessage[co.blocke.scalajack.ScalaJackError](msg){ - sj.read[SampleJBoolean](js) - } - } - - test("Byte must break") { - val js = """{"b1":127,"b2":-128,"b3":false,"b4":64,"b5":null}""".asInstanceOf[JSON] - val msg = """Expected a Number here - |{"b1":127,"b2":-128,"b3":false,"b4":64,"b5":null} - |-------------------------^""".stripMargin - interceptMessage[co.blocke.scalajack.ScalaJackError](msg){ - sj.read[SampleJByte](js) - } - } - - test("Char must break") { - val js = """{"c1":"Z","c2":3,"c3":null}""".asInstanceOf[JSON] - val msg = """Expected a String here - |{"c1":"Z","c2":3,"c3":null} - |---------------^""".stripMargin - interceptMessage[co.blocke.scalajack.ScalaJackError](msg){ - sj.read[SampleJChar](js) - } - val js2 = """{"c1":"Z","c2":"","c3":null}""".asInstanceOf[JSON] - val msg2 = """Tried to read a Character but empty string found - |{"c1":"Z","c2":"","c3":null} - |----------------^""".stripMargin - interceptMessage[co.blocke.scalajack.ScalaJackError](msg2){ - sj.read[SampleJChar](js2) - } - } - - test("Double must break") { - val js = - """{"d1":1.7976931348623157E308,"d2":4.9E-324,"d3":"0.0","d4":-123.4567,"d5":null}""".asInstanceOf[JSON] - val msg = - """Expected a Number here - |{"d1":1.7976931348623157E308,"d2":4.9E-324,"d3":"0.0","d4":-123.4567,"d5":null} - |------------------------------------------------^""".stripMargin - interceptMessage[co.blocke.scalajack.ScalaJackError](msg){ - sj.read[SampleJDouble](js) - } - } - - test("Float must break") { - val js = - """{"f1":3.4028235E38,"f2":"1.4E-45","f3":0.0,"f4":-123.4567,"f5":null}""".asInstanceOf[JSON] - val msg = - """Expected a Number here - |{"f1":3.4028235E38,"f2":"1.4E-45","f3":0.0,"f4":-123.4567,"f5":null} - |------------------------^""".stripMargin - interceptMessage[co.blocke.scalajack.ScalaJackError](msg){ - sj.read[SampleJFloat](js) - } - } - - test("Int must break") { - val js = """{"i1":2147483647,"i2":-2147483648,"i3":false,"i4":123,"i5":null}""".asInstanceOf[JSON] - val msg = - """Expected a Number here - |{"i1":2147483647,"i2":-2147483648,"i3":false,"i4":123,"i5":null} - |---------------------------------------^""".stripMargin - interceptMessage[co.blocke.scalajack.ScalaJackError](msg){ - sj.read[SampleJInt](js) - } - val js2 = """{"i1":2147483647,"i2":-2147483648,"i3":0.3,"i4":123,"i5":null}""".asInstanceOf[JSON] - interceptMessage[java.lang.NumberFormatException]("For input string: \"0.3\""){ - sj.read[SampleJInt](js2) - } - } - - test("Long must break") { - val js = - """{"l1":9223372036854775807,"l2":-9223372036854775808,"l3":"0","l4":123,"l5":null}""".asInstanceOf[JSON] - val msg = - """Expected a Number here - |...23372036854775807,"l2":-9223372036854775808,"l3":"0","l4":123,"l5":null} - |----------------------------------------------------^""".stripMargin - interceptMessage[co.blocke.scalajack.ScalaJackError](msg){ - sj.read[SampleJLong](js) - } - val js2 = - """{"l1":9223372036854775807,"l2":-9223372036854775808,"l3":0.3,"l4":123,"l5":null}""".asInstanceOf[JSON] - interceptMessage[java.lang.NumberFormatException]("For input string: \"0.3\""){ - sj.read[SampleJLong](js2) - } - } - - test("Number must break") { - val js = """{"n1":-128,"n2":127,"n3":"-32768","n4":32767,"n5":-2147483648,"n6":2147483647,"n7":-9223372036854775808,"n8":9223372036854755807,"n9":9923372036854755810,"n10":0,"n11":3.4E-38,"n12":3.4E38,"n13":1.7E-308,"n14":1.7E308,"n15":1.8E+308,"n16":0.0,"n17":null}""".asInstanceOf[JSON] - val msg = - """Expected a Number here - |{"n1":-128,"n2":127,"n3":"-32768","n4":32767,"n5":-2147483648,"n6":2147483647... - |-------------------------^""".stripMargin - interceptMessage[co.blocke.scalajack.ScalaJackError](msg){ - sj.read[SampleJNumber](js) - } - } - - test("Short must break") { - val js = """{"s1":false,"s2":-32768,"s3":0,"s4":123,"s5":null}""".asInstanceOf[JSON] - val msg = """Expected a Number here - |{"s1":false,"s2":-32768,"s3":0,"s4":123,"s5":null} - |------^""".stripMargin - interceptMessage[co.blocke.scalajack.ScalaJackError](msg){ - sj.read[SampleJShort](js) - } - val js2 = """{"s1":2.3,"s2":-32768,"s3":0,"s4":123,"s5":null}""".asInstanceOf[JSON] - interceptMessage[java.lang.NumberFormatException]("For input string: \"2.3\""){ - sj.read[SampleJShort](js2) - } - } diff --git a/core/src/test/scala/co.blocke.scalajack/json/primitives/Model.scala b/core/src/test/scala/co.blocke.scalajack/json/primitives/Model.scala deleted file mode 100644 index 451c9854..00000000 --- a/core/src/test/scala/co.blocke.scalajack/json/primitives/Model.scala +++ /dev/null @@ -1,197 +0,0 @@ -package co.blocke.scalajack -package json.primitives - -import java.util.UUID -import java.lang.{ - Boolean => JBoolean, - Byte => JByte, - Character => JChar, - Double => JDouble, - Float => JFloat, - Integer => JInt, - Long => JLong, - Number => JNumber, - Short => JShort -} -import java.math.{ BigDecimal => JBigDecimal, BigInteger => JBigInteger } -import java.time._ -import scala.math._ - -// === Scala -case class SampleBigDecimal( - bd1: BigDecimal, - bd2: BigDecimal, - bd3: BigDecimal, - bd4: BigDecimal, - bd5: BigDecimal, - bd6: BigDecimal) -case class SampleBigInt(bi1: BigInt, bi2: BigInt, bi3: BigInt, bi4: BigInt) -case class SampleBinary(b1: Array[Byte], b2: Array[Byte]) -case class SampleBoolean(bool1: Boolean, bool2: Boolean) -case class SampleByte(b1: Byte, b2: Byte, b3: Byte, b4: Byte) -case class SampleChar(c1: Char, c2: Char, c3: Char) -case class SampleDouble(d1: Double, d2: Double, d3: Double, d4: Double) - -object Size extends Enumeration { - val Small, Medium, Large = Value -} -object SizeWithType extends Enumeration { - type SizeWithType = Value - val Little, Grand = Value -} -import SizeWithType._ -case class SampleEnum( - e1: Size.Value, - e2: Size.Value, - e3: Size.Value, - e4: Size.Value, - e5: Size.Value, - e6: SizeWithType) - -enum Color { - case Red, Blue, Green -} -case class TVColors( color1: Color, color2: Color ) - -sealed trait Flavor -case object Vanilla extends Flavor -case object Chocolate extends Flavor -case object Bourbon extends Flavor - -sealed trait Vehicle -case class Truck(numberOfWheels: Int) extends Vehicle -case class Car(numberOfWheels: Int, color: String) extends Vehicle -case class Plane(numberOfEngines: Int) extends Vehicle - -case class Ride( wheels: Vehicle ) -case class Favorite( flavor: Flavor ) - -case class SampleFloat(f1: Float, f2: Float, f3: Float, f4: Float) -case class SampleInt(i1: Int, i2: Int, i3: Int, i4: Int) -case class SampleLong(l1: Long, l2: Long, l3: Long, l4: Long) -case class SampleShort(s1: Short, s2: Short, s3: Short, s4: Short) -case class SampleString(s1: String, s2: String, s3: String) - -// === Java -case class SampleJBigDecimal( - bd1: JBigDecimal, - bd2: JBigDecimal, - bd3: JBigDecimal, - bd4: JBigDecimal, - bd5: JBigDecimal) -case class SampleJBigInteger( - bi1: JBigInteger, - bi2: JBigInteger, - bi3: JBigInteger, - bi4: JBigInteger, - bi5: JBigInteger, - bi6: JBigInteger, - bi7: JBigInteger) -case class SampleJBoolean( - bool1: JBoolean, - bool2: JBoolean, - bool3: JBoolean, - bool4: JBoolean, - bool5: JBoolean) -case class SampleJByte(b1: JByte, b2: JByte, b3: JByte, b4: JByte, b5: JByte) -case class SampleJChar(c1: JChar, c2: JChar, c3: JChar) -case class SampleJDouble( - d1: JDouble, - d2: JDouble, - d3: JDouble, - d4: JDouble, - d5: JDouble) -case class SampleJFloat( - f1: JFloat, - f2: JFloat, - f3: JFloat, - f4: JFloat, - f5: JFloat) -case class SampleJInt(i1: JInt, i2: JInt, i3: JInt, i4: JInt, i5: JInt) -case class SampleJLong(l1: JLong, l2: JLong, l3: JLong, l4: JLong, l5: JLong) -case class SampleJNumber( - n1: JNumber, - n2: JNumber, - n3: JNumber, - n4: JNumber, - n5: JNumber, - n6: JNumber, - n7: JNumber, - n8: JNumber, - n9: JNumber, - n10: JNumber, - n11: JNumber, - n12: JNumber, - n13: JNumber, - n14: JNumber, - n15: JNumber, - n16: JNumber, - n17: JNumber) -case class SampleJShort( - s1: JShort, - s2: JShort, - s3: JShort, - s4: JShort, - s5: JShort) -case class SampleUUID(u1: UUID, u2: UUID) - -// === Java Time -case class SampleDuration(d1: Duration, d2: Duration, d3: Duration) -case class SampleInstant( - i1: Instant, - i2: Instant, - i3: Instant, - i4: Instant, - i5: Instant) -case class SampleLocalDateTime( - d1: LocalDateTime, - d2: LocalDateTime, - d3: LocalDateTime, - d4: LocalDateTime) -case class SampleLocalDate( - d1: LocalDate, - d2: LocalDate, - d3: LocalDate, - d4: LocalDate) -case class SampleLocalTime( - d1: LocalTime, - d2: LocalTime, - d3: LocalTime, - d4: LocalTime, - d5: LocalTime, - d6: LocalTime) -case class SampleOffsetDateTime( - o1: OffsetDateTime, - o2: OffsetDateTime, - o3: OffsetDateTime, - o4: OffsetDateTime) -case class SampleOffsetTime( - o1: OffsetTime, - o2: OffsetTime, - o3: OffsetTime, - o4: OffsetTime) -case class SamplePeriod(p1: Period, p2: Period, p3: Period) -case class SampleZonedDateTime(o1: ZonedDateTime, o2: ZonedDateTime) - -// === Any primitives -case class AnyShell(a: Any) - -// === Value Classes -case class VCBigDecimal(vc: BigDecimal) extends AnyVal -case class VCBigInt(vc: BigInt) extends AnyVal -case class VCBoolean(vc: Boolean) extends AnyVal -case class VCByte(vc: Byte) extends AnyVal -case class VCChar(vc: Char) extends AnyVal -case class VCDouble(vc: Double) extends AnyVal -case class VCEnum(vc: Color) extends AnyVal -case class VCEnumeration(vc: Size.Value) extends AnyVal -case class VCFloat(vc: Float) extends AnyVal -case class VCInt(vc: Int) extends AnyVal -case class VCLong(vc: Long) extends AnyVal -case class VCShort(vc: Short) extends AnyVal -case class VCString(vc: String) extends AnyVal -case class VCUUID(vc: UUID) extends AnyVal -case class VCNumber(vc: Number) extends AnyVal - -// === Permissives test -case class Holder[T](value: T) \ No newline at end of file diff --git a/core/src/test/scala/co.blocke.scalajack/json/primitives/PermissivePrimitives.scala b/core/src/test/scala/co.blocke.scalajack/json/primitives/PermissivePrimitives.scala deleted file mode 100644 index d2e27d3f..00000000 --- a/core/src/test/scala/co.blocke.scalajack/json/primitives/PermissivePrimitives.scala +++ /dev/null @@ -1,233 +0,0 @@ -package co.blocke.scalajack -package json.primitives - -import TestUtil._ -import munit._ -import munit.internal.console -import co.blocke.scalajack.json.JSON -import co.blocke.scala_reflection._ - -class PermissivePrimitives() extends FunSuite: - - val sj = co.blocke.scalajack.ScalaJack().allowPermissivePrimitives() - - test("Boolean must work") { - describe("--------------------------------\n: Permissive Primitive Tests :\n--------------------------------", Console.BLUE) - - assertEquals(Holder(true), sj.read[Holder[Boolean]]("""{"value":true}""".asInstanceOf[JSON])) - assertEquals(Holder(true), sj.read[Holder[Boolean]]("""{"value":"true"}""".asInstanceOf[JSON])) - assertEquals(Holder(false), sj.read[Holder[Boolean]]("""{"value":false}""".asInstanceOf[JSON])) - assertEquals(Holder(false), sj.read[Holder[Boolean]]("""{"value":"false"}""".asInstanceOf[JSON])) - assertEquals(Holder(java.lang.Boolean.TRUE), sj.read[Holder[java.lang.Boolean]]("""{"value":true}""".asInstanceOf[JSON])) - assertEquals(Holder(java.lang.Boolean.TRUE), sj.read[Holder[java.lang.Boolean]]("""{"value":"true"}""".asInstanceOf[JSON])) - assertEquals(Holder(java.lang.Boolean.FALSE), sj.read[Holder[java.lang.Boolean]]("""{"value":false}""".asInstanceOf[JSON])) - assertEquals(Holder(java.lang.Boolean.FALSE), sj.read[Holder[java.lang.Boolean]]("""{"value":"false"}""".asInstanceOf[JSON])) - assertEquals(Holder[java.lang.Boolean](null), sj.read[Holder[java.lang.Boolean]]("""{"value":null}""".asInstanceOf[JSON])) - - val msg = """Expected a java.lang.Boolean here - |{"value":""} - |----------^""".stripMargin - interceptMessage[co.blocke.scalajack.ScalaJackError](msg){ - sj.read[Holder[java.lang.Boolean]]("""{"value":""}""".asInstanceOf[JSON]) - } - - assertEquals("""{"value":true}""".asInstanceOf[JSON], sj.render(Holder(true))) - assertEquals("""{"value":false}""".asInstanceOf[JSON], sj.render(Holder(false))) - assertEquals("""{"value":true}""".asInstanceOf[JSON], sj.render(Holder(java.lang.Boolean.TRUE))) - assertEquals("""{"value":false}""".asInstanceOf[JSON], sj.render(Holder(java.lang.Boolean.FALSE))) - assertEquals("""{"value":null}""".asInstanceOf[JSON], sj.render(Holder[java.lang.Boolean](null))) - } - - test("Byte must work") { - assertEquals(Holder(42.toByte), sj.read[Holder[Byte]]("""{"value":42}""".asInstanceOf[JSON])) - assertEquals(Holder(42.toByte), sj.read[Holder[Byte]]("""{"value":"42"}""".asInstanceOf[JSON])) - assertEquals(Holder(java.lang.Byte.valueOf(42.toByte)), sj.read[Holder[java.lang.Byte]]("""{"value":42}""".asInstanceOf[JSON])) - assertEquals(Holder(java.lang.Byte.valueOf(42.toByte)), sj.read[Holder[java.lang.Byte]]("""{"value":"42"}""".asInstanceOf[JSON])) - // assertEquals(Holder[java.lang.Byte](null), sj.read[Holder[java.lang.Byte]]("""{"value":null}""".asInstanceOf[JSON])) - - val msg = """Expected a java.lang.Byte here - |{"value":""} - |----------^""".stripMargin - interceptMessage[co.blocke.scalajack.ScalaJackError](msg){ - sj.read[Holder[java.lang.Byte]]("""{"value":""}""".asInstanceOf[JSON]) - } - - assertEquals("""{"value":42}""".asInstanceOf[JSON], sj.render(Holder(42.toByte))) - assertEquals("""{"value":42}""".asInstanceOf[JSON], sj.render(Holder(java.lang.Byte.valueOf(42.toByte)))) - assertEquals("""{"value":null}""".asInstanceOf[JSON], sj.render(Holder[java.lang.Byte](null))) - } - - test("Double must work") { - assertEquals(Holder(42.5), sj.read[Holder[Double]]("""{"value":42.5}""".asInstanceOf[JSON])) - assertEquals(Holder(42.5), sj.read[Holder[Double]]("""{"value":"42.5"}""".asInstanceOf[JSON])) - assertEquals(Holder(java.lang.Double.valueOf(42.5)), sj.read[Holder[java.lang.Double]]("""{"value":42.5}""".asInstanceOf[JSON])) - assertEquals(Holder(java.lang.Double.valueOf(42.5)), sj.read[Holder[java.lang.Double]]("""{"value":"42.5"}""".asInstanceOf[JSON])) - // assertEquals(Holder[java.lang.Double](null), sj.read[Holder[java.lang.Double]]("""{"value":null}""".asInstanceOf[JSON])) - - val msg = """Expected a java.lang.Double here - |{"value":""} - |----------^""".stripMargin - interceptMessage[co.blocke.scalajack.ScalaJackError](msg){ - sj.read[Holder[java.lang.Double]]("""{"value":""}""".asInstanceOf[JSON]) - } - - assertEquals("""{"value":42.5}""".asInstanceOf[JSON], sj.render(Holder(42.5))) - assertEquals("""{"value":42.5}""".asInstanceOf[JSON], sj.render(Holder(java.lang.Double.valueOf(42.5)))) - assertEquals("""{"value":null}""".asInstanceOf[JSON], sj.render(Holder[java.lang.Double](null))) - } - - test("Float must work") { - assertEquals(Holder(42.5.toFloat), sj.read[Holder[Float]]("""{"value":42.5}""".asInstanceOf[JSON])) - assertEquals(Holder(42.5.toFloat), sj.read[Holder[Float]]("""{"value":"42.5"}""".asInstanceOf[JSON])) - assertEquals(Holder(java.lang.Float.valueOf(42.5.toFloat)), sj.read[Holder[java.lang.Float]]("""{"value":42.5}""".asInstanceOf[JSON])) - assertEquals(Holder(java.lang.Float.valueOf(42.5.toFloat)), sj.read[Holder[java.lang.Float]]("""{"value":"42.5"}""".asInstanceOf[JSON])) - // assertEquals(Holder[java.lang.Float](null), sj.read[Holder[java.lang.Float]]("""{"value":null}""".asInstanceOf[JSON])) - - val msg = """Expected a java.lang.Float here - |{"value":""} - |----------^""".stripMargin - interceptMessage[co.blocke.scalajack.ScalaJackError](msg){ - sj.read[Holder[java.lang.Float]]("""{"value":""}""".asInstanceOf[JSON]) - } - - assertEquals("""{"value":42.5}""".asInstanceOf[JSON], sj.render(Holder(42.5.toFloat))) - assertEquals("""{"value":42.5}""".asInstanceOf[JSON], sj.render(Holder(java.lang.Float.valueOf(42.5.toFloat)))) - assertEquals("""{"value":null}""".asInstanceOf[JSON], sj.render(Holder[java.lang.Float](null))) - } - - test("Int must work") { - assertEquals(Holder(42), sj.read[Holder[Int]]("""{"value":42}""".asInstanceOf[JSON])) - assertEquals(Holder(42), sj.read[Holder[Int]]("""{"value":"42"}""".asInstanceOf[JSON])) - assertEquals(Holder(java.lang.Integer.valueOf(42)), sj.read[Holder[java.lang.Integer]]("""{"value":42}""".asInstanceOf[JSON])) - assertEquals(Holder(java.lang.Integer.valueOf(42)), sj.read[Holder[java.lang.Integer]]("""{"value":"42"}""".asInstanceOf[JSON])) - // assertEquals(Holder[java.lang.Integer](null), sj.read[Holder[java.lang.Integer]]("""{"value":null}""".asInstanceOf[JSON])) - - val msg = """Expected a java.lang.Integer here - |{"value":""} - |----------^""".stripMargin - interceptMessage[co.blocke.scalajack.ScalaJackError](msg){ - sj.read[Holder[java.lang.Integer]]("""{"value":""}""".asInstanceOf[JSON]) - } - - assertEquals("""{"value":42}""".asInstanceOf[JSON], sj.render(Holder(42))) - assertEquals("""{"value":42}""".asInstanceOf[JSON], sj.render(Holder(java.lang.Integer.valueOf(42)))) - assertEquals("""{"value":null}""".asInstanceOf[JSON], sj.render(Holder[java.lang.Integer](null))) - } - - test("Long must work") { - assertEquals(Holder(42.toLong), sj.read[Holder[Long]]("""{"value":42}""".asInstanceOf[JSON])) - assertEquals(Holder(42.toLong), sj.read[Holder[Long]]("""{"value":"42"}""".asInstanceOf[JSON])) - assertEquals(Holder(java.lang.Long.valueOf(42.toLong)), sj.read[Holder[java.lang.Long]]("""{"value":42}""".asInstanceOf[JSON])) - assertEquals(Holder(java.lang.Long.valueOf(42.toLong)), sj.read[Holder[java.lang.Long]]("""{"value":"42"}""".asInstanceOf[JSON])) - // assertEquals(Holder[java.lang.Long](null), sj.read[Holder[java.lang.Long]]("""{"value":null}""".asInstanceOf[JSON])) - - val msg = """Expected a java.lang.Long here - |{"value":""} - |----------^""".stripMargin - interceptMessage[co.blocke.scalajack.ScalaJackError](msg){ - sj.read[Holder[java.lang.Long]]("""{"value":""}""".asInstanceOf[JSON]) - } - - assertEquals("""{"value":42}""".asInstanceOf[JSON], sj.render(Holder(42.toLong))) - assertEquals("""{"value":42}""".asInstanceOf[JSON], sj.render(Holder(java.lang.Long.valueOf(42.toLong)))) - assertEquals("""{"value":null}""".asInstanceOf[JSON], sj.render(Holder[java.lang.Long](null))) - } - - test("Short must work") { - assertEquals(Holder(42.toShort), sj.read[Holder[Short]]("""{"value":42}""".asInstanceOf[JSON])) - assertEquals(Holder(42.toShort), sj.read[Holder[Short]]("""{"value":"42"}""".asInstanceOf[JSON])) - assertEquals(Holder(java.lang.Short.valueOf(42.toShort)), sj.read[Holder[java.lang.Short]]("""{"value":42}""".asInstanceOf[JSON])) - assertEquals(Holder(java.lang.Short.valueOf(42.toShort)), sj.read[Holder[java.lang.Short]]("""{"value":"42"}""".asInstanceOf[JSON])) - // assertEquals(Holder[java.lang.Short](null), sj.read[Holder[java.lang.Short]]("""{"value":null}""".asInstanceOf[JSON])) - - val msg = """Expected a java.lang.Short here - |{"value":""} - |----------^""".stripMargin - interceptMessage[co.blocke.scalajack.ScalaJackError](msg){ - sj.read[Holder[java.lang.Short]]("""{"value":""}""".asInstanceOf[JSON]) - } - - assertEquals("""{"value":42}""".asInstanceOf[JSON], sj.render(Holder(42.toShort))) - assertEquals("""{"value":42}""".asInstanceOf[JSON], sj.render(Holder(java.lang.Short.valueOf(42.toShort)))) - assertEquals("""{"value":null}""".asInstanceOf[JSON], sj.render(Holder[java.lang.Short](null))) - } - - test("Scala BigInt must work") { - assertEquals(Holder(BigInt(42)), sj.read[Holder[BigInt]]("""{"value":42}""".asInstanceOf[JSON])) - assertEquals(Holder(BigInt(42)), sj.read[Holder[BigInt]]("""{"value":"42"}""".asInstanceOf[JSON])) - assertEquals(Holder[BigInt](null), sj.read[Holder[BigInt]]("""{"value":null}""".asInstanceOf[JSON])) - - val msg = """Expected a scala.math.BigInt here - |{"value":""} - |----------^""".stripMargin - interceptMessage[co.blocke.scalajack.ScalaJackError](msg){ - sj.read[Holder[BigInt]]("""{"value":""}""".asInstanceOf[JSON]) - } - - assertEquals("""{"value":42}""".asInstanceOf[JSON], sj.render(Holder(BigInt(42)))) - assertEquals("""{"value":null}""".asInstanceOf[JSON], sj.render(Holder[BigInt](null))) - } - - test("Java BigInteger must work") { - assertEquals(Holder(java.math.BigInteger.valueOf(42)), sj.read[Holder[java.math.BigInteger]]("""{"value":42}""".asInstanceOf[JSON])) - assertEquals(Holder(java.math.BigInteger.valueOf(42)), sj.read[Holder[java.math.BigInteger]]("""{"value":"42"}""".asInstanceOf[JSON])) - // assertEquals(Holder[java.math.BigInteger](null), sj.read[Holder[java.math.BigInteger]]("""{"value":null}""".asInstanceOf[JSON])) - - val msg = """Expected a java.math.BigInteger here - |{"value":""} - |----------^""".stripMargin - interceptMessage[co.blocke.scalajack.ScalaJackError](msg){ - sj.read[Holder[java.math.BigInteger]]("""{"value":""}""".asInstanceOf[JSON]) - } - - assertEquals("""{"value":42}""".asInstanceOf[JSON], sj.render(Holder(java.math.BigInteger.valueOf(42)))) - assertEquals("""{"value":null}""".asInstanceOf[JSON], sj.render(Holder[java.math.BigInteger](null))) - } - - test("Scala BigDecimal must work") { - assertEquals(Holder(BigDecimal("12.34")), sj.read[Holder[BigDecimal]]("""{"value":12.34}""".asInstanceOf[JSON])) - assertEquals(Holder(BigDecimal("12.34")), sj.read[Holder[BigDecimal]]("""{"value":"12.34"}""".asInstanceOf[JSON])) - // assertEquals(Holder[BigDecimal](null), sj.read[Holder[BigDecimal]]("""{"value":null}""".asInstanceOf[JSON])) - - val msg = """Expected a scala.math.BigDecimal here - |{"value":""} - |----------^""".stripMargin - interceptMessage[co.blocke.scalajack.ScalaJackError](msg){ - sj.read[Holder[BigDecimal]]("""{"value":""}""".asInstanceOf[JSON]) - } - - assertEquals("""{"value":12.34}""".asInstanceOf[JSON], sj.render(Holder(BigDecimal("12.34")))) - assertEquals("""{"value":null}""".asInstanceOf[JSON], sj.render(Holder[BigDecimal](null))) - } - - test("Java BigDecimal must work") { - assertEquals(Holder(new java.math.BigDecimal("12.34")), sj.read[Holder[java.math.BigDecimal]]("""{"value":12.34}""".asInstanceOf[JSON])) - assertEquals(Holder(new java.math.BigDecimal("12.34")), sj.read[Holder[java.math.BigDecimal]]("""{"value":"12.34"}""".asInstanceOf[JSON])) - // assertEquals(Holder[java.math.BigDecimal](null), sj.read[Holder[java.math.BigDecimal]]("""{"value":null}""".asInstanceOf[JSON])) - - val msg = """Expected a java.math.BigDecimal here - |{"value":""} - |----------^""".stripMargin - interceptMessage[co.blocke.scalajack.ScalaJackError](msg){ - sj.read[Holder[java.math.BigDecimal]]("""{"value":""}""".asInstanceOf[JSON]) - } - - assertEquals("""{"value":12.34}""".asInstanceOf[JSON], sj.render(Holder(new java.math.BigDecimal("12.34")))) - assertEquals("""{"value":null}""".asInstanceOf[JSON], sj.render(Holder[java.math.BigDecimal](null))) - } - - test("Java Number must work") { - assertEquals(Holder(java.lang.Double.valueOf(42.5).asInstanceOf[Number]), sj.read[Holder[java.lang.Number]]("""{"value":42.5}""".asInstanceOf[JSON])) - assertEquals(Holder(java.lang.Double.valueOf(42.5).asInstanceOf[Number]), sj.read[Holder[java.lang.Number]]("""{"value":"42.5"}""".asInstanceOf[JSON])) - // assertEquals(Holder[java.lang.Number](null), sj.read[Holder[java.lang.Number]]("""{"value":null}""".asInstanceOf[JSON])) - - val msg = """Expected a java.lang.Number here - |{"value":""} - |----------^""".stripMargin - interceptMessage[co.blocke.scalajack.ScalaJackError](msg){ - sj.read[Holder[java.lang.Number]]("""{"value":""}""".asInstanceOf[JSON]) - } - - assertEquals("""{"value":42.5}""".asInstanceOf[JSON], sj.render(Holder(java.lang.Double.valueOf(42.5).asInstanceOf[Number]))) - assertEquals("""{"value":null}""".asInstanceOf[JSON], sj.render(Holder[java.lang.Number](null))) - } \ No newline at end of file diff --git a/core/src/test/scala/co.blocke.scalajack/json/primitives/ScalaPrim.scala b/core/src/test/scala/co.blocke.scalajack/json/primitives/ScalaPrim.scala deleted file mode 100644 index ec405651..00000000 --- a/core/src/test/scala/co.blocke.scalajack/json/primitives/ScalaPrim.scala +++ /dev/null @@ -1,334 +0,0 @@ -package co.blocke.scalajack -package json.primitives - -import co.blocke.scala_reflection._ -import scala.math.BigDecimal -import java.util.UUID -import TestUtil._ -import munit._ -import munit.internal.console -import co.blocke.scalajack.json.JSON - -class ScalaPrim() extends FunSuite: - - val sj = co.blocke.scalajack.ScalaJack() - - test("BigDecimal must work") { - describe("---------------------------\n: Scala Primitive Tests :\n---------------------------", Console.BLUE) - describe("+++ Positive Tests +++") - - val inst = SampleBigDecimal( - BigDecimal(123L), - BigDecimal(1.23), - BigDecimal(0), - BigDecimal("123.456"), - BigDecimal( - "0.1499999999999999944488848768742172978818416595458984375" - ), - null - ) - - val js = sj.render(inst) - assertEquals( - """{"bd1":123,"bd2":1.23,"bd3":0,"bd4":123.456,"bd5":0.1499999999999999944488848768742172978818416595458984375,"bd6":null}""".asInstanceOf[JSON], - js - ) - assertEquals(inst, sj.read[SampleBigDecimal](js)) - } - - test("BigInt must work") { - val inst = SampleBigInt( - BigInt("-90182736451928374653345"), - BigInt("90182736451928374653345"), - BigInt(0), - null - ) - val js = sj.render(inst) - assertEquals( - """{"bi1":-90182736451928374653345,"bi2":90182736451928374653345,"bi3":0,"bi4":null}""".asInstanceOf[JSON], - js - ) - assertEquals(inst, sj.read[SampleBigInt](js)) - } - - test("Binary must work") { - val inst = SampleBinary( - null, - hexStringToByteArray("e04fd020ea3a6910a2d808002b30309d") - ) - val js = sj.render(inst) - // assertEquals("""{"b1":null,"b2":"4E/QIOo6aRCi2AgAKzAwnQ=="}""".asInstanceOf[JSON], js) - val inst2 = sj.read[SampleBinary](js) - assert(null == inst2.b1) - assertEquals(inst.b2.toList, inst2.b2.toList) - } - - test("Boolean must work (not nullable)") { - val inst = SampleBoolean(bool1 = true, bool2 = false) - val js = sj.render(inst) - assertEquals("""{"bool1":true,"bool2":false}""".asInstanceOf[JSON], js) - assertEquals(inst, sj.read[SampleBoolean](js)) - } - - test("Byte must work (not nullable)") { - val inst = SampleByte(Byte.MaxValue, Byte.MinValue, 0, 64) - val js = sj.render(inst) - assertEquals("""{"b1":127,"b2":-128,"b3":0,"b4":64}""".asInstanceOf[JSON], js ) - assertEquals(inst, sj.read[SampleByte](js)) - } - - test("Char must work (not nullable)") { - val inst = SampleChar(Char.MaxValue, 'Z', '\u20A0') - val js = sj.render(inst) - assertEquals( - ("""{"c1":"\""" + """uffff","c2":"Z","c3":"\""" + """u20a0"}""").asInstanceOf[JSON], - js) - assertEquals(inst, sj.read[SampleChar](js)) - } - - test("Double must work (not nullable)") { - val inst = - SampleDouble(Double.MaxValue, Double.MinValue, 0.0, -123.4567) - val js = sj.render(inst) - assertEquals( - """{"d1":1.7976931348623157E308,"d2":-1.7976931348623157E308,"d3":0.0,"d4":-123.4567}""".asInstanceOf[JSON], - js) - assertEquals(inst, sj.read[SampleDouble](js)) - } - - test("Float must work") { - val inst = SampleFloat(Float.MaxValue, Float.MinValue, 0.0F, -123.4567F) - val js = sj.render(inst) - assertEquals( - """{"f1":3.4028235E38,"f2":-3.4028235E38,"f3":0.0,"f4":-123.4567}""".asInstanceOf[JSON], - js) - assertEquals(inst, sj.read[SampleFloat](js)) - } - test("Int must work (not nullable)") { - val inst = SampleInt(Int.MaxValue, Int.MinValue, 0, 123) - val js = sj.render(inst) - assertEquals("""{"i1":2147483647,"i2":-2147483648,"i3":0,"i4":123}""".asInstanceOf[JSON], js) - assertEquals(inst, sj.read[SampleInt](js)) - } - test("Long must work (not nullable)") { - val inst = SampleLong(Long.MaxValue, Long.MinValue, 0L, 123L) - val js = sj.render(inst) - assertEquals( - """{"l1":9223372036854775807,"l2":-9223372036854775808,"l3":0,"l4":123}""".asInstanceOf[JSON], - js) - assertEquals(inst, sj.read[SampleLong](js)) - } - test("Short must work (not nullable)") { - val inst = SampleShort(Short.MaxValue, Short.MinValue, 0, 123) - val js = sj.render(inst) - assertEquals("""{"s1":32767,"s2":-32768,"s3":0,"s4":123}""".asInstanceOf[JSON], js ) - assertEquals(inst, sj.read[SampleShort](js)) - } - test("String must work") { - val inst = SampleString("something\b\n\f\r\t☆", "", null) - val js = sj.render(inst) - // The weird '+' here is to break up the unicode so it won't be interpreted and wreck the test. - assertEquals( - ("""{"s1":"something\b\n\f\r\t\""" + """u2606","s2":"","s3":null}""").asInstanceOf[JSON], - js) - assertEquals(inst, sj.read[SampleString](js)) - } - - test("UUID must work") { - val inst = SampleUUID( - null, - UUID.fromString("580afe0d-81c0-458f-9e09-4486c7af0fe9") - ) - val js = sj.render(inst) - assertEquals( - """{"u1":null,"u2":"580afe0d-81c0-458f-9e09-4486c7af0fe9"}""".asInstanceOf[JSON], - js) - assertEquals(inst, sj.read[SampleUUID](js)) - } - - - //-------------------------------------------------------- - - - test("BigDecimal must break") { - describe("--- Negative Tests ---") - val js = - """{"bd1":123,"bd2":1.23,"bd3":0,"bd4":123.456,"bd5":"0.1499999999999999944488848768742172978818416595458984375","bd6":null}""".asInstanceOf[JSON] - val msg = - """Expected a Number here - |{"bd1":123,"bd2":1.23,"bd3":0,"bd4":123.456,"bd5":"0.149999999999999994448884... - |--------------------------------------------------^""".stripMargin - interceptMessage[co.blocke.scalajack.ScalaJackError](msg){ - sj.read[SampleBigDecimal](js) - } - } - - test("BigInt must break") { - val js = - """{"bi1":"-90182736451928374653345","bi2":90182736451928374653345,"bi3":0,"bi4":null}""".asInstanceOf[JSON] - val msg = - """Expected a Number here - |{"bi1":"-90182736451928374653345","bi2":90182736451928374653345,"bi3":0,"bi4"... - |-------^""".stripMargin - interceptMessage[co.blocke.scalajack.ScalaJackError](msg){ - sj.read[SampleBigInt](js) - } - } - - test("Boolean must break") { - val js = """{"bool1":true,"bool2":"false"}""".asInstanceOf[JSON] - val msg = """Expected a Boolean here - |{"bool1":true,"bool2":"false"} - |----------------------^""".stripMargin - interceptMessage[co.blocke.scalajack.ScalaJackError](msg){ - sj.read[SampleBoolean](js) - } - val js2 = """{"bool1":true,"bool2":123}""".asInstanceOf[JSON] - val msg2 = """Expected a Boolean here - |{"bool1":true,"bool2":123} - |----------------------^""".stripMargin - interceptMessage[co.blocke.scalajack.ScalaJackError](msg2){ - sj.read[SampleBoolean](js2) - } - val js3 = """{"bool1":true,"bool2":null}""".asInstanceOf[JSON] - val msg3 = """Expected a Boolean here - |{"bool1":true,"bool2":null} - |----------------------^""".stripMargin - interceptMessage[co.blocke.scalajack.ScalaJackError](msg3){ - sj.read[SampleBoolean](js3) - } - } - - test("Byte must break") { - val js = """{"b1":true,"b2":-128,"b3":0,"b4":64}""".asInstanceOf[JSON] - val msg = """Expected a Number here - |{"b1":true,"b2":-128,"b3":0,"b4":64} - |------^""".stripMargin - interceptMessage[co.blocke.scalajack.ScalaJackError](msg){ - sj.read[SampleByte](js) - } - val js2 = """{"b1":12,"b2":-128,"b3":0,"b4":null}""".asInstanceOf[JSON] - val msg2 = """Expected a Number here - |{"b1":12,"b2":-128,"b3":0,"b4":null} - |-------------------------------^""".stripMargin - interceptMessage[co.blocke.scalajack.ScalaJackError](msg2){ - sj.read[SampleByte](js2) - } - } - - test("Char must break") { - val js = """{"c1":null,"c2":"Y","c3":"Z"}""".asInstanceOf[JSON] - val msg = """A Char typed value cannot be null - |{"c1":null,"c2":"Y","c3":"Z"} - |---------^""".stripMargin - interceptMessage[co.blocke.scalajack.ScalaJackError](msg){ - sj.read[SampleChar](js) - } - val js2 = """{"c1":"","c2":"Y","c3":"Z"}""".asInstanceOf[JSON] - val msg2 = """Tried to read a Char but empty string found - |{"c1":"","c2":"Y","c3":"Z"} - |-------^""".stripMargin - interceptMessage[co.blocke.scalajack.ScalaJackError](msg2){ - sj.read[SampleChar](js2) - } - } - - test("Double must break") { - val js = - """{"d1":1.79769313486E23157E308,"d2":-1.7976931348623157E308,"d3":0.0,"d4":-123.4567}""".asInstanceOf[JSON] - val msg = - """Cannot parse an Double from value - |{"d1":1.79769313486E23157E308,"d2":-1.7976931348623157E308,"d3":0.0,"d4":-123... - |----------------------------^""".stripMargin - interceptMessage[co.blocke.scalajack.ScalaJackError](msg){ - sj.read[SampleDouble](js) - } - } - - test("Float must break") { - val js = - """{"f1":3.4028235E38,"f2":"-3.4028235E38","f3":0.0,"f4":-123.4567}""".asInstanceOf[JSON] - val msg = - """Expected a Number here - |{"f1":3.4028235E38,"f2":"-3.4028235E38","f3":0.0,"f4":-123.4567} - |------------------------^""".stripMargin - interceptMessage[co.blocke.scalajack.ScalaJackError](msg){ - sj.read[SampleFloat](js) - } - } - - test("Int must break") { - val js = """{"i1":2147483647,"i2":-2147483648,"i3":"0","i4":123}""".asInstanceOf[JSON] - val msg = """Expected a Number here - |{"i1":2147483647,"i2":-2147483648,"i3":"0","i4":123} - |---------------------------------------^""".stripMargin - interceptMessage[co.blocke.scalajack.ScalaJackError](msg){ - sj.read[SampleInt](js) - } - val js2 = """{"i1":2147483647,"i2":-2147483648,"i3":2.3,"i4":123}""".asInstanceOf[JSON] - val msg2 = """Cannot parse an Int from value - |{"i1":2147483647,"i2":-2147483648,"i3":2.3,"i4":123} - |-----------------------------------------^""".stripMargin - interceptMessage[co.blocke.scalajack.ScalaJackError](msg2){ - sj.read[SampleInt](js2) - } - } - - test("Long must break") { - val js = - """{"l1":9223372036854775807,"l2":-9223372036854775808,"l3":true,"l4":123}""".asInstanceOf[JSON] - val msg = - """Expected a Number here - |...23372036854775807,"l2":-9223372036854775808,"l3":true,"l4":123} - |----------------------------------------------------^""".stripMargin - interceptMessage[co.blocke.scalajack.ScalaJackError](msg){ - sj.read[SampleLong](js) - } - val js2 = - """{"l1":9223372036854775807,"l2":-9223372036854775808,"l3":0.3,"l4":123}""".asInstanceOf[JSON] - val msg2 = - """Cannot parse an Long from value - |...372036854775807,"l2":-9223372036854775808,"l3":0.3,"l4":123} - |----------------------------------------------------^""".stripMargin - interceptMessage[co.blocke.scalajack.ScalaJackError](msg2){ - sj.read[SampleLong](js2) - } - } - - test("Short must break") { - val js = """{"s1":32767,"s2":true,"s3":0,"s4":123}""".asInstanceOf[JSON] - val msg = """Expected a Number here - |{"s1":32767,"s2":true,"s3":0,"s4":123} - |-----------------^""".stripMargin - interceptMessage[co.blocke.scalajack.ScalaJackError](msg){ - sj.read[SampleShort](js) - } - val js2 = """{"s1":32767,"s2":3.4,"s3":0,"s4":123}""".asInstanceOf[JSON] - val msg2 = """Cannot parse an Short from value - |{"s1":32767,"s2":3.4,"s3":0,"s4":123} - |-------------------^""".stripMargin - interceptMessage[co.blocke.scalajack.ScalaJackError](msg2){ - sj.read[SampleShort](js2) - } - } - - test("String must break") { - val js = """{"s1":"something","s2":-19,"s3":null}""".asInstanceOf[JSON] - val msg = """Expected a String here - |{"s1":"something","s2":-19,"s3":null} - |-----------------------^""".stripMargin - interceptMessage[co.blocke.scalajack.ScalaJackError](msg){ - sj.read[SampleString](js) - } - } - - test("UUID must break") { - val js = - """{"u1":"bogus","u2":"580afe0d-81c0-458f-9e09-4486c7af0fe9"}""".asInstanceOf[JSON] - val msg = """Failed to create UUID value from parsed text bogus - |{"u1":"bogus","u2":"580afe0d-81c0-458f-9e09-4486c7af0fe9"} - |------------^""".stripMargin - interceptMessage[co.blocke.scalajack.ScalaJackError](msg){ - sj.read[SampleUUID](js) - } - } diff --git a/core/src/test/scala/co.blocke.scalajack/json/primitives/TimePrim.scala b/core/src/test/scala/co.blocke.scalajack/json/primitives/TimePrim.scala deleted file mode 100644 index b908f511..00000000 --- a/core/src/test/scala/co.blocke.scalajack/json/primitives/TimePrim.scala +++ /dev/null @@ -1,299 +0,0 @@ -package co.blocke.scalajack -package json.primitives - -import co.blocke.scala_reflection._ -import TestUtil._ -import munit._ -import munit.internal.console -import co.blocke.scalajack.json.JSON -import java.time._ - -class TimePrim() extends FunSuite: - - val sj = co.blocke.scalajack.ScalaJack() - - test("Duration must work") { - describe("--------------------------\n: Time Primitive Tests :\n--------------------------", Console.BLUE) - describe("+++ Positive Tests +++") - val inst = SampleDuration(Duration.ZERO, Duration.parse("P2DT3H4M"), null) - val js = sj.render(inst) - assertEquals("""{"d1":"PT0S","d2":"PT51H4M","d3":null}""".asInstanceOf[JSON],js) - assert(inst == sj.read[SampleDuration](js)) - } - - test("Instant must work") { - val inst = SampleInstant( - Instant.EPOCH, - Instant.MAX, - Instant.MIN, - Instant.parse("2007-12-03T10:15:30.00Z"), - null - ) - val js = sj.render(inst) - assertEquals( - """{"i1":"1970-01-01T00:00:00Z","i2":"+1000000000-12-31T23:59:59.999999999Z","i3":"-1000000000-01-01T00:00:00Z","i4":"2007-12-03T10:15:30Z","i5":null}""".asInstanceOf[JSON],js) - assert(inst == sj.read[SampleInstant](js)) - } - - test("LocalDateTime must work") { - val inst = SampleLocalDateTime( - LocalDateTime.MAX, - LocalDateTime.MIN, - LocalDateTime.parse("2007-12-03T10:15:30"), - null - ) - val js = sj.render(inst) - assertEquals( - """{"d1":"+999999999-12-31T23:59:59.999999999","d2":"-999999999-01-01T00:00:00","d3":"2007-12-03T10:15:30","d4":null}""".asInstanceOf[JSON],js) - assert(inst == sj.read[SampleLocalDateTime](js)) - } - - test("LocalDate must work") { - val inst = SampleLocalDate( - LocalDate.MAX, - LocalDate.MIN, - LocalDate.parse("2007-12-03"), - null - ) - val js = sj.render(inst) - assertEquals( - """{"d1":"+999999999-12-31","d2":"-999999999-01-01","d3":"2007-12-03","d4":null}""".asInstanceOf[JSON],js) - assert(inst == sj.read[SampleLocalDate](js)) - } - - test("LocalTime must work") { - val inst = SampleLocalTime( - LocalTime.MAX, - LocalTime.MIN, - LocalTime.MIDNIGHT, - LocalTime.NOON, - LocalTime.parse("10:15:30"), - null - ) - val js = sj.render(inst) - assertEquals( - """{"d1":"23:59:59.999999999","d2":"00:00:00","d3":"00:00:00","d4":"12:00:00","d5":"10:15:30","d6":null}""".asInstanceOf[JSON],js) - assert(inst == sj.read[SampleLocalTime](js)) - } - - test("OffsetDateTime must work") { - val inst = SampleOffsetDateTime( - OffsetDateTime.MAX, - OffsetDateTime.MIN, - OffsetDateTime.parse("2007-12-03T10:15:30+01:00"), - null - ) - val js = sj.render(inst) - assertEquals( - """{"o1":"+999999999-12-31T23:59:59.999999999-18:00","o2":"-999999999-01-01T00:00:00+18:00","o3":"2007-12-03T10:15:30+01:00","o4":null}""".asInstanceOf[JSON],js) - assert(inst == sj.read[SampleOffsetDateTime](js)) - } - - test("OffsetTime must work") { - val inst = SampleOffsetTime( - OffsetTime.MAX, - OffsetTime.MIN, - OffsetTime.parse("10:15:30+01:00"), - null - ) - val js = sj.render(inst) - assertEquals( - """{"o1":"23:59:59.999999999-18:00","o2":"00:00:00+18:00","o3":"10:15:30+01:00","o4":null}""".asInstanceOf[JSON],js) - assert(inst == sj.read[SampleOffsetTime](js)) - } - - test("Period must work") { - val inst = SamplePeriod(Period.ZERO, Period.parse("P1Y2M3D"), null) - val js = sj.render(inst) - assertEquals("""{"p1":"P0D","p2":"P1Y2M3D","p3":null}""".asInstanceOf[JSON],js) - assert(inst == sj.read[SamplePeriod](js)) - } - - test("ZonedDateTime must work") { - val inst = SampleZonedDateTime( - ZonedDateTime.parse("2007-12-03T10:15:30+01:00[Europe/Paris]"), - null - ) - val js = sj.render(inst) - assertEquals("""{"o1":"2007-12-03T10:15:30+01:00[Europe/Paris]","o2":null}""".asInstanceOf[JSON],js) - assert(inst == sj.read[SampleZonedDateTime](js)) - } - - test("Duration must break") { - describe("--- Negative Tests ---") - - val js = """{"d1":"PT0S","d2":21,"d3":null}""".asInstanceOf[JSON] - val msg = """Expected a String here - |{"d1":"PT0S","d2":21,"d3":null} - |------------------^""".stripMargin - interceptMessage[co.blocke.scalajack.ScalaJackError](msg){ - sj.read[SampleDuration](js) - } - val js2 = """{"d1":"PT0S","d2":"bogus","d3":null}""".asInstanceOf[JSON] - val msg2 = """Failed to parse Duration from input 'bogus' - |{"d1":"PT0S","d2":"bogus","d3":null} - |------------------------^""".stripMargin - interceptMessage[co.blocke.scalajack.ScalaJackError](msg2){ - sj.read[SampleDuration](js2) - } - } - - test("Instant must break") { - val js = - """{"i1":"1970-01-01T00:00:00Z","i2":false,"i3":"-1000000000-01-01T00:00:00Z","i4":"2007-12-03T10:15:30Z","i5":null}""".asInstanceOf[JSON] - val msg = - """Expected a String here - |{"i1":"1970-01-01T00:00:00Z","i2":false,"i3":"-1000000000-01-01T00:00:00Z","i... - |----------------------------------^""".stripMargin - interceptMessage[co.blocke.scalajack.ScalaJackError](msg){ - sj.read[SampleInstant](js) - } - val js2 = - """{"i1":"1970-01-01T00:00:00Z","i2":"bogus","i3":"-1000000000-01-01T00:00:00Z","i4":"2007-12-03T10:15:30Z","i5":null}""".asInstanceOf[JSON] - val msg2 = - """Failed to parse Instant from input 'bogus' - |{"i1":"1970-01-01T00:00:00Z","i2":"bogus","i3":"-1000000000-01-01T00:00:00Z",... - |----------------------------------------^""".stripMargin - interceptMessage[co.blocke.scalajack.ScalaJackError](msg2){ - sj.read[SampleInstant](js2) - } - } - - test("LocalDateTime must break") { - val js = - """{"d1":-1,"d2":"-999999999-01-01T00:00:00","d3":"2007-12-03T10:15:30","d4":null}""".asInstanceOf[JSON] - val msg = - """Expected a String here - |{"d1":-1,"d2":"-999999999-01-01T00:00:00","d3":"2007-12-03T10:15:30","d4":null} - |------^""".stripMargin - interceptMessage[co.blocke.scalajack.ScalaJackError](msg){ - sj.read[SampleLocalDateTime](js) - } - val js2 = - """{"d1":"bogus","d2":"-999999999-01-01T00:00:00","d3":"2007-12-03T10:15:30","d1":null}""".asInstanceOf[JSON] - val msg2 = - """Failed to parse LocalDateTime from input 'bogus' - |{"d1":"bogus","d2":"-999999999-01-01T00:00:00","d3":"2007-12-03T10:15:30","d1... - |------------^""".stripMargin - interceptMessage[co.blocke.scalajack.ScalaJackError](msg2){ - sj.read[SampleLocalDateTime](js2) - } - } - - test("LocalDate must break") { - val js = - """{"d1":-1,"d2":"-999999999-01-01","d3":"2007-12-03","d4":null}""".asInstanceOf[JSON] - val msg = """Expected a String here - |{"d1":-1,"d2":"-999999999-01-01","d3":"2007-12-03","d4":null} - |------^""".stripMargin - interceptMessage[co.blocke.scalajack.ScalaJackError](msg){ - sj.read[SampleLocalDate](js) - } - val js2 = - """{"d1":"bogus","d2":"-999999999-01-01","d3":"2007-12-03","d4":null}""".asInstanceOf[JSON] - val msg2 = - """Failed to parse LocalDate from input 'bogus' - |{"d1":"bogus","d2":"-999999999-01-01","d3":"2007-12-03","d4":null} - |------------^""".stripMargin - interceptMessage[co.blocke.scalajack.ScalaJackError](msg2){ - sj.read[SampleLocalDate](js2) - } - } - - test("LocalTime must break") { - val js = - """{"d1":"23:59:59.999999999","d2":"00:00:00","d3":"00:00:00","d4":"12:00:00","d5":false,"d6":null}""".asInstanceOf[JSON] - val msg = - """Expected a String here - |...:"00:00:00","d3":"00:00:00","d4":"12:00:00","d5":false,"d6":null} - |----------------------------------------------------^""".stripMargin - interceptMessage[co.blocke.scalajack.ScalaJackError](msg){ - sj.read[SampleLocalTime](js) - } - val js2 = - """{"d1":"23:59:59.999999999","d2":"00:00:00","d3":"00:00:00","d4":"12:00:00","d5":"Bogus","d6":null}""".asInstanceOf[JSON] - val msg2 = - """Failed to parse LocalTime from input 'Bogus' - |...0:00","d3":"00:00:00","d4":"12:00:00","d5":"Bogus","d6":null} - |----------------------------------------------------^""".stripMargin - interceptMessage[co.blocke.scalajack.ScalaJackError](msg2){ - sj.read[SampleLocalTime](js2) - } - } - - test("OffsetDateTime must break") { - val js = - """{"o1":"+999999999-12-31T23:59:59.999999999-18:00","o2":2,"o3":"2007-12-03T10:15:30+01:00","o4":null}""".asInstanceOf[JSON] - val msg = - """Expected a String here - |..."+999999999-12-31T23:59:59.999999999-18:00","o2":2,"o3":"2007-12-03T10:15:30... - |----------------------------------------------------^""".stripMargin - interceptMessage[co.blocke.scalajack.ScalaJackError](msg){ - sj.read[SampleOffsetDateTime](js) - } - val js2 = - """{"o1":"+999999999-12-31T23:59:59.999999999-18:00","o2":"-999999999-01T00:00:00+18:00","o3":"2007-12-03T10:15:30+01:00","o4":null}""".asInstanceOf[JSON] - val msg2 = - """Failed to parse OffsetDateTime from input '-999999999-01T00:00:00+18:00' - |...9999999-18:00","o2":"-999999999-01T00:00:00+18:00","o3":"2007-12-03T10:15:30... - |----------------------------------------------------^""".stripMargin - interceptMessage[co.blocke.scalajack.ScalaJackError](msg2){ - sj.read[SampleOffsetDateTime](js2) - } - } - - test("OffsetTime must break") { - val js = - """{"o1":"23:59:59.999999999-18:00","o2":false,"o3":"10:15:30+01:00","o4":null}""".asInstanceOf[JSON] - val msg = - """Expected a String here - |{"o1":"23:59:59.999999999-18:00","o2":false,"o3":"10:15:30+01:00","o4":null} - |--------------------------------------^""".stripMargin - interceptMessage[co.blocke.scalajack.ScalaJackError](msg){ - sj.read[SampleOffsetTime](js) - } - val js2 = - """{"o1":"23:59:59.999999999-18:00","o2":"00:00:00:00+18:00","o3":"10:15:30+01:00","o4":null}""".asInstanceOf[JSON] - val msg2 = - """Failed to parse OffsetTime from input '00:00:00:00+18:00' - |...23:59:59.999999999-18:00","o2":"00:00:00:00+18:00","o3":"10:15:30+01:00","o4... - |----------------------------------------------------^""".stripMargin - interceptMessage[co.blocke.scalajack.ScalaJackError](msg2){ - sj.read[SampleOffsetTime](js2) - } - } - - test("Period must break") { - val js = """{"p1":"P0D","p2":5,"p3":null}""".asInstanceOf[JSON] - val msg = """Expected a String here - |{"p1":"P0D","p2":5,"p3":null} - |-----------------^""".stripMargin - interceptMessage[co.blocke.scalajack.ScalaJackError](msg){ - sj.read[SamplePeriod](js) - } - val js2 = """{"p1":"P0D","p2":"bogus","p3":null}""".asInstanceOf[JSON] - val msg2 = """Failed to parse Period from input 'bogus' - |{"p1":"P0D","p2":"bogus","p3":null} - |-----------------------^""".stripMargin - interceptMessage[co.blocke.scalajack.ScalaJackError](msg2){ - sj.read[SamplePeriod](js2) - } - } - - test("ZonedDateTime must break") { - val js = """{"o1":true,"o2":null}""".asInstanceOf[JSON] - val msg = """Expected a String here - |{"o1":true,"o2":null} - |------^""".stripMargin - interceptMessage[co.blocke.scalajack.ScalaJackError](msg){ - sj.read[SampleZonedDateTime](js) - } - val js2 = """{"o1":"2007-12-03T10:15:30+01:00 Earth","o2":null}""".asInstanceOf[JSON] - val msg2 = - """Failed to parse ZonedDateTime from input '2007-12-03T10:15:30+01:00 Earth' - |{"o1":"2007-12-03T10:15:30+01:00 Earth","o2":null} - |--------------------------------------^""".stripMargin - interceptMessage[co.blocke.scalajack.ScalaJackError](msg2){ - sj.read[SampleZonedDateTime](js2) - } - } diff --git a/core/src/test/scala/co.blocke.scalajack/json/primitives/ValueClassPrim.scala b/core/src/test/scala/co.blocke.scalajack/json/primitives/ValueClassPrim.scala deleted file mode 100644 index f71c126c..00000000 --- a/core/src/test/scala/co.blocke.scalajack/json/primitives/ValueClassPrim.scala +++ /dev/null @@ -1,179 +0,0 @@ -package co.blocke.scalajack -package json.primitives - -import TestUtil._ -import munit._ -import munit.internal.console -import co.blocke.scalajack.json.JSON -import co.blocke.scala_reflection._ -import java.util.UUID - - -class ValueClassPrim() extends FunSuite: - - val sj = co.blocke.scalajack.ScalaJack() - - test("Value class of BigDecimal") { - describe("--------------------------------\n: ValueClass Primitive Tests :\n--------------------------------", Console.BLUE) - describe("+++ Positive Tests +++") - - val inst = VCBigDecimal(BigDecimal(12.34)) - val js = sj.render(inst) - assertEquals("""12.34""".asInstanceOf[JSON], js) - assertEquals(inst, sj.read[VCBigDecimal](js)) - } - - test("Value class of BigDecimal with null") { - val inst = VCBigDecimal(null) - val js = sj.render(inst) - assertEquals("""null""".asInstanceOf[JSON], js) - assertEquals(inst, sj.read[VCBigDecimal](js)) - } - - test("Value class of BigInt") { - val inst = VCBigInt(BigInt(1)) - val js = sj.render(inst) - assertEquals("""1""".asInstanceOf[JSON], js) - assertEquals(inst, sj.read[VCBigInt](js)) - } - - test("Value class of BigInt with null") { - val inst = VCBigInt(null) - val js = sj.render(inst) - assertEquals("""null""".asInstanceOf[JSON], js) - assertEquals(inst, sj.read[VCBigInt](js)) - } - - test("Value class of Byte") { - val inst = VCByte(100.asInstanceOf[Byte]) - val js = sj.render(inst) - assertEquals("""100""".asInstanceOf[JSON], js) - assertEquals(inst, sj.read[VCByte](js)) - } - - test("Value class of Boolean") { - val inst = VCBoolean(false) - val js = sj.render(inst) - assertEquals("""false""".asInstanceOf[JSON], js) - assertEquals(inst, sj.read[VCBoolean](js)) - } - - test("Value class of Char") { - val inst = VCChar('Z') - val js = sj.render(inst) - assertEquals(""""Z"""".asInstanceOf[JSON], js) - assertEquals(inst, sj.read[VCChar](js)) - } - - test("Value class of Double") { - val inst = VCDouble(100.5) - val js = sj.render(inst) - assertEquals("""100.5""".asInstanceOf[JSON], js) - assertEquals(inst, sj.read[VCDouble](js)) - } - - test("Value class of Enumeration") { - val inst = VCEnumeration(Size.Medium) - val js = sj.render(inst) - assertEquals(""""Medium"""".asInstanceOf[JSON], js) - assertEquals(inst, sj.read[VCEnumeration](js)) - } - - test("Value class of Enumeration with null") { - val inst = VCEnumeration(null) - val js = sj.render(inst) - assertEquals("""null""".asInstanceOf[JSON], js) - assertEquals(inst, sj.read[VCEnumeration](js)) - } - - test("Value class of Enum") { - val inst = VCEnum(Color.Green) - val js = sj.render(inst) - assertEquals(""""Green"""".asInstanceOf[JSON], js) - assertEquals(inst, sj.read[VCEnum](js)) - } - - test("Value class of Enum with null") { - val inst = VCEnum(null) - val js = sj.render(inst) - assertEquals("""null""".asInstanceOf[JSON], js) - assertEquals(inst, sj.read[VCEnum](js)) - } - - test("Value class of Float") { - val inst = VCFloat(100.5F) - val js = sj.render(inst) - assertEquals("""100.5""".asInstanceOf[JSON], js) - assertEquals(inst, sj.read[VCFloat](js)) - } - - test("Value class of Int") { - val inst = VCInt(100) - val js = sj.render(inst) - assertEquals("""100""".asInstanceOf[JSON], js) - assertEquals(inst, sj.read[VCInt](js)) - } - - test("Value class of Long") { - val inst = VCLong(100L) - val js = sj.render(inst) - assertEquals("""100""".asInstanceOf[JSON], js) - assertEquals(inst, sj.read[VCLong](js)) - } - - test("Value class of Short") { - val inst = VCShort(100.asInstanceOf[Short]) - val js = sj.render(inst) - assertEquals("""100""".asInstanceOf[JSON], js) - assertEquals(inst, sj.read[VCShort](js)) - } - - test("Value class of String") { - val inst = VCString("foo") - val js = sj.render(inst) - assertEquals(""""foo"""".asInstanceOf[JSON], js) - assertEquals(inst, sj.read[VCString](js)) - } - - test("Value class of String with null") { - val inst = VCString(null) - val js = sj.render(inst) - assertEquals("""null""".asInstanceOf[JSON], js) - assertEquals(inst, sj.read[VCString](js)) - } - - test("Value class of UUID") { - val inst = VCUUID(UUID.fromString("54cab778-7b9e-4b07-9d37-87b97a011e55")) - val js = sj.render(inst) - assertEquals(""""54cab778-7b9e-4b07-9d37-87b97a011e55"""".asInstanceOf[JSON], js) - assertEquals(inst, sj.read[VCUUID](js)) - } - - test("Value class of UUID with null") { - val inst = VCUUID(null) - val js = sj.render(inst) - assertEquals("""null""".asInstanceOf[JSON], js) - assertEquals(inst, sj.read[VCUUID](js)) - } - - test("Value class of Number") { - val inst = VCNumber(25) - val js = sj.render(inst) - assertEquals("""25""".asInstanceOf[JSON], js) - assertEquals((inst,true), { - val r = sj.read[VCNumber](js) - (r, r.vc.isInstanceOf[Byte]) - }) - } - - test("Wrong JSON for wrapped type") { - describe("--- Negative Tests ---") - - val js = """100.25""".asInstanceOf[JSON] - val msg = """Cannot parse an Short from value - |100.25 - |-----^""".stripMargin - interceptMessage[co.blocke.scalajack.ScalaJackError](msg){ - sj.read[VCShort](js) - } - } diff --git a/core/src/test/scala/co.blocke.scalajack/json/structures/Eithers.scala b/core/src/test/scala/co.blocke.scalajack/json/structures/Eithers.scala deleted file mode 100644 index 9eadc235..00000000 --- a/core/src/test/scala/co.blocke.scalajack/json/structures/Eithers.scala +++ /dev/null @@ -1,96 +0,0 @@ -package co.blocke.scalajack -package json.structures - -import co.blocke.scala_reflection._ -import TestUtil._ -import munit._ -import munit.internal.console -import co.blocke.scalajack.json.JSON - - -class Eithers() extends FunSuite: - - val sj = co.blocke.scalajack.ScalaJack() - - test("Left - two class types") { - describe("------------------\n: Either Tests :\n------------------",Console.BLUE) - describe("+++ Positive Tests +++") - - val inst: Either[Parrot, DumpTruck] = Left(Parrot("blue")) - try { - val js = sj.render(inst) - assertEquals("""{"color":"blue"}""".asInstanceOf[JSON],js) - assertEquals(inst,sj.read[Either[Parrot, DumpTruck]](js)) - } catch { - case t: Throwable => println("Boom") - } - } - - test("Right - two class types") { - val inst: Either[Parrot, DumpTruck] = Right(DumpTruck(axles = 2)) - val js = sj.render(inst) - assertEquals("""{"axles":2}""".asInstanceOf[JSON],js) - assertEquals(inst,sj.read[Either[Parrot, DumpTruck]](js)) - } - - test("Left - class type and scalar type") { - val inst: Either[Parrot, String] = Left(Parrot("red")) - val js = sj.render(inst) - assertEquals("""{"color":"red"}""".asInstanceOf[JSON],js) - assert(inst == sj.read[Either[Parrot, DumpTruck]](js)) - } - - test("Right - class type and scalar type") { - val inst = EitherHolder[Parrot, String](Right("quack")) - val js = sj.render(inst) - assertEquals("""{"either":"quack"}""".asInstanceOf[JSON],js) - assertEquals(inst,sj.read[EitherHolder[Parrot, String]](js)) - } - - test("Either is null") { - val inst: Either[Parrot, String] = null - val js = sj.render(inst) - assertEquals("null".asInstanceOf[JSON],js) - assert(null == sj.read[Either[Parrot, String]](js)) - } - - test("Different classes with identical fields--favor Right") { - val js = """{"numLegs":4}""".asInstanceOf[JSON] - assertEquals(sj.read[Either[Chair, Table]](js),Right(Table(4))) - } - - test("Handles traits - Right") { - val inst = EitherHolder[String, Pet](Right(Dog("Fido", 13))) - val js = sj.render(inst) - assertEquals( - """{"either":{"_hint":"co.blocke.scalajack.json.structures.Dog","name":"Fido","kind":13}}""".asInstanceOf[JSON],js) - assertEquals(inst,sj.read[EitherHolder[String, Pet]](js)) - } - - test("Handles traits - Left") { - val inst = EitherHolder[Pet, String](Left(Dog("Fido", 13))) - val js = sj.render(inst) - assertEquals( - """{"either":{"_hint":"co.blocke.scalajack.json.structures.Dog","name":"Fido","kind":13}}""".asInstanceOf[JSON],js) - assertEquals(inst,sj.read[EitherHolder[Pet, String]](js)) - } - - test("Same instance Left and Right") { - describe("--- Negative Tests ---",Console.BLUE) - val js = "\"foo\"" - val msg = - """Types java.lang.String and java.lang.String are not mutually exclusive""".stripMargin - interceptMessage[IllegalArgumentException](msg){ - sj.read[Either[String, String]](js.asInstanceOf[JSON]) - } - } - - test("Neither value works") { - val js = "25" - val msg = """Failed to read either side of Either - |25 - |^""".stripMargin - interceptMessage[co.blocke.scalajack.ScalaJackError](msg){ - sj.read[Either[String, Boolean]](js.asInstanceOf[JSON]) - } - } diff --git a/core/src/test/scala/co.blocke.scalajack/json/structures/Model.scala b/core/src/test/scala/co.blocke.scalajack/json/structures/Model.scala deleted file mode 100644 index d7282ff5..00000000 --- a/core/src/test/scala/co.blocke.scalajack/json/structures/Model.scala +++ /dev/null @@ -1,55 +0,0 @@ -package co.blocke.scalajack -package json.structures - - -// === Eithers -case class Parrot(color: String) -case class DumpTruck(axles: Int) -case class EitherHolder[L, R](either: Either[L, R]) - -case class Chair(numLegs: Int) -case class Table(numLegs: Int) - -trait Pet { val name: String } -case class Dog[A](name: String, kind: A) extends Pet - - -// === Structures -trait Body -case class FancyBody(message: String) extends Body -case class DefaultBody(message: String = "Unknown body") extends Body -case class AnyBody(stuff: Any) extends Body - -trait Hobby -case class InsideHobby(desc: String) extends Hobby - -case class Envelope[T <: Body](id: String, body: T) { - type Giraffe = T -} - -// Type member X should be ignored! Only used internally -case class BigEnvelope[T <: Body, H <: Hobby, X]( - id: String, - body: T, - hobby: H) { - type Giraffe = T - type Hippo = H - type IgnoreMe = X - - val x: IgnoreMe = null.asInstanceOf[IgnoreMe] -} - -case class Bigger(foo: Int, env: Envelope[FancyBody]) - -// === Unions -case class Person(name: String, age: Int) -case class Multi2(one: List[Boolean] | List[String]) -case class Multi3(one: List[String] | List[Int] | Boolean) -case class Multi4(one: List[String] | List[Int] | Boolean | Person) - -// === Intersections -trait InterA{ val a: Int } -trait InterB{ val b: Boolean } -trait InterC{ val c: Char } -case class InterImpl(a: Int, b: Boolean, c: Char) extends InterA with InterB with InterC -case class IntersectionHolder( a: InterA & InterB & InterC ) \ No newline at end of file diff --git a/core/src/test/scala/co.blocke.scalajack/json/structures/SealedTraits.scala b/core/src/test/scala/co.blocke.scalajack/json/structures/SealedTraits.scala deleted file mode 100644 index 2f3ced76..00000000 --- a/core/src/test/scala/co.blocke.scalajack/json/structures/SealedTraits.scala +++ /dev/null @@ -1,104 +0,0 @@ -package co.blocke.scalajack -package json.structures - -import co.blocke.scala_reflection._ -import TestUtil._ -import munit._ -import munit.internal.console -import co.blocke.scalajack.json.JSON - -// Unambiguous member names -sealed trait ContactPoint -case class EmailAddress(emailAddress: String) extends ContactPoint -case class PhoneNumber(phoneNumber: String) extends ContactPoint - -// Ambiguous member names -sealed trait Vehicle -case class Truck(numberOfWheels: Int) extends Vehicle -case class Car(numberOfWheels: Int, color: String) extends Vehicle -case class Plane(numberOfEngines: Int) extends Vehicle - -// Case object implementation -sealed trait Flavor -case object Vanilla extends Flavor -case object Chocolate extends Flavor -case object Bourbon extends Flavor - -sealed trait Stay -case class VillaStay(name: String) extends Stay -case class RanchStay(name: String) extends Stay - -case class NotSealed() - - -class SealedTraits() extends FunSuite: - - val sj = co.blocke.scalajack.ScalaJack() - - test("Read - unambiguous") { - describe("------------------------\n: Sealed Trait Tests :\n------------------------",Console.BLUE) - - assert(EmailAddress("foo@bar.com") == sj.read[ContactPoint]("""{"emailAddress":"foo@bar.com"}""".asInstanceOf[JSON])) - } - - test("Write - unambiguous") { - assertEquals("""{"phoneNumber":"12223334444"}""".asInstanceOf[JSON], sj.render[ContactPoint](PhoneNumber("12223334444"))) - } - - test("Read - ambiguous") { - val msg = - """Multiple sub-classes of co.blocke.scalajack.json.structures.Stay match field names Set(name) - |{"name":"Wilderness"} - |--------------------^""".stripMargin - val js = """{"name":"Wilderness"}""".asInstanceOf[JSON] - interceptMessage[co.blocke.scalajack.ScalaJackError](msg){ - sj.read[Stay](js) - } - } - - test("Write - ambiguous") { - assertEquals( - """{"name":"Wilderness"}""".asInstanceOf[JSON], sj.render[Stay](VillaStay("Wilderness"))) - } - - test("Case object implementation") { - val flavors: List[Flavor] = List(Bourbon, Vanilla, Chocolate) - val js = sj.render(flavors) - assertEquals("""["Bourbon","Vanilla","Chocolate"]""".asInstanceOf[JSON],js) - assertEquals(flavors, sj.read[List[Flavor]](js) ) - } - - // No more hints on sealed traits!! - // test("Type hints with modification") { - // val sj2 = co.blocke.scalajack.ScalaJack().withHints((RType.of[Stay] -> "stay_kind")) - // val s: Stay = VillaStay("Hacienda") - // val js = sj2.render(s) - // assertEquals( - // """{"stay_kind":"co.blocke.scalajack.json.misc.VillaStay","name":"Hacienda"}""".asInstanceOf[JSON],js) - // assertEquals(s, sj2.read[Stay](js) ) - // } - - test("Handles null") { - val js = """null""".asInstanceOf[JSON] - val inst = sj.read[ContactPoint](js) - assertEquals(inst, null) - assert( sj.render[ContactPoint](inst) == js) - } - - test("Handle not a sealed trait") { - val js = """{"d":3,"color":"Red"}""".asInstanceOf[JSON] - val msg = - """No sub-classes of co.blocke.scalajack.json.structures.ContactPoint match field names Set(d, color) - |{"d":3,"color":"Red"} - |--------------------^""".stripMargin - interceptMessage[co.blocke.scalajack.ScalaJackError](msg){ - sj.read[ContactPoint](js) - } - } - - test("Resolved ambiguous trait") { - val inst = Car(4,"Red") - val js = sj.render(inst) - assertEquals("""{"numberOfWheels":4,"color":"Red"}""".asInstanceOf[JSON], js) - assert(inst == sj.read[Vehicle](js)) - } \ No newline at end of file diff --git a/core/src/test/scala/co.blocke.scalajack/json/structures/TryAndCapture.scala b/core/src/test/scala/co.blocke.scalajack/json/structures/TryAndCapture.scala deleted file mode 100644 index 03ce9a82..00000000 --- a/core/src/test/scala/co.blocke.scalajack/json/structures/TryAndCapture.scala +++ /dev/null @@ -1,60 +0,0 @@ -package co.blocke.scalajack -package json.structures - -import co.blocke.scala_reflection._ -import TestUtil._ -import munit._ -import munit.internal.console -import co.blocke.scalajack.json.JSON -import scala.util._ -import json.JsonMatcher -import co.blocke.scalajack.SJCapture - -case class Embed(stuff: List[String], num: Int) -case class Boom(name: String, other: Try[Embed]) -case class Cap(name: String) extends SJCapture - - -class TryAndCapture() extends FunSuite: - - val sj = co.blocke.scalajack.ScalaJack() - - test("Try success") { - describe( "---------------------------\n: Try and Capture Tests :\n---------------------------",Console.BLUE) - describe("Try:") - - val js = """{"name":"Greg","other":{"stuff":["a","b","c"],"num":2}}""".asInstanceOf[JSON] - val obj = sj.read[Boom](js) - assertEquals(Boom("Greg", Success(Embed(List("a", "b", "c"), 2))), obj) - assertEquals("""{"name":"Greg","other":{"stuff":["a","b","c"],"num":2}}""".asInstanceOf[JSON], sj.render(obj) ) - } - - test("Try failure") { - val js = """{"name":"Greg","other":[1,2,3]}""".asInstanceOf[JSON] - val obj = sj.read[Boom](js) - val msg = """Expected start of object here - |{"name":"Greg","other":[1,2,3]} - |-----------------------^""".stripMargin - assertEquals(msg, obj.other.asInstanceOf[Failure[_]].exception.getMessage) - assertEquals(js, sj.render(obj) ) - } - - test("Try failure 2") { - val js = """{"name":"Greg","other": -12.45 ,"num":2}""".asInstanceOf[JSON] - val obj = sj.read[Boom](js) - val msg = """Expected start of object here - |{"name":"Greg","other": -12.45 ,"num":2} - |-------------------------^""".stripMargin - assertEquals(msg, obj.other.asInstanceOf[Failure[_]].exception.getMessage) - assert("""{"name":"Greg","other":-12.45}""".asInstanceOf[JSON] == sj.render(obj) ) - } - - test("Capture can write semantically equivalent JSON") { - describe("Capture:") - - val js = - """{"name":"Greg", "foo":[1,2,"t" ], "zing" : {"dot":{"age":25,"food":"Pizza"}}, "blather":"wow", "boo": -29384.34, "maybe": false }""".asInstanceOf[JSON] - val h = sj.read[Cap](js) - assertEquals(h, Cap("Greg")) - assert(JsonMatcher.jsonMatches(sj.render(h), js)) - } diff --git a/core/src/test/scala/co.blocke.scalajack/json/structures/TypeMembers.scala b/core/src/test/scala/co.blocke.scalajack/json/structures/TypeMembers.scala deleted file mode 100644 index a3821683..00000000 --- a/core/src/test/scala/co.blocke.scalajack/json/structures/TypeMembers.scala +++ /dev/null @@ -1,92 +0,0 @@ -package co.blocke.scalajack -package json.structures - -import co.blocke.scala_reflection._ -import TestUtil._ -import munit._ -import munit.internal.console -import co.blocke.scalajack.json.JSON - -trait Thing -case class BigThing(s: String) extends Thing -case class Boo[T <: Thing]( a: T ) - - -class TypeMembers extends FunSuite: - - val sj = co.blocke.scalajack.ScalaJack() - val sj2 = co.blocke.scalajack.ScalaJack().parseOrElse((RType.of[Body] -> RType.of[DefaultBody])) - - test("Read and match") { - describe("-----------------------------\n: Externalized Type Tests :\n-----------------------------",Console.BLUE) - - val json = - """{"Giraffe":"co.blocke.scalajack.json.structures.FancyBody","id":"ABC","body":{"message":"Hello"}}""".asInstanceOf[JSON] - val expected: Envelope[Body] = Envelope("ABC", FancyBody("Hello")) - val x = sj.read[Envelope[Body]](json) - // Test match functionality - val num = x.body match { - case _: FancyBody => 1 - case _ => 2 - } - assertEquals((expected, 1),(x, num)) - } - - test("Write -- Concrete T value") { - val value: Envelope[FancyBody] = Envelope("DEF", FancyBody("BOO")) - val expected = - """{"Giraffe":"co.blocke.scalajack.json.structures.FancyBody","id":"DEF","body":{"message":"BOO"}}""".asInstanceOf[JSON] - assertEquals(expected, sj.render[Envelope[FancyBody]](value)) - } - - test("Write -- Trait T value") { - val value: Envelope[Body] = Envelope("DEF", FancyBody("BOO")) - val expected = - """{"Giraffe":"co.blocke.scalajack.json.structures.FancyBody","id":"DEF","body":{"message":"BOO"}}""".asInstanceOf[JSON] - assertEquals(expected, sj.render[Envelope[Body]](value)) - } - - test("Wrapped") { - val inst = Bigger(25, Envelope("abc", FancyBody("msg here"))) - val js = sj.render(inst) - assertEquals( - """{"foo":25,"env":{"Giraffe":"co.blocke.scalajack.json.structures.FancyBody","id":"abc","body":{"message":"msg here"}}}""".asInstanceOf[JSON], js) - assertEquals(inst, sj.read[Bigger](js)) - } - - test("Type modifier works") { - val sjm = co.blocke.scalajack.ScalaJack().withTypeValueModifier( - co.blocke.scalajack.model.ClassNameHintModifier( - (hint: String) => "co.blocke.scalajack.json.structures." + hint, - (cname: String) => cname.split('.').last - ) - ) - val value: Envelope[Body] = Envelope("DEF", FancyBody("BOO")) - val js = sjm.render[Envelope[Body]](value) - assertEquals( - """{"Giraffe":"FancyBody","id":"DEF","body":{"message":"BOO"}}""".asInstanceOf[JSON], js) - assertEquals(value, sjm.read[Envelope[Body]](js)) - } - - test("Handles mutliple externalized types (bonus: with modifier)") { - val sjm = co.blocke.scalajack.ScalaJack().withTypeValueModifier( - co.blocke.scalajack.model.ClassNameHintModifier( - (hint: String) => "co.blocke.scalajack.json.structures." + hint, - (cname: String) => cname.split('.').last - ) - ) - val value: BigEnvelope[Body, Hobby, Int] = - BigEnvelope("DEF", FancyBody("BOO"), InsideHobby("stamps")) - val js = sjm.render[BigEnvelope[Body, Hobby, Int]](value) - assertEquals( - """{"Giraffe":"FancyBody","Hippo":"InsideHobby","id":"DEF","body":{"message":"BOO"},"hobby":{"desc":"stamps"}}""".asInstanceOf[JSON], - js) - assertEquals(value, sjm.read[BigEnvelope[Body, Hobby, Int]](js)) - } - - test("Works with ParseOrElse") { - val js = - """{"Giraffe":"co.blocke.scalajack.json.structures.FancyBody","id":"DEF","body":{"bogus":"BOO"}}""".asInstanceOf[JSON] - val expected: Envelope[Body] = Envelope("DEF", DefaultBody("Unknown body")) - assertEquals(expected,sj2.read[Envelope[Body]](js)) - } diff --git a/core/src/test/scala/co.blocke.scalajack/json/structures/UnionsAndIntersections.scala b/core/src/test/scala/co.blocke.scalajack/json/structures/UnionsAndIntersections.scala deleted file mode 100644 index 33590eec..00000000 --- a/core/src/test/scala/co.blocke.scalajack/json/structures/UnionsAndIntersections.scala +++ /dev/null @@ -1,111 +0,0 @@ -package co.blocke.scalajack -package json.structures - -import co.blocke.scala_reflection._ -import scala.math.BigDecimal -import java.util.UUID -import TestUtil._ -import munit._ -import munit.internal.console -import co.blocke.scalajack.json.JSON - - -class UnionsAndIntersections() extends FunSuite: - - val sj = co.blocke.scalajack.ScalaJack() - - test("Simple union type") { - describe("-----------------------------------\n: Union and Intersection Tests :\n----------------------------------",Console.BLUE) - - // Right - val inst = Multi2(List("a","b","c")) - val js = sj.render(inst) - assertEquals(js, """{"one":["a","b","c"]}""".asInstanceOf[JSON]) - assertEquals(sj.read[Multi2](js), inst) - - // Left - val inst2 = Multi2(List(true,false)) - val js2 = sj.render(inst2) - assertEquals(js2, """{"one":[true,false]}""".asInstanceOf[JSON]) - assertEquals(sj.read[Multi2](js2), inst2) - - // Failure - val js3 = """{"one":12.34}""".asInstanceOf[JSON] - val msg = """Failed to read any values for union type - |{"one":12.34} - |------^""".stripMargin - interceptMessage[co.blocke.scalajack.ScalaJackError](msg){ - sj.read[Multi2](js3) - } - } - - test("3-way union type") { - // Position 1 - val inst = Multi3(List("a","b","c")) - val js = sj.render(inst) - assertEquals(js, """{"one":["a","b","c"]}""".asInstanceOf[JSON]) - assertEquals(sj.read[Multi3](js), inst) - - // Position 2 - val inst2 = Multi3(List(1,2,3)) - val js2 = sj.render(inst2) - assertEquals(js2, """{"one":[1,2,3]}""".asInstanceOf[JSON]) - assertEquals(sj.read[Multi3](js2), inst2) - - // Position 3 - val inst3 = Multi3(false) - val js3 = sj.render(inst3) - assertEquals(js3, """{"one":false}""".asInstanceOf[JSON]) - assertEquals(sj.read[Multi3](js3), inst3) - - // Failure - val js4 = """{"one":12.34}""".asInstanceOf[JSON] - val msg = """Failed to read any values for union type - |{"one":12.34} - |------^""".stripMargin - interceptMessage[co.blocke.scalajack.ScalaJackError](msg){ - sj.read[Multi3](js4) - } - } - - test("4-way union type") { - // Position 1 - val inst = Multi4(List("a","b","c")) - val js = sj.render(inst) - assertEquals(js, """{"one":["a","b","c"]}""".asInstanceOf[JSON]) - assertEquals(sj.read[Multi4](js), inst) - - // Position 2 - val inst2 = Multi4(List(1,2,3)) - val js2 = sj.render(inst2) - assertEquals(js2, """{"one":[1,2,3]}""".asInstanceOf[JSON]) - assertEquals(sj.read[Multi4](js2), inst2) - - // Position 3 - val inst3 = Multi4(false) - val js3 = sj.render(inst3) - assertEquals(js3, """{"one":false}""".asInstanceOf[JSON]) - assertEquals(sj.read[Multi4](js3), inst3) - - // Position 4 - val inst4 = Multi4(Person("Bob",34)) - val js4 = sj.render(inst4) - assertEquals(js4, """{"one":{"name":"Bob","age":34}}""".asInstanceOf[JSON]) - assertEquals(sj.read[Multi4](js4), inst4) - - // Failure - val js5 = """{"one":12.34}""".asInstanceOf[JSON] - val msg = """Failed to read any values for union type - |{"one":12.34} - |------^""".stripMargin - interceptMessage[co.blocke.scalajack.ScalaJackError](msg){ - sj.read[Multi4](js5) - } - } - - test("Intersection type") { - val inst = IntersectionHolder( InterImpl(5,true,'Z') ) - val js = sj.render(inst) - assertEquals(js, """{"a":{"_hint":"co.blocke.scalajack.json.structures.InterImpl","a":5,"b":true,"c":"Z"}}""".asInstanceOf[JSON]) - assertEquals(inst, sj.read[IntersectionHolder](js)) - } diff --git a/core/src/test/scala/co.blocke.scalajack/json4s/AnyColl.scala b/core/src/test/scala/co.blocke.scalajack/json4s/AnyColl.scala deleted file mode 100644 index ba269f28..00000000 --- a/core/src/test/scala/co.blocke.scalajack/json4s/AnyColl.scala +++ /dev/null @@ -1,77 +0,0 @@ -package co.blocke.scalajack -package json4s - -import org.json4s._ -import org.json4s.{ Diff, JDecimal, JNothing, JObject } -import TestUtil._ -import munit._ -import munit.internal.console -import co.blocke.scalajack.json4s.Json4sFlavor - -import scala.math.BigDecimal - -class AnyColl() extends FunSuite: - - val sj = co.blocke.scalajack.ScalaJack(Json4sFlavor()) - - test("List works (Int)") { - describe( - "-----------------------------------\n: Any Collection Tests (Json4s) :\n-----------------------------------", Console.BLUE - ) - val inst: Any = List(1, 2L, 3.2, BigDecimal(123.45)) - val js4s = sj.render(inst) - val expected = JArray(List(JInt(1), JLong(2), JDouble(3.2), JDecimal(123.45))) - assertEquals(Diff(JNothing, JNothing, JNothing), js4s.diff(expected)) - assertEquals(inst, sj.read[Any](js4s)) - } - - test("First-Level List works (Class)") { - val inst: Any = List(Player("Mike", 34), Player("Sarah", 29)) - val js4s = sj.render(inst) - val expected = JArray( - List( - JObject( - List( - "_hint" -> JString("co.blocke.scalajack.json4s.Player"), - "name" -> JString("Mike"), - "age" -> JInt(34) - ) - ), - JObject( - List( - "_hint" -> JString("co.blocke.scalajack.json4s.Player"), - "name" -> JString("Sarah"), - "age" -> JInt(29) - ) - ) - ) - ) - assertEquals(Diff(JNothing, JNothing, JNothing), js4s.diff(expected)) - assert(List(Player("Mike", 34), Player("Sarah", 29)) == sj.read[List[Any]](js4s)) - } - - test("Map works (Int,Int)") { - val inst: Any = Map(1 -> 2, 3 -> 4) - val js4s = sj.render(inst) - val expected = JObject(List("1" -> JInt(2), "3" -> JInt(4))) - assertEquals(Diff(JNothing, JNothing, JNothing), js4s.diff(expected)) - assert(Map("1" -> 2, "3" -> 4) == - sj.read[Any](js4s)) // May keys converted to String when read back in (because they're Any) - } - - test("Map works (String,Int)") { - val inst: Any = Map("yes" -> 1, "no" -> 2) - val js4s = sj.render(inst) - val expected = JObject(List("yes" -> JInt(1), "no" -> JInt(2))) - assertEquals(Diff(JNothing, JNothing, JNothing), js4s.diff(expected)) - assertEquals(inst, sj.read[Any](js4s)) - } - - test("First-Level Map works (Class)") { - val js4s = JObject( - "_hint" -> JString("co.blocke.scalajack.json4s.Player"), - "name" -> JString("Mike"), - "age" -> JInt(34) - ) - assert(Player("Mike", 34) == sj.read[Any](js4s)) - } diff --git a/core/src/test/scala/co.blocke.scalajack/json4s/Custom.scala b/core/src/test/scala/co.blocke.scalajack/json4s/Custom.scala deleted file mode 100644 index 2e2f9bd0..00000000 --- a/core/src/test/scala/co.blocke.scalajack/json4s/Custom.scala +++ /dev/null @@ -1,90 +0,0 @@ -package co.blocke.scalajack -package json4s - -import TestUtil._ -import munit._ -import munit.internal.console -import org.json4s._ -import co.blocke.scalajack.json4s._ -import co.blocke.scala_reflection.RType - -class Custom extends FunSuite: - - val sj = co.blocke.scalajack.ScalaJack(Json4sFlavor()) - - test("parse()") { - describe( - "---------------------------\n: Custom Tests (Json4s) :\n---------------------------", Console.BLUE - ) - val p = sj.parse(JInt(5)) - assertEquals(p.isInstanceOf[Json4sParser], true) - } - - test("allowPermissivePrimitives()") { - interceptMessage[co.blocke.scalajack.ScalaJackError]("Permissive primitives not supported for Json4s"){ - sj.allowPermissivePrimitives() - } - } - - test("parseOrElse()") { - val sj2 = sj.parseOrElse(RType.of[Address] -> RType.of[DefaultAddress]) - val js4s = - JObject( - List("_hint" -> JString("unknown"), "postalCode" -> JString("12345")) - ) - assertEquals(sj2.read[Address](js4s), DefaultAddress("12345")) - } - - test("withAdapters()") { - val sj2 = sj.withAdapters(PhoneAdapter) - val dbo = JObject( - List("name" -> JString("Fred"), "phone" -> JString("123-456-7890")) - ) - assertEquals(sj2.read[Employee](dbo), Employee("Fred", "1234567890".asInstanceOf[Phone])) - } - - test("withDefaultHint()") { - val sj2 = co.blocke.scalajack.ScalaJack(Json4sFlavor()).withDefaultHint("kind") - val dbo = new JObject( - List( - "kind" -> JString("co.blocke.scalajack.json4s.USDemographic"), - "age" -> JInt(34), - "address" -> JObject( - List( - "kind" -> JString("co.blocke.scalajack.json4s.USAddress"), - "street" -> JString("123 Main"), - "city" -> JString("New York"), - "state" -> JString("NY"), - "postalCode" -> JString("39822") - ) - ) - ) - ) - assertEquals(sj2.read[Demographic](dbo), USDemographic(34, USAddress("123 Main", "New York", "NY", "39822")) - ) - } - - test("withHints()") { - val sj2 = co.blocke.scalajack.ScalaJack(Json4sFlavor()).withHints( - RType.of[Address] -> "addr_kind", - RType.of[Demographic] -> "demo" - ) - val dbo = new JObject( - List( - "demo" -> JString("co.blocke.scalajack.json4s.USDemographic"), - "age" -> JInt(34), - "address" -> JObject( - List( - "addr_kind" -> JString("co.blocke.scalajack.json4s.USAddress"), - "street" -> JString("123 Main"), - "city" -> JString("New York"), - "state" -> JString("NY"), - "postalCode" -> JString("39822") - ) - ) - ) - ) - assertEquals(sj2.read[Demographic](dbo), USDemographic(34, USAddress("123 Main", "New York", "NY", "39822")) - ) - } - // Other custom configs are already covered in other tests elsewhere diff --git a/core/src/test/scala/co.blocke.scalajack/json4s/Json4sSpec.scala b/core/src/test/scala/co.blocke.scalajack/json4s/Json4sSpec.scala deleted file mode 100644 index 0ec02a5f..00000000 --- a/core/src/test/scala/co.blocke.scalajack/json4s/Json4sSpec.scala +++ /dev/null @@ -1,238 +0,0 @@ -package co.blocke.scalajack -package json4s - -import org.json4s._ -import org.json4s.{ Diff, JNothing, JObject } -import TestUtil._ -import munit._ -import munit.internal.console -import co.blocke.scalajack._ -import co.blocke.scalajack.json4s._ -import co.blocke.scala_reflection.RType - -class Json4sSpec extends FunSuite: - - val sj = co.blocke.scalajack.ScalaJack(Json4sFlavor()) - - test("Null Arrays work") { - describe( - "------------------\n: Json4s Tests :\n------------------", Console.BLUE - ) - val inst: List[String] = null - val js4s = sj.render(inst) - assert(Diff(JNothing, JNothing, JNothing) == js4s.diff(JNull) ) - assertEquals(inst, sj.read[List[String]](js4s)) - } - - test("Null Maps work") { - val inst: Map[String, String] = null - val js4s = sj.render(inst) - assert(Diff(JNothing, JNothing, JNothing) == js4s.diff(JNull) ) - assertEquals(inst, sj.read[Map[String, String]](js4s)) - - val s: String = null - val inst2 = Map("a" -> 1, s -> 2) - interceptMessage[co.blocke.scalajack.ScalaJackError]("Map keys cannot be null."){ - sj.render(inst2) - } - } - - test("Null strings work") { - val inst: String = null - val js4s = sj.render(inst) - assert(Diff(JNothing, JNothing, JNothing) == js4s.diff(JNull) ) - assertEquals(inst, sj.read[String](js4s)) - } - - test("Null objects work") { - val inst: Player = null - val js4s = sj.render(inst) - assert(Diff(JNothing, JNothing, JNothing) == js4s.diff(JNull) ) - assert(inst == sj.read[Player](js4s)) - } - - test("Tuples work") { - val inst = List(("Fred", 34), ("Sally", 29)) - val js4s = sj.render(inst) - val expected = JArray( - List( - JArray(List(JString("Fred"), JInt(34))), - JArray(List(JString("Sally"), JInt(29))) - ) - ) - assert(Diff(JNothing, JNothing, JNothing) == js4s.diff(expected) ) - assertEquals(inst, sj.read[List[(String, Int)]](js4s)) - } - - test("Bad JValueBuilder access") { - val b = JValueBuilder() - interceptMessage[co.blocke.scalajack.ScalaJackError]("No value set for internal json4s builder"){ - b.result() - } - } - test("SJCapture works") { - val js4s = JObject( - List( - "name" -> JString("Harry"), - "age" -> JInt(43), - "foo" -> JBool(true), - "bar" -> JInt(3) - ) - ) - val inst = sj.read[PlayerCapture](js4s) - assertEquals(PlayerCapture("Harry", 43), inst ) - assert(Diff(JNothing, JNothing, JNothing) == js4s.diff(sj.render(inst))) - } - - test("Trait support") { - val inst: Thing[Int, String] = AThing(5, "foo") - val js4s = sj.render(inst) - val expected = JObject( - List( - "_hint" -> JString("co.blocke.scalajack.json4s.AThing"), - "a" -> JInt(5), - "b" -> JString("foo") - ) - ) - assert(Diff(JNothing, JNothing, JNothing) == js4s.diff(expected) ) - assertEquals(inst, sj.read[Thing[Int, String]](js4s)) - } - - test("Malformed error works") { - val js4s = JArray(List(JInt(3), JDouble(3.1))) - interceptMessage[co.blocke.scalajack.ScalaJackError]("Cannot parse an Int from value"){ - sj.read[List[Int]](js4s) - } - } - - test("Unexpected error works") { - val js4s = JArray(List(JInt(3), JDouble(3.1))) - interceptMessage[co.blocke.scalajack.ScalaJackError]("Expected object here, not 'JArray(List(JInt(3), JDouble(3.1)))'"){ - sj.read[Player](js4s) - } - } - - test("Hint mods work") { - val prependHintMod = model.ClassNameHintModifier( - (hint: String) => "co.blocke.scalajack.json4s." + hint, - (cname: String) => cname.split('.').last - ) - val sjx = sj.withHintModifiers((RType.of[Address], prependHintMod)) - val inst: Demographic = - USDemographic(50, USAddress("123 Main", "New York", "NY", "39822")) - val js4s = sjx.render(inst) - val expected = JObject( - List( - "_hint" -> JString("co.blocke.scalajack.json4s.USDemographic"), - "age" -> JInt(50), - "address" -> JObject( - List( - "_hint" -> JString("USAddress"), - "street" -> JString("123 Main"), - "city" -> JString("New York"), - "state" -> JString("NY"), - "postalCode" -> JString("39822") - ) - ) - ) - ) - assert(Diff(JNothing, JNothing, JNothing) == js4s.diff(expected) ) - assertEquals(inst, sjx.read[Demographic](js4s)) - } - - test("Broken hint mod (no class)") { - val prependHintMod = model.ClassNameHintModifier( - (hint: String) => "co.blocke.scalajack.bogus." + hint, - (cname: String) => cname.split('.').last - ) - val sjx = sj.withHintModifiers((RType.of[Address], prependHintMod)) - val js4s = JObject( - List( - "_hint" -> JString("co.blocke.scalajack.json4s.USDemographic"), - "age" -> JInt(50), - "address" -> JObject( - List( - "_hint" -> JString("BogusAddress"), - "street" -> JString("123 Main"), - "city" -> JString("New York"), - "state" -> JString("NY"), - "postalCode" -> JString("39822") - ) - ) - ) - ) - interceptMessage[co.blocke.scalajack.ScalaJackError]("Couldn't marshal class for BogusAddress"){ - sj.read[Demographic](js4s) - } - } - - test("Null object value") { - val inst = USDemographic(25, null) - val js4s = sj.render(inst) - assert(Diff(JNothing, JNothing, JNothing) == js4s.diff(JObject(List("age" -> JInt(25), "address" -> JNull)))) - assertEquals(inst, sj.read[USDemographic](js4s)) - } - - test("No type hint in trait") { - val js4s = JObject(List("a" -> JInt(5), "b" -> JString("foo"))) - interceptMessage[co.blocke.scalajack.ScalaJackError]("Type hint '_hint' not found"){ - sj.read[Thing[Int, String]](js4s) - } - } - - test("Any type that looks like trait but unknown hint") { - val js4s = JObject( - List("_hint" -> JInt(4), "name" -> JString("Fred"), "age" -> JInt(55)) - ) - assert(Map("_hint" -> 4, "name" -> "Fred", "age" -> 55) == sj.read[Any](js4s)) - } - - test("Non-scalars can't be map keys for Json4s") { - val p = Player("Fred", 1) - val m = Map(p -> 3) - interceptMessage[co.blocke.scalajack.ScalaJackError]("Json4s type org.json4s.JsonAST$JObject is not supported as a Map key"){ - sj.render(m) - } - val js4s = JObject( - List( - "name" -> JString("Harry"), - "age" -> JInt(43), - "foo" -> JBool(true), - "bar" -> JInt(3) - ) - ) - interceptMessage[co.blocke.scalajack.ScalaJackError]("Only scalar values are supported as JValue Map keys"){ - sj.read[Map[Player, Int]](js4s) - } - } - - test("Externalized type hints work (with type modifier!)") { - import model._ - val scalaJack = co.blocke.scalajack.ScalaJack(Json4sFlavor()).withTypeValueModifier( - ClassNameHintModifier( - (hint: String) => "co.blocke.scalajack.json4s." + hint, - (cname: String) => cname.split('.').last - ) - ) - val value: Envelope[Body] = Envelope("DEF", FancyBody("BOO")) - val d = scalaJack.render[Envelope[Body]](value) - assertEquals( - "JObject(List((Giraffe,JString(FancyBody)), (id,JString(DEF)), (body,JObject(List((message,JString(BOO)))))))", - d.toString) - assertEquals(scalaJack.read[Envelope[Body]](d), value) - } - - test("Source as string") { - val js4s = JObject( - List( - "name" -> JString("Harry"), - "age" -> JInt(43), - "foo" -> JBool(true), - "bar" -> JInt(3) - ) - ) - val p = sj.parse(js4s) - assertEquals(p.sourceAsString, - "JObject(List((name,JString(Harry)), (age,JInt(43)), (foo,JBool(true)), (bar,JInt(3))))" - ) - } diff --git a/core/src/test/scala/co.blocke.scalajack/json4s/Model.scala b/core/src/test/scala/co.blocke.scalajack/json4s/Model.scala deleted file mode 100644 index b6db8484..00000000 --- a/core/src/test/scala/co.blocke.scalajack/json4s/Model.scala +++ /dev/null @@ -1,118 +0,0 @@ -package co.blocke.scalajack -package json4s - -import java.util.UUID -import co.blocke.scalajack.model._ -import co.blocke.scala_reflection.RType -import co.blocke.scala_reflection.info.AliasInfo -import scala.collection.mutable -import co.blocke.scalajack.SJCapture - -// === Scala -case class SampleBigDecimal( - bd1: BigDecimal, - bd2: BigDecimal, - bd3: BigDecimal, - bd4: BigDecimal, - bd5: BigDecimal, - bd6: BigDecimal) -case class SampleBigInt(bi1: BigInt, bi2: BigInt, bi3: BigInt, bi4: BigInt) -case class SampleBinary(b1: Array[Byte], b2: Array[Byte]) -case class SampleBoolean(bool1: Boolean, bool2: Boolean) -case class SampleByte(b1: Byte, b2: Byte, b3: Byte, b4: Byte) -case class SampleChar(c1: Char, c2: Char, c3: Char) -case class SampleDouble(d1: Double, d2: Double, d3: Double, d4: Double) - -object Size extends Enumeration { - val Small, Medium, Large = Value -} -case class SampleEnum( - e1: Size.Value, - e2: Size.Value, - e3: Size.Value, - e4: Size.Value, - e5: Size.Value) - -case class SampleFloat(f1: Float, f2: Float, f3: Float, f4: Float) -case class SampleInt(i1: Int, i2: Int, i3: Int, i4: Int) -case class SampleLong(l1: Long, l2: Long, l3: Long, l4: Long) -case class SampleShort(s1: Short, s2: Short, s3: Short, s4: Short) -case class SampleString(s1: String, s2: String, s3: String) - -case class SampleUUID(u1: UUID, u2: UUID) - -case class Player(name: String, age: Int) -case class PlayerCapture(name: String, age: Int) extends SJCapture - -case class OptionBigInt(o: Option[BigInt]) -case class OptionClass(name: String, age: Option[Int]) -case class OptionTuple(foo: Int, t: (Boolean, Option[String], Int)) -trait Person { val name: String } -case class SomeClass(name: String, age: Int) extends Person -trait Thing[A, B] { val a: A; val b: B } -case class AThing[Y, X](a: X, b: Y) extends Thing[X, Y] - -case class WrappedMaps( - a: Map[Byte, Int], - b: Map[Int, Int], - c: Map[Long, Int], - d: Map[Double, Int], - e: Map[Float, Int], - f: Map[Short, Int], - g: Map[BigInt, Int], - h: Map[BigDecimal, Int], - i: Map[Boolean, Int], - j: Map[Char, Int], - k: Map[String, Int]) - -trait Address { val postalCode: String } -case class USAddress( - street: String, - city: String, - state: String, - postalCode: String) - extends Address -case class DefaultAddress(postalCode: String) extends Address -trait Demographic { val address: Address } -case class USDemographic(age: Int, address: Address) extends Demographic - -trait Body -case class FancyBody(message: String) extends Body -case class Envelope[T <: Body](id: String, body: T) { - type Giraffe = T -} - -opaque type Phone >: Null = String - -// Override just Phone -object PhoneAdapter extends TypeAdapterFactory with TypeAdapter[Phone]: - def matches(concrete: RType): Boolean = - concrete match { - case a: AliasInfo if a.name == "Phone" => - true - case _ => - false - } - def makeTypeAdapter(concrete: RType)(implicit taCache: TypeAdapterCache): TypeAdapter[Phone] = this - val info = RType.of[Phone] - override def isStringish: Boolean = true - - def read(parser: Parser): Phone = - parser.expectString() match { - case null => null.asInstanceOf[Phone] - case s: String => s.replaceAll("-", "").asInstanceOf[Phone] - } - - def write[WIRE]( - t: Phone, - writer: Writer[WIRE], - out: mutable.Builder[WIRE, WIRE]): Unit = t match { - case null => writer.writeNull(out) - case _ => - writer.writeString( - "%s-%s-%s".format(t.toString.substring(0, 3), t.toString.substring(3, 6), t.toString.substring(6)), - out - ) - } - -case class Employee(name: String, phone: Phone) diff --git a/core/src/test/scala/co.blocke.scalajack/json4s/Parsing.scala b/core/src/test/scala/co.blocke.scalajack/json4s/Parsing.scala deleted file mode 100644 index 4ece3a6c..00000000 --- a/core/src/test/scala/co.blocke.scalajack/json4s/Parsing.scala +++ /dev/null @@ -1,103 +0,0 @@ -package co.blocke.scalajack -package json4s - -import TestUtil._ -import munit._ -import munit.internal.console -import org.json4s._ -import co.blocke.scalajack.json4s.Json4sFlavor - -class Parsing() extends FunSuite: - - val sj = co.blocke.scalajack.ScalaJack(Json4sFlavor()) - - test("Null String value") { - describe( - "----------------------\n: Parsing (Json4s) :\n----------------------", Console.BLUE - ) - assertEquals(sj.read[String](null), null) - } - - test("Null (BSON null) String value") { - assertEquals(sj.read[String](JNull), null) - } - - test("Non-String value where String expected") { - interceptMessage[co.blocke.scalajack.ScalaJackError]("Expected string here, not 'JInt(5)'"){ - sj.read[String](JInt(5)) - } - } - - test("Null List value") { - assertEquals(sj.read[List[Int]](null), null) - } - - test("Null (BSON null) List value") { - assertEquals(sj.read[List[Int]](JNull), null) - } - - test("Non-List value where List expected") { - interceptMessage[co.blocke.scalajack.ScalaJackError]("Expected list here, not 'JInt(5)'"){ - sj.read[List[Int]](JInt(5)) - } - } - - test("Null tuple value") { - assertEquals(sj.read[(Int, Int)](null), null) - } - - test("Null (BSON null) tuple value") { - assertEquals(sj.read[(Int, Int)](JNull), null) - } - - test("Non-tuple value where tuple expected") { - interceptMessage[co.blocke.scalajack.ScalaJackError]("Expected tuple (list) here, not 'JInt(5)'"){ - sj.read[(Int, Int)](JInt(5)) - } - } - - test("Null Map value") { - assertEquals(sj.read[Map[String, Int]](null), null) - } - - test("Null (BSON null) Map value") { - assertEquals(sj.read[Map[String, Int]](JNull), null) - } - - test("Non-Map value where Map expected") { - interceptMessage[co.blocke.scalajack.ScalaJackError]("Expected map here, not 'JInt(5)'"){ - sj.read[Map[String, Int]](JInt(5)) - } - } - - test("Null object value") { - assertEquals(sj.read[Person](null), null) - } - - test("Null (BSON null) object value") { - assertEquals(sj.read[Person](JNull), null) - } - - test("Non-Boolean value where Boolean expected") { - interceptMessage[co.blocke.scalajack.ScalaJackError]("Expected boolean here, not 'JInt(5)'"){ - sj.read[Boolean](JInt(5)) - } - } - - test("Non-Number value where Number expected") { - interceptMessage[co.blocke.scalajack.ScalaJackError]("Expected number here, not 'JString(x)'"){ - sj.read[Int](new JString("x")) - } - } - - test("Attempt to scan for type hint on a non-object") { - interceptMessage[co.blocke.scalajack.ScalaJackError]("Expected object here, not 'JInt(5)'"){ - sj.read[Address](JInt(5)) - } - } - - test("Attempt to resolve type members on a non-object") { - interceptMessage[co.blocke.scalajack.ScalaJackError]("Expected object here, not 'JInt(5)'"){ - sj.read[Envelope[FancyBody]](JInt(5)) - } - } \ No newline at end of file diff --git a/core/src/test/scala/co.blocke.scalajack/json4s/ScalaPrim.scala b/core/src/test/scala/co.blocke.scalajack/json4s/ScalaPrim.scala deleted file mode 100644 index fffcc201..00000000 --- a/core/src/test/scala/co.blocke.scalajack/json4s/ScalaPrim.scala +++ /dev/null @@ -1,154 +0,0 @@ -package co.blocke.scalajack -package json4s - -import TestUtil._ -import munit._ -import munit.internal.console -import scala.math.BigDecimal -import org.json4s.JsonDSL._ -import org.json4s._ -import co.blocke.scalajack.json4s.Json4sFlavor - -class ScalaPrim() extends FunSuite: - - val sj = co.blocke.scalajack.ScalaJack(Json4sFlavor()) - - test("BigDecimal must work") { - describe( - "------------------------------------\n: Scala Primitive Tests (Json4s) :\n------------------------------------", Console.BLUE - ) - val inst = SampleBigDecimal( - BigDecimal(123L), - BigDecimal(1.23), - BigDecimal(0), - BigDecimal("123.456"), - BigDecimal( - "0.1499999999999999944488848768742172978818416595458984375" - ), - null - ) - val js4s = sj.render(inst) - val expected = JObject() ~ ("bd1" -> JDecimal(123L)) ~ ("bd2" -> JDecimal( - 1.23 - )) ~ ("bd3" -> JDecimal(0)) ~ ("bd4" -> JDecimal(123.456)) ~ ("bd5" -> JDecimal( - BigDecimal( - "0.1499999999999999944488848768742172978818416595458984375" - ) - )) ~ ("bd6" -> JNull) - assert(Diff(JNothing, JNothing, JNothing) == js4s.diff(expected) ) - assertEquals(inst, sj.read[SampleBigDecimal](js4s)) - } - - test("BigInt must work") { - val inst = SampleBigInt( - BigInt("-90182736451928374653345"), - BigInt("90182736451928374653345"), - BigInt(0), - null - ) - val js4s = sj.render(inst) - val expected = JObject() ~ ("bi1" -> JInt( - BigInt("-90182736451928374653345") - )) ~ ("bi2" -> JInt(BigInt("90182736451928374653345"))) ~ ("bi3" -> JInt(0)) ~ ("bi4" -> JNull) - assert(Diff(JNothing, JNothing, JNothing) == js4s.diff(expected) ) - assertEquals(inst, sj.read[SampleBigInt](js4s)) - } - - test("Boolean must work (not nullable)") { - val inst = SampleBoolean(true, false) - val js4s = sj.render(inst) - val expected = JObject() ~ ("bool1" -> JBool(true)) ~ ("bool2" -> JBool( - false - )) - assert(Diff(JNothing, JNothing, JNothing) == js4s.diff(expected) ) - assertEquals(inst, sj.read[SampleBoolean](js4s)) - } - - test("Double must work (not nullable)") { - val inst = - SampleDouble(Double.MaxValue, Double.MinValue, 0.0, -123.4567) - val js4s = sj.render(inst) - val expected = JObject() ~ ("d1" -> JDouble(Double.MaxValue)) ~ ("d2" -> JDouble( - Double.MinValue - )) ~ ("d3" -> JDouble(0.0)) ~ ("d4" -> JDouble(-123.4567)) - assert(Diff(JNothing, JNothing, JNothing) ==js4s.diff(expected) ) - assertEquals(inst, sj.read[SampleDouble](js4s)) - } - - test("Enumeration must work (not nullable)") { - val inst = - SampleEnum(Size.Small, Size.Medium, Size.Large, null, Size.Medium) - val js4s = sj.render(inst) - val expected = JObject() ~ ("e1" -> JString("Small")) ~ ("e2" -> JString("Medium")) - ~ ("e3" -> JString("Large")) ~ ("e4" -> JNull) ~ ("e5" -> JString("Medium")) - assert(Diff(JNothing, JNothing, JNothing) == js4s.diff(expected) ) - // mutate e5 into an ordinal... - val js2 = js4s.asInstanceOf[JObject] ~ ("e5" -> JInt(1)) - assertEquals(inst, sj.read[SampleEnum](js2)) - } - - test("Enumerations as Ints must work") { - val sj2 = sj.enumsAsInts() - val inst = - SampleEnum(Size.Small, Size.Medium, Size.Large, null, Size.Medium) - val js4s = sj2.render(inst) - val expected = JObject() ~ ("e1" -> JInt(0)) ~ ("e2" -> JInt(1)) ~ ("e3" -> JInt( - 2 - )) ~ ("e4" -> JNull) ~ ("e5" -> JInt(1)) - assert(Diff(JNothing, JNothing, JNothing) == js4s.diff(expected) ) - assertEquals(inst, sj2.read[SampleEnum](js4s)) - } - - test("Int must work (not nullable)") { - val inst = SampleInt(Int.MaxValue, Int.MinValue, 0, 123) - val js4s = sj.render(inst) - val expected = JObject() ~ ("i1" -> JInt(Int.MaxValue)) ~ ("i2" -> JInt( - Int.MinValue - )) ~ ("i3" -> JInt(0)) ~ ("i4" -> JInt(123)) - assert(Diff(JNothing, JNothing, JNothing) == js4s.diff(expected) ) - assertEquals(inst, sj.read[SampleInt](js4s)) - } - - test("Long must work (not nullable)") { - val inst = SampleLong(Long.MaxValue, Long.MinValue, 0L, 123L) - val js4s = sj.render(inst) - val expected = JObject() ~ ("l1" -> JLong(Long.MaxValue)) ~ ("l2" -> JLong( - Long.MinValue - )) ~ ("l3" -> JLong(0L)) ~ ("l4" -> JLong(123L)) - assert(Diff(JNothing, JNothing, JNothing) == js4s.diff(expected) ) - assertEquals(inst, sj.read[SampleLong](js4s)) - } - - test("Map of string-wrapped primitives work") { - val inst = WrappedMaps( - Map(3.toByte -> 2), - Map(1 -> 2), - Map(5L -> 7), - Map(1.2 -> 3), - Map(1.2F -> 3), - Map(2.toShort -> 9), - Map(BigInt(5) -> 6), - Map(BigDecimal(4.9) -> 8), - Map(true -> 1), - Map('c' -> 1), - Map("stuff" -> 99) - ) - val js4s = sj.render(inst) - val expected = JObject( - List( - "a" -> JObject(List("3" -> JInt(2))), - "b" -> JObject(List("1" -> JInt(2))), - "c" -> JObject(List("5" -> JInt(7))), - "d" -> JObject(List("1.2" -> JInt(3))), - "e" -> JObject(List("1.2" -> JInt(3))), - "f" -> JObject(List("2" -> JInt(9))), - "g" -> JObject(List("5" -> JInt(6))), - "h" -> JObject(List("4.9" -> JInt(8))), - "i" -> JObject(List("true" -> JInt(1))), - "j" -> JObject(List("c" -> JInt(1))), - "k" -> JObject(List("stuff" -> JInt(99))) - ) - ) - assert(Diff(JNothing, JNothing, JNothing) == js4s.diff(expected) ) - assertEquals(inst, sj.read[WrappedMaps](js4s)) - } diff --git a/core/src/test/scala/co.blocke.scalajack/yaml/collections/AnyColl.scala b/core/src/test/scala/co.blocke.scalajack/yaml/collections/AnyColl.scala deleted file mode 100644 index b6f4b8bc..00000000 --- a/core/src/test/scala/co.blocke.scalajack/yaml/collections/AnyColl.scala +++ /dev/null @@ -1,115 +0,0 @@ -package co.blocke.scalajack -package yaml -package collections - -import TestUtil._ -import munit._ -import munit.internal.console - -case class Player(name: String, age: Int) - -class AnyColl() extends FunSuite: - - val sj = ScalaJack(YamlFlavor()) - - test("List works (Int)") { - describe( - "---------------------------------\n: Any Collection Tests (YAML) :\n---------------------------------", Console.BLUE - ) - val inst: Any = List(1, 2, 3) - val yaml = sj.render(inst) - val comparison = """- 1 - |- 2 - |- 3 - |""".stripMargin.asInstanceOf[YAML] - assertEquals(comparison, yaml ) - assert(List(1, 2, 3) == sj.read[Any](yaml)) - } - - test("List works (String)") { - val inst: Any = List("one", "two", "three") - val yaml = sj.render(inst) - val comparison = """- one - |- two - |- three - |""".stripMargin.asInstanceOf[YAML] - assertEquals(comparison, yaml) - assert(List("one", "two", "three") == sj.read[Any](yaml)) - } - - test("First-Level List works (Class)") { - val inst: Any = List(Player("Mike", 34), Player("Sarah", 29)) - val yaml = sj.render(inst) - val comparison = """- _hint: co.blocke.scalajack.yaml.collections.Player - | name: Mike - | age: 34 - |- _hint: co.blocke.scalajack.yaml.collections.Player - | name: Sarah - | age: 29 - |""".stripMargin.asInstanceOf[YAML] - assertEquals(comparison, yaml ) - assert(List(Player("Mike", 34), Player("Sarah", 29)) == sj.read[List[Any]](yaml)) - } - - test("Map works (Int,Int)") { - val inst: Any = Map(1 -> 2, 3 -> 4) - val yaml = sj.render(inst) - val comparison = """1: 2 - |3: 4 - |""".stripMargin.asInstanceOf[YAML] - assertEquals(comparison, yaml ) - assertEquals(inst, sj.read[Any](yaml)) - } - - test("Map works (String,Int)") { - val inst: Any = Map("yes" -> 1, "no" -> 2) - val yaml = sj.render(inst) - val comparison = """yes: 1 - |no: 2 - |""".stripMargin.asInstanceOf[YAML] - assertEquals(comparison, yaml ) - assert(Map("yes" -> 1, "no" -> 2) == sj.read[Any](yaml)) - } - - test("First-Level Map works (Class)") { - val yaml = - """_hint: co.blocke.scalajack.yaml.collections.Player - |name: Mike - |age: 34 - |""".stripMargin.asInstanceOf[YAML] - assert(Player("Mike", 34) == sj.read[Any](yaml)) - } - - test("Second-Level Map works (Class keys) (Class)") { - val yaml = - """? - | _hint: co.blocke.scalajack.yaml.collections.Player - | name: Mike - | age: 34 - |: 15 - |? - | name: Mike - | age: 34 - |: 16 - |""".stripMargin.asInstanceOf[YAML] - assert( - Map(Player("Mike", 34) -> 15, Map("name" -> "Mike", "age" -> 34) -> 16) == sj.read[Any](yaml) - ) - } - - test("Second-Level Map (List keys) works (Class)") { - val yaml = - """? - | - - | _hint: co.blocke.scalajack.yaml.collections.Player - | name: Mike - | age: 34 - | - - | name: Mike - | age: 34 - |: 15 - |""".stripMargin.asInstanceOf[YAML] - assert( - Map(List(Player("Mike", 34), Map("name" -> "Mike", "age" -> 34)) -> 15) == sj.read[Any](yaml) - ) - } diff --git a/core/src/test/scala/co.blocke.scalajack/yaml/mapkeys/ClassPrimKeys.scala b/core/src/test/scala/co.blocke.scalajack/yaml/mapkeys/ClassPrimKeys.scala deleted file mode 100644 index fd6dfe3e..00000000 --- a/core/src/test/scala/co.blocke.scalajack/yaml/mapkeys/ClassPrimKeys.scala +++ /dev/null @@ -1,271 +0,0 @@ -package co.blocke.scalajack -package yaml -package mapkeys - -import TestUtil._ -import munit._ -import munit.internal.console -import java.util.UUID -import model.StringMatchHintModifier -import co.blocke.scala_reflection.RType - -class ClassPrimKeys() extends FunSuite: - - val sj = ScalaJack(YamlFlavor()) - - test("Simple (flat) class as key") { - describe( - "--------------------------------\n: Class Map Key Tests (YAML) :\n--------------------------------", Console.BLUE - ) - describe("+++ Positive Tests +++") - val a = SimpleClass("Larry", 32, isOk = true, "golf") - val b = SimpleClass("Mike", 27, isOk = false, 125) - val inst = SampleSimple(Map(a -> b)) - val yaml = sj.render(inst) - val comparison = """m: - | ? name: Larry - | age: 32 - | isOk: true - | favorite: golf - | : name: Mike - | age: 27 - | isOk: false - | favorite: 125 - |""".stripMargin.asInstanceOf[YAML] - assertEquals(comparison, yaml ) - assertEquals(inst, sj.read[SampleSimple](yaml)) - } - - test("Complex class (having members that are classes) as key") { - val a = SimpleClass("Larry", 32, isOk = true, "golf") - val b = SimpleClass("Mike", 27, isOk = false, 125) - val c1 = ComplexClass( - UUID.fromString("1e6c2b31-4dfe-4bf6-a0a0-882caaff0e9c"), - a, - allDone = true - ) - val c2 = ComplexClass( - UUID.fromString("1e6c2b31-4dfe-4bf6-a0a0-882caaff0e9d"), - b, - allDone = false - ) - val inst = SampleComplex(Map(c1 -> c2)) - val yaml = sj.render(inst) - val comparison = """m: - | ? id: 1e6c2b31-4dfe-4bf6-a0a0-882caaff0e9c - | simple: - | name: Larry - | age: 32 - | isOk: true - | favorite: golf - | allDone: true - | : id: 1e6c2b31-4dfe-4bf6-a0a0-882caaff0e9d - | simple: - | name: Mike - | age: 27 - | isOk: false - | favorite: 125 - | allDone: false - |""".stripMargin.asInstanceOf[YAML] - assertEquals(comparison, yaml ) - assertEquals(inst, sj.read[SampleComplex](yaml)) - } - - test("Simple (flat) trait as key") { - val a: Pet = FishPet("Flipper", Food.Veggies, 74.33) - val b: Pet = DogPet("Fido", Food.Meat, 3) - val inst = SamplePet(Map(a -> b)) - val yaml = sj.render(inst) - val comparison = """m: - | ? _hint: co.blocke.scalajack.yaml.mapkeys.FishPet - | name: Flipper - | food: Veggies - | waterTemp: 74.33 - | : _hint: co.blocke.scalajack.yaml.mapkeys.DogPet - | name: Fido - | food: Meat - | numLegs: 3 - |""".stripMargin.asInstanceOf[YAML] - assertEquals(comparison, yaml ) - assertEquals(inst, sj.read[SamplePet](yaml)) - } - - test("Complex trait (having members that are traits) as key") { - val a: Pet = FishPet("Flipper", Food.Veggies, 74.33) - val b: Pet = DogPet("Fido", Food.Meat, 3) - val c: Pet = CompoundPet("Legion", Food.Pellets, b) - val inst = SamplePet(Map(c -> a)) - val yaml = sj.render(inst) - val comparison = """m: - | ? _hint: co.blocke.scalajack.yaml.mapkeys.CompoundPet - | name: Legion - | food: Pellets - | pet: - | _hint: co.blocke.scalajack.yaml.mapkeys.DogPet - | name: Fido - | food: Meat - | numLegs: 3 - | : _hint: co.blocke.scalajack.yaml.mapkeys.FishPet - | name: Flipper - | food: Veggies - | waterTemp: 74.33 - |""".stripMargin.asInstanceOf[YAML] - assertEquals(comparison, yaml ) - assertEquals(inst, sj.read[SamplePet](yaml)) - } - - test( - "Complex trait (having members that are traits) as key where trait member is null" - ) { - val a: Pet = FishPet("Flipper", Food.Veggies, 74.33) - val b: Pet = null.asInstanceOf[Pet] // DogPet("Fido", Food.Meat, 3) - val c: Pet = CompoundPet("Legion", Food.Pellets, b) - val inst = SamplePet(Map(c -> a)) - val yaml = sj.render(inst) - val comparison = """m: - | ? _hint: co.blocke.scalajack.yaml.mapkeys.CompoundPet - | name: Legion - | food: Pellets - | pet: null - | : _hint: co.blocke.scalajack.yaml.mapkeys.FishPet - | name: Flipper - | food: Veggies - | waterTemp: 74.33 - |""".stripMargin.asInstanceOf[YAML] - assertEquals(comparison, yaml ) - assertEquals(inst, sj.read[SamplePet](yaml)) - } - - test("Class having collections as members") { - val a = PolyClass(Map("a" -> 1, "b" -> 2), List("one", "two")) - val b = PolyClass(Map("x" -> 9, "y" -> 10), List("aye", "you")) - val inst = SamplePolyClass(Map(a -> b)) - val yaml = sj.render(inst) - val comparison = """m: - | ? lookup: - | a: 1 - | b: 2 - | favs: [one, two] - | : lookup: - | x: 9 - | y: 10 - | favs: [aye, you] - |""".stripMargin.asInstanceOf[YAML] - assertEquals(comparison, yaml ) - assertEquals(inst, sj.read[SamplePolyClass](yaml)) - } - - test("Class having collections as members (empty collections") { - val a = PolyClass(Map.empty[String, Int], List.empty[String]) - val b = PolyClass(Map.empty[String, Int], List.empty[String]) - val inst = SamplePolyClass(Map(a -> b)) - val yaml = sj.render(inst) - val comparison = """m: - | ? lookup: {} - | favs: [] - | : lookup: {} - | favs: [] - |""".stripMargin.asInstanceOf[YAML] - assertEquals(comparison, yaml ) - assertEquals(inst, sj.read[SamplePolyClass](yaml)) - } - - test("Custom trait hint field and value for key trait") { - val petHintMod = StringMatchHintModifier( - Map("BreathsWater" -> "co.blocke.scalajack.yaml.mapkeys.FishPet", "BreathsAir" -> "co.blocke.scalajack.yaml.mapkeys.DogPet") - ) - val sj2 = sj - .withHints((RType.of[Pet] -> "kind")) - .withHintModifiers((RType.of[Pet] -> petHintMod)) - - val a: Pet = FishPet("Flipper", Food.Veggies, 74.33) - val b: Pet = DogPet("Fido", Food.Meat, 3) - val inst = SamplePet(Map(a -> b)) - val yaml = sj2.render(inst) - val comparison = """m: - | ? kind: BreathsWater - | name: Flipper - | food: Veggies - | waterTemp: 74.33 - | : kind: BreathsAir - | name: Fido - | food: Meat - | numLegs: 3 - |""".stripMargin.asInstanceOf[YAML] - assertEquals(comparison, yaml ) - assertEquals(inst, sj2.read[SamplePet](yaml)) - } - - test("Custom trait hint field and value for key member's trait") { - val petHintMod = StringMatchHintModifier( - Map("BreathsWater" -> "co.blocke.scalajack.yaml.mapkeys.FishPet", "BreathsAir" -> "co.blocke.scalajack.yaml.mapkeys.DogPet") - ) - val sj2 = sj - .withHints((RType.of[Pet] -> "kind")) - .withHintModifiers((RType.of[Pet] -> petHintMod)) - - val a: PetHolder = - ShinyPetHolder("123 Main", FishPet("Flipper", Food.Veggies, 74.33)) - val b: PetHolder = - ShinyPetHolder("210 North", DogPet("Fido", Food.Meat, 3)) - val inst = SampleShiny(Map(a -> b)) - val yaml = sj2.render(inst) - val comparison = """m: - | ? _hint: co.blocke.scalajack.yaml.mapkeys.ShinyPetHolder - | address: 123 Main - | pet: - | kind: BreathsWater - | name: Flipper - | food: Veggies - | waterTemp: 74.33 - | : _hint: co.blocke.scalajack.yaml.mapkeys.ShinyPetHolder - | address: 210 North - | pet: - | kind: BreathsAir - | name: Fido - | food: Meat - | numLegs: 3 - |""".stripMargin.asInstanceOf[YAML] - assertEquals(comparison, yaml ) - assertEquals(inst, sj2.read[SampleShiny](yaml)) - } - - test("Key value is a class having a noncanoncial map") { - val a = NCKey(Map(0 -> false, 1 -> true), "truth") - val b = NCKey(Map(1 -> false, 0 -> true), "lie") - val inst = SampleNCKey(Map(a -> b)) - val yaml = sj.render(inst) - val comparison = """m: - | ? nc: - | 0: false - | 1: true - | name: truth - | : nc: - | 1: false - | 0: true - | name: lie - |""".stripMargin.asInstanceOf[YAML] - assertEquals(comparison, yaml ) - assertEquals(inst, sj.read[SampleNCKey](yaml)) - } - - test("Extra/unneeded fields in key's JSON harmlessly ignored") { - val yaml = - """m: - | ? - | name: Larry - | bogus: false - | age: 32 - | isOk: true - | favorite: golf - | : - | name: Mike - | age: 27 - | isOk: false - | favorite: 125 - |""".stripMargin.asInstanceOf[YAML] - val a = SimpleClass("Larry", 32, isOk = true, "golf") - val b = SimpleClass("Mike", 27, isOk = false, 125) - val inst = SampleSimple(Map(a -> b)) - assertEquals(inst, sj.read[SampleSimple](yaml)) - } \ No newline at end of file diff --git a/core/src/test/scala/co.blocke.scalajack/yaml/mapkeys/JavaPrimKeys.scala b/core/src/test/scala/co.blocke.scalajack/yaml/mapkeys/JavaPrimKeys.scala deleted file mode 100644 index f23e63e7..00000000 --- a/core/src/test/scala/co.blocke.scalajack/yaml/mapkeys/JavaPrimKeys.scala +++ /dev/null @@ -1,357 +0,0 @@ -package co.blocke.scalajack -package yaml -package mapkeys - -import TestUtil._ -import munit._ -import munit.internal.console -import java.lang.{Boolean => JBoolean, Byte => JByte, Character => JChar, Double => JDouble, Float => JFloat, Integer => JInteger, Long => JLong, Short => JShort} -import java.math.{BigDecimal => JBigDecimal, BigInteger => JBigInteger} -import java.time._ - -class JavaPrimKeys() extends FunSuite: - - val sj = ScalaJack(YamlFlavor()) - - test("With BigDecimal Key") { - describe( - "-----------------------------------------\n: Java Primitive Map Key Tests (YAML) :\n-----------------------------------------", Console.BLUE - ) - describe("Simple DelimSpec:") - val inst = SampleJBigDecimal( - Map( - new JBigDecimal("123.456") -> new JBigDecimal("1"), - new JBigDecimal("789.123") -> new JBigDecimal("2") - ) - ) - val yaml = sj.render(inst) - val comparison = """m: - | 123.456: !!float '1' - | 789.123: !!float '2' - |""".stripMargin.asInstanceOf[YAML] - assertEquals(comparison, yaml ) - assertEquals(inst, sj.read[SampleJBigDecimal](yaml)) - } - - test("With BigInteger Key") { - val inst = SampleJBigInteger( - Map( - new JBigInteger("123") -> new JBigInteger("1"), - new JBigInteger("789") -> new JBigInteger("2") - ) - ) - val yaml = sj.render(inst) - val comparison = """m: - | 123: 1 - | 789: 2 - |""".stripMargin.asInstanceOf[YAML] - assertEquals(comparison, yaml ) - assertEquals(inst, sj.read[SampleJBigInteger](yaml)) - } - - test("With Boolean Key") { - val inst = SampleJBoolean( - Map( - true.asInstanceOf[JBoolean] -> false.asInstanceOf[JBoolean], - false.asInstanceOf[JBoolean] -> true.asInstanceOf[JBoolean] - ) - ) - val yaml = sj.render(inst) - val comparison = """m: - | true: false - | false: true - |""".stripMargin.asInstanceOf[YAML] - assertEquals(comparison, yaml ) - assertEquals(inst, sj.read[SampleJBoolean](yaml)) - } - - test("With Byte Key") { - val inst = SampleJByte( - Map( - 16.toByte.asInstanceOf[JByte] -> 2.toByte.asInstanceOf[JByte], - 48.toByte.asInstanceOf[JByte] -> 9.toByte.asInstanceOf[JByte] - ) - ) - val yaml = sj.render(inst) - val comparison = """m: - | 16: 2 - | 48: 9 - |""".stripMargin.asInstanceOf[YAML] - assertEquals(comparison, yaml ) - assertEquals(inst, sj.read[SampleJByte](yaml)) - } - - test("With Char Key") { - val inst = SampleJChar( - Map( - 'a'.asInstanceOf[JChar] -> 'A'.asInstanceOf[JChar], - 'z'.asInstanceOf[JChar] -> 'Z'.asInstanceOf[JChar] - ) - ) - val yaml = sj.render(inst) - val comparison = """m: - | a: A - | z: Z - |""".stripMargin.asInstanceOf[YAML] - assertEquals(comparison, yaml ) - assertEquals(inst, sj.read[SampleJChar](yaml)) - } - - test("With Double Key") { - val inst = SampleJDouble( - Map( - 12.34.asInstanceOf[JDouble] -> 56.78.asInstanceOf[JDouble], - 90.12.asInstanceOf[JDouble] -> 34.56.asInstanceOf[JDouble] - ) - ) - val yaml = sj.render(inst) - val comparison = """m: - | 12.34: 56.78 - | 90.12: 34.56 - |""".stripMargin.asInstanceOf[YAML] - assertEquals(comparison, yaml ) - assertEquals(inst, sj.read[SampleJDouble](yaml)) - } - - test("With Float Key") { - val inst = SampleJFloat( - Map( - 12.34F.asInstanceOf[JFloat] -> 56.78F.asInstanceOf[JFloat], - 90.12F.asInstanceOf[JFloat] -> 34.56F.asInstanceOf[JFloat] - ) - ) - val yaml = sj.render(inst) - val comparison = """m: - | 12.34: 56.78 - | 90.12: 34.56 - |""".stripMargin.asInstanceOf[YAML] - assertEquals(comparison, yaml ) - assertEquals(inst, sj.read[SampleJFloat](yaml)) - } - - test("With Integer Key") { - val inst = SampleJInteger( - Map( - 12.asInstanceOf[JInteger] -> 56.asInstanceOf[JInteger], - 90.asInstanceOf[JInteger] -> 34.asInstanceOf[JInteger] - ) - ) - val yaml = sj.render(inst) - val comparison = """m: - | 12: 56 - | 90: 34 - |""".stripMargin.asInstanceOf[YAML] - assertEquals(comparison, yaml ) - assertEquals(inst, sj.read[SampleJInteger](yaml)) - } - - test("With Long Key") { - val inst = SampleJLong( - Map( - 12L.asInstanceOf[JLong] -> 56L.asInstanceOf[JLong], - 90L.asInstanceOf[JLong] -> 34L.asInstanceOf[JLong] - ) - ) - val yaml = sj.render(inst) - val comparison = """m: - | 12: 56 - | 90: 34 - |""".stripMargin.asInstanceOf[YAML] - assertEquals(comparison, yaml ) - assertEquals(inst, sj.read[SampleJLong](yaml)) - } - - test("With Number Key") { - val inst = SampleJNumber( - Map( - JByte.valueOf("-128") -> JByte.valueOf("127"), - JShort.valueOf("-32768") -> JShort.valueOf("32767"), - JInteger.valueOf("-2147483648") -> JInteger.valueOf("2147483647"), - JLong.valueOf("-9223372036854775808") -> JLong - .valueOf("9223372036854755807"), - JByte.valueOf("0") -> new JBigInteger("9923372036854755810"), - JFloat.valueOf("3.4e-038") -> JFloat.valueOf("3.4e+038"), - JDouble.valueOf("1.7e-308") -> JDouble.valueOf("1.7e+308"), - new JBigDecimal("1.8e+308") -> JFloat.valueOf("0.0") - ) - ) - val result = SampleJNumber( - Map( - JByte.valueOf("0") -> new JBigDecimal("9923372036854755810"), - JInteger.valueOf("-2147483648") -> JInteger.valueOf("2147483647"), - JLong.valueOf("-9223372036854775808") -> JLong - .valueOf("9223372036854755807"), - JByte.valueOf("-128") -> JByte.valueOf("127"), - JFloat.valueOf("3.4E-38") -> JFloat.valueOf("3.4E38"), - JShort.valueOf("-32768") -> JShort.valueOf("32767"), - new JBigDecimal("1.8E+308") -> JByte.valueOf("0"), - JDouble.valueOf("1.7E-308") -> JDouble.valueOf("1.7E308") - ) - ) - val yaml = sj.render(inst) - val comparison = """m: - | -128: 127 - | !!float '3.4E-38': !!float '3.4E38' - | -32768: 32767 - | !!float '1.8E+308': 0.0 - | !!float '1.7E-308': !!float '1.7E308' - | 0: 9923372036854755810 - | -2147483648: 2147483647 - | -9223372036854775808: 9223372036854755807 - |""".stripMargin.asInstanceOf[YAML] - assertEquals(comparison, yaml ) - val read = sj.read[SampleJNumber](yaml) - assertEquals(result, read ) - } - - test("With Short Key") { - val inst = SampleJShort( - Map( - 12.toShort.asInstanceOf[JShort] -> 56.toShort - .asInstanceOf[JShort], - 90.toShort.asInstanceOf[JShort] -> 34.toShort.asInstanceOf[JShort] - ) - ) - val yaml = sj.render(inst) - val comparison = """m: - | 12: 56 - | 90: 34 - |""".stripMargin.asInstanceOf[YAML] - assertEquals(comparison, yaml ) - assertEquals(inst, sj.read[SampleJShort](yaml)) - } - - test("With Duration Key") { - describe("Time DelimSpec:") - val inst = - SampleDuration(Map(Duration.ZERO -> Duration.parse("P2DT3H4M"))) - val yaml = sj.render(inst) - val comparison = """m: - | PT0S: PT51H4M - |""".stripMargin.asInstanceOf[YAML] - assertEquals(comparison, yaml ) - assertEquals(inst, sj.read[SampleDuration](yaml)) - } - - test("With Instant Key") { - val inst = SampleInstant( - Map( - Instant.EPOCH -> Instant.MAX, - Instant.MIN -> Instant.parse("2007-12-03T10:15:30.00Z") - ) - ) - val yaml = sj.render(inst) - val comparison = """m: - | 1970-01-01T00:00:00Z: +1000000000-12-31T23:59:59.999999999Z - | -1000000000-01-01T00:00:00Z: 2007-12-03T10:15:30Z - |""".stripMargin.asInstanceOf[YAML] - assertEquals(comparison, yaml ) - assertEquals(inst, sj.read[SampleInstant](yaml)) - } - - test("With LocalDateTime Key") { - val inst = SampleLocalDateTime( - Map( - LocalDateTime.MAX -> LocalDateTime.MIN, - LocalDateTime.parse("2007-12-03T10:15:30") -> null - ) - ) - val yaml = sj.render(inst) - val comparison = """m: - | +999999999-12-31T23:59:59.999999999: -999999999-01-01T00:00:00 - | 2007-12-03T10:15:30: null - |""".stripMargin.asInstanceOf[YAML] - assertEquals(comparison, yaml ) - assertEquals(inst, sj.read[SampleLocalDateTime](yaml)) - } - - test("With LocalDate Key") { - val inst = SampleLocalDate( - Map( - LocalDate.MAX -> LocalDate.MIN, - LocalDate.parse("2007-12-03") -> null - ) - ) - val yaml = sj.render(inst) - val comparison = """m: - | +999999999-12-31: -999999999-01-01 - | 2007-12-03: null - |""".stripMargin.asInstanceOf[YAML] - assertEquals(comparison, yaml ) - assertEquals(inst, sj.read[SampleLocalDate](yaml)) - } - - test("With LocalTime Key") { - val inst = SampleLocalTime( - Map( - LocalTime.MAX -> LocalTime.MIN, - LocalTime.MIDNIGHT -> LocalTime.NOON, - LocalTime.parse("10:15:30") -> null - ) - ) - val yaml = sj.render(inst) - val comparison = """m: - | 23:59:59.999999999: 00:00:00 - | 00:00:00: 12:00:00 - | 10:15:30: null - |""".stripMargin.asInstanceOf[YAML] - assertEquals(comparison, yaml ) - assertEquals(inst, sj.read[SampleLocalTime](yaml)) - } - - test("With OffsetDateTime Key") { - val inst = SampleOffsetDateTime( - Map( - OffsetDateTime.MAX -> OffsetDateTime.MIN, - OffsetDateTime.parse("2007-12-03T10:15:30+01:00") -> null - ) - ) - val yaml = sj.render(inst) - val comparison = """m: - | +999999999-12-31T23:59:59.999999999-18:00: -999999999-01-01T00:00:00+18:00 - | 2007-12-03T10:15:30+01:00: null - |""".stripMargin.asInstanceOf[YAML] - assertEquals(comparison, yaml ) - assertEquals(inst, sj.read[SampleOffsetDateTime](yaml)) - } - - test("With OffsetTime Key") { - val inst = SampleOffsetTime( - Map( - OffsetTime.MAX -> OffsetTime.MIN, - OffsetTime.parse("10:15:30+01:00") -> null - ) - ) - val yaml = sj.render(inst) - val comparison = """m: - | 23:59:59.999999999-18:00: 00:00:00+18:00 - | 10:15:30+01:00: null - |""".stripMargin.asInstanceOf[YAML] - assertEquals(comparison, yaml ) - assertEquals(inst, sj.read[SampleOffsetTime](yaml)) - } - - test("With Period Key") { - val inst = SamplePeriod(Map(Period.ZERO -> Period.parse("P1Y2M3D"))) - val yaml = sj.render(inst) - val comparison = """m: - | P0D: P1Y2M3D - |""".stripMargin.asInstanceOf[YAML] - assertEquals(comparison, yaml ) - assertEquals(inst, sj.read[SamplePeriod](yaml)) - } - - test("With ZonedDateTime Key") { - val inst = SampleZonedDateTime( - Map( - ZonedDateTime - .parse("2007-12-03T10:15:30+01:00[Europe/Paris]") -> null - ) - ) - val yaml = sj.render(inst) - val comparison = """m: - | 2007-12-03T10:15:30+01:00[Europe/Paris]: null - |""".stripMargin.asInstanceOf[YAML] - assertEquals(comparison, yaml ) - assertEquals(inst, sj.read[SampleZonedDateTime](yaml)) - } \ No newline at end of file diff --git a/core/src/test/scala/co.blocke.scalajack/yaml/mapkeys/ListCollKeys.scala b/core/src/test/scala/co.blocke.scalajack/yaml/mapkeys/ListCollKeys.scala deleted file mode 100644 index 3befa84b..00000000 --- a/core/src/test/scala/co.blocke.scalajack/yaml/mapkeys/ListCollKeys.scala +++ /dev/null @@ -1,212 +0,0 @@ -package co.blocke.scalajack -package yaml -package mapkeys - -import TestUtil._ -import munit._ -import munit.internal.console - -class ListCollKeys() extends FunSuite: - - val sj = ScalaJack(YamlFlavor()) - - test("List as key") { - describe( - "-------------------------------\n: List Map Key Tests (YAML) :\n-------------------------------", Console.BLUE - ) - val l1 = List(1, 2, 3) - val l2 = List(4, 5, 6) - val inst = Map(l1 -> l2) - val yaml = sj.render(inst) - val comparison = """? [1, 2, 3] - |: [4, 5, 6] - |""".stripMargin.asInstanceOf[YAML] - assertEquals(comparison, yaml ) - assertEquals(inst, sj.read[Map[List[Int], List[Int]]](yaml)) - } - - test("List of Lists as key") { - val l1 = List(List(1, 2, 3), List(9, 8, 7)) - val l2 = List(List(4, 5, 6), List(1, 3, 5)) - val inst = Map(l1 -> l2) - val yaml = sj.render(inst) - val comparison = """? - [1, 2, 3] - | - [9, 8, 7] - |: - [4, 5, 6] - | - [1, 3, 5] - |""".stripMargin.asInstanceOf[YAML] - assertEquals(comparison, yaml ) - assertEquals(inst, sj.read[Map[List[List[Int]], List[List[Int]]]](yaml)) - } - - test("List of Tuples as key") { - val l1: List[(String, String)] = List(("A", "a"), ("B", "b"), (null, "c")) - val l2: List[(String, String)] = List(("X", "x"), ("Y", "y"), (null, "z")) - val inst = Map(l1 -> l2) - val yaml = sj.render(inst) - val comparison = """? - [A, a] - | - [B, b] - | - [null, c] - |: - [X, x] - | - [Y, y] - | - [null, z] - |""".stripMargin.asInstanceOf[YAML] - assertEquals(comparison, yaml ) - assertEquals(inst, sj.read[Map[List[(String, String)], List[(String, String)]]](yaml)) - } - - test("List of Maps as key") { - val l1 = List(Map("wow" -> true), Map("ya" -> false)) - val l2 = List(Map("zing" -> false), Map("bling" -> true)) - val inst = Map(l1 -> l2) - val yaml = sj.render(inst) - val comparison = """? - wow: true - | - ya: false - |: - zing: false - | - bling: true - |""".stripMargin.asInstanceOf[YAML] - assertEquals(comparison, yaml ) - assertEquals(inst, sj.read[Map[List[Map[String, Boolean]], List[Map[String, Boolean]]]](yaml)) - } - - test("List of Case Class as key") { - val fish = FishPet("Flipper", Food.Meat, 68.9) - val inst = Map(List(fish, fish) -> List(fish, fish)) - val yaml = sj.render(inst) - val comparison = """? - name: Flipper - | food: Meat - | waterTemp: 68.9 - | - name: Flipper - | food: Meat - | waterTemp: 68.9 - |: - name: Flipper - | food: Meat - | waterTemp: 68.9 - | - name: Flipper - | food: Meat - | waterTemp: 68.9 - |""".stripMargin.asInstanceOf[YAML] - assertEquals(comparison, yaml ) - assertEquals(inst, sj.read[Map[List[FishPet], List[FishPet]]](yaml)) - } - - test("List of Trait as key") { - val fish: Pet = FishPet("Flipper", Food.Meat, 68.9) - val inst = Map(List(fish, fish) -> List(fish, fish)) - val yaml = sj.render(inst) - val comparison = """? - _hint: co.blocke.scalajack.yaml.mapkeys.FishPet - | name: Flipper - | food: Meat - | waterTemp: 68.9 - | - _hint: co.blocke.scalajack.yaml.mapkeys.FishPet - | name: Flipper - | food: Meat - | waterTemp: 68.9 - |: - _hint: co.blocke.scalajack.yaml.mapkeys.FishPet - | name: Flipper - | food: Meat - | waterTemp: 68.9 - | - _hint: co.blocke.scalajack.yaml.mapkeys.FishPet - | name: Flipper - | food: Meat - | waterTemp: 68.9 - |""".stripMargin.asInstanceOf[YAML] - assertEquals(comparison, yaml ) - assertEquals(inst, sj.read[Map[List[Pet], List[Pet]]](yaml)) - } - - test("List of Any as key") { - val inst: Map[List[Any], List[Any]] = - Map(List(23L, "wow", true) -> List(12.2, 0)) - val yaml = sj.render(inst) - val comparison = """? - 23 - | - wow - | - true - |: - 12.2 - | - 0.0 - |""".stripMargin.asInstanceOf[YAML] - assertEquals(comparison, yaml ) - assertEquals(true, - sj.read[Map[List[Any], List[Any]]](yaml) - .isInstanceOf[Map[List[Any], List[Any]]] - ) - } - - test("List of parameterized class as key") { - val inst = Map( - List(AThing(true, "True"), AThing(false, "False")) -> List( - AThing(true, "Yes"), - AThing(false, "No") - ) - ) - val yaml = sj.render(inst) - val comparison = """? - a: true - | b: True - | - a: false - | b: False - |: - a: true - | b: Yes - | - a: false - | b: No - |""".stripMargin.asInstanceOf[YAML] - assertEquals(comparison, yaml ) - assertEquals(true, - sj.read[Map[List[AThing[String, Boolean]], List[AThing[String, Boolean]]]](yaml) - .isInstanceOf[Map[List[AThing[String, Boolean]], List[AThing[String, Boolean]]]] - ) - } - - test("List of parameterized trait as key") { - val inst: Map[List[Thing[Boolean, String]], List[Thing[Boolean, String]]] = - Map( - List(AThing(true, "True"), AThing(false, "False")) -> List( - AThing(true, "Yes"), - AThing(false, "No") - ) - ) - val yaml = sj.render(inst) - val comparison = """? - _hint: co.blocke.scalajack.yaml.mapkeys.AThing - | a: true - | b: True - | - _hint: co.blocke.scalajack.yaml.mapkeys.AThing - | a: false - | b: False - |: - _hint: co.blocke.scalajack.yaml.mapkeys.AThing - | a: true - | b: Yes - | - _hint: co.blocke.scalajack.yaml.mapkeys.AThing - | a: false - | b: No - |""".stripMargin.asInstanceOf[YAML] - assertEquals(comparison, yaml ) - assertEquals(true, - sj.read[Map[List[Thing[Boolean, String]], List[Thing[Boolean, String]]]](yaml) - .isInstanceOf[Map[List[Thing[Boolean, String]], List[Thing[Boolean, String]]]] - ) - } - - test("List of Optional as key") { - val inst: Map[List[Option[String]], List[Option[String]]] = - Map(List(Some("hey"), Some("you")) -> List(Some("stop"), Some("go"))) - val yaml = sj.render(inst) - val comparison = """? - hey - | - you - |: - stop - | - go - |""".stripMargin.asInstanceOf[YAML] - assertEquals(comparison, yaml ) - assertEquals(inst, sj.read[Map[List[Option[String]], List[Option[String]]]](yaml)) - } - - test("List of ValueClass as key") { - val inst = - Map(List(VCChar('A'), VCChar('a')) -> List(VCChar('B'), VCChar('b'))) - val yaml = sj.render(inst) - val comparison = """? - A - | - a - |: - B - | - b - |""".stripMargin.asInstanceOf[YAML] - assertEquals(comparison, yaml ) - assertEquals(inst, sj.read[Map[List[VCChar], List[VCChar]]](yaml)) - } \ No newline at end of file diff --git a/core/src/test/scala/co.blocke.scalajack/yaml/mapkeys/MapCollKeys.scala b/core/src/test/scala/co.blocke.scalajack/yaml/mapkeys/MapCollKeys.scala deleted file mode 100644 index 23fa9bd7..00000000 --- a/core/src/test/scala/co.blocke.scalajack/yaml/mapkeys/MapCollKeys.scala +++ /dev/null @@ -1,311 +0,0 @@ -package co.blocke.scalajack -package yaml -package mapkeys - -import TestUtil._ -import munit._ -import munit.internal.console - -class MapCollKeys() extends FunSuite: - - val sj = ScalaJack(YamlFlavor()) - - test("Map as key") { - describe( - "------------------------------\n: Map Map Key Tests (YAML) :\n------------------------------", Console.BLUE - ) - val m1 = Map(1 -> 2) - val m2 = Map(3 -> 4) - val inst = Map(m1 -> m2) - val yaml = sj.render(inst) - val comparison = """? 1: 2 - |: 3: 4 - |""".stripMargin.asInstanceOf[YAML] - assertEquals(comparison, yaml ) - assertEquals(inst, sj.read[Map[Map[Int, Int], Map[Int, Int]]](yaml)) - } - - test("Map as key, map value is null") { - val m1 = Map(1 -> 2) - val m2: Map[Int, Int] = null - val inst = Map(m1 -> m2) - val yaml = sj.render(inst) - val comparison = """? 1: 2 - |: null - |""".stripMargin.asInstanceOf[YAML] - assertEquals(comparison, yaml ) - assertEquals(inst, sj.read[Map[Map[Int, Int], Map[Int, Int]]](yaml)) - } - - test("Map of Lists as key") { - val m1 = List(Food.Meat, Food.Veggies) - val m2 = List(Food.Seeds, Food.Pellets) - val inst = Map(Map(m1 -> m2) -> Map(m2 -> m1)) - val yaml = sj.render(inst) - val comparison = """? ? - Meat - | - Veggies - | : - Seeds - | - Pellets - |: ? - Seeds - | - Pellets - | : - Meat - | - Veggies - |""".stripMargin.asInstanceOf[YAML] - assertEquals(comparison, yaml ) - assertEquals(inst, sj.read[Map[Map[List[Food.Value], List[Food.Value]], Map[List[Food.Value], List[Food.Value]]]](yaml)) - } - - test("Map of Maps as key") { - val m1 = Map(Food.Meat -> Food.Veggies) - val m2 = Map(Food.Seeds -> Food.Pellets) - val inst = Map(Map(m1 -> m2) -> Map(m2 -> m1)) - val yaml = sj.render(inst) - val comparison = """? ? Meat: Veggies - | : Seeds: Pellets - |: ? Seeds: Pellets - | : Meat: Veggies - |""".stripMargin.asInstanceOf[YAML] - assertEquals(comparison, yaml ) - assertEquals(inst, sj.read[Map[Map[Map[Food.Value, Food.Value], Map[Food.Value, Food.Value]], Map[Map[Food.Value, Food.Value], Map[Food.Value, Food.Value]]]](yaml)) - } - - test("Map of Tuples as key") { - val m1 = (Food.Meat, Food.Veggies) - val m2 = (Food.Seeds, Food.Pellets) - val inst = Map(Map(m1 -> m2) -> Map(m2 -> m1)) - val yaml = sj.render(inst) - val comparison = """? ? - Meat - | - Veggies - | : - Seeds - | - Pellets - |: ? - Seeds - | - Pellets - | : - Meat - | - Veggies - |""".stripMargin.asInstanceOf[YAML] - assertEquals(comparison, yaml ) - assertEquals(inst, - sj.read[Map[Map[(Food.Value, Food.Value), (Food.Value, Food.Value)], Map[(Food.Value, Food.Value), (Food.Value, Food.Value)]]]( - yaml - ) - ) - } - - test("Map of Case Class as key") { - val m1 = - Map(DogPet("Fido", Food.Meat, 4) -> FishPet("Flipper", Food.Meat, 87.3)) - val m2 = - Map(FishPet("Flipper", Food.Meat, 87.3) -> DogPet("Fido", Food.Meat, 4)) - val inst = Map(Map(m1 -> m2) -> Map(m2 -> m1)) - val yaml = sj.render(inst) - val comparison = """? ? ? name: Fido - | food: Meat - | numLegs: 4 - | : name: Flipper - | food: Meat - | waterTemp: 87.3 - | : ? name: Flipper - | food: Meat - | waterTemp: 87.3 - | : name: Fido - | food: Meat - | numLegs: 4 - |: ? ? name: Flipper - | food: Meat - | waterTemp: 87.3 - | : name: Fido - | food: Meat - | numLegs: 4 - | : ? name: Fido - | food: Meat - | numLegs: 4 - | : name: Flipper - | food: Meat - | waterTemp: 87.3 - |""".stripMargin.asInstanceOf[YAML] - assertEquals(comparison, yaml ) - assertEquals(inst, sj.read[Map[Map[Map[DogPet, FishPet], Map[FishPet, DogPet]], Map[Map[FishPet, DogPet], Map[DogPet, FishPet]]]](yaml)) - } - - test("Map of Trait as key") { - val m1: Map[Pet, Pet] = - Map(DogPet("Fido", Food.Meat, 4) -> FishPet("Flipper", Food.Meat, 87.3)) - val m2: Map[Pet, Pet] = - Map(FishPet("Flipper", Food.Meat, 87.3) -> DogPet("Fido", Food.Meat, 4)) - val inst = Map(Map(m1 -> m2) -> Map(m2 -> m1)) - val yaml = sj.render(inst) - val comparison = """? ? ? _hint: co.blocke.scalajack.yaml.mapkeys.DogPet - | name: Fido - | food: Meat - | numLegs: 4 - | : _hint: co.blocke.scalajack.yaml.mapkeys.FishPet - | name: Flipper - | food: Meat - | waterTemp: 87.3 - | : ? _hint: co.blocke.scalajack.yaml.mapkeys.FishPet - | name: Flipper - | food: Meat - | waterTemp: 87.3 - | : _hint: co.blocke.scalajack.yaml.mapkeys.DogPet - | name: Fido - | food: Meat - | numLegs: 4 - |: ? ? _hint: co.blocke.scalajack.yaml.mapkeys.FishPet - | name: Flipper - | food: Meat - | waterTemp: 87.3 - | : _hint: co.blocke.scalajack.yaml.mapkeys.DogPet - | name: Fido - | food: Meat - | numLegs: 4 - | : ? _hint: co.blocke.scalajack.yaml.mapkeys.DogPet - | name: Fido - | food: Meat - | numLegs: 4 - | : _hint: co.blocke.scalajack.yaml.mapkeys.FishPet - | name: Flipper - | food: Meat - | waterTemp: 87.3 - |""".stripMargin.asInstanceOf[YAML] - assertEquals(comparison, yaml ) - assertEquals(inst, sj.read[Map[Map[Map[Pet, Pet], Map[Pet, Pet]], Map[Map[Pet, Pet], Map[Pet, Pet]]]](yaml)) - } - - test("Map of Any as key") { - val m1: Map[Any, Any] = Map(123.45 -> 2) - val m2: Map[Any, Any] = Map(398328372 -> 0) - val inst = Map(Map(m1 -> m2) -> Map(m2 -> m1)) - val yaml = sj.render(inst) - val comparison = """? ? 123.45: 2 - | : 398328372: 0 - |: ? 398328372: 0 - | : 123.45: 2 - |""".stripMargin.asInstanceOf[YAML] - assertEquals(comparison, yaml ) - assertEquals(true, - sj.read[Map[Map[Map[Any, Any], Map[Any, Any]], Map[Map[Any, Any], Map[Any, Any]]]](yaml) - .isInstanceOf[Map[Map[Map[Any, Any], Map[Any, Any]], Map[Map[Any, Any], Map[Any, Any]]]] - ) - } - - test("Map of parameterized class as key") { - val m1: Map[AThing[Int, String], AThing[Int, String]] = - Map(AThing("one", 1) -> AThing("two", 2)) - val m2: Map[AThing[Int, String], AThing[Int, String]] = - Map(AThing("four", 4) -> AThing("three", 3)) - val inst = Map(Map(m1 -> m2) -> Map(m2 -> m1)) - val yaml = sj.render(inst) - val comparison = """? ? ? a: one - | b: 1 - | : a: two - | b: 2 - | : ? a: four - | b: 4 - | : a: three - | b: 3 - |: ? ? a: four - | b: 4 - | : a: three - | b: 3 - | : ? a: one - | b: 1 - | : a: two - | b: 2 - |""".stripMargin.asInstanceOf[YAML] - assertEquals(comparison, yaml ) - assertEquals(true, - sj.read[Map[Map[Map[AThing[Int, String], AThing[Int, String]], Map[AThing[Int, String], AThing[Int, String]]], - Map[Map[AThing[Int, String], AThing[Int, String]], Map[AThing[Int, String], AThing[Int, String]]]]](yaml) - .isInstanceOf[Map[Map[Map[AThing[Int, String], AThing[Int, String]], Map[AThing[Int, String], AThing[Int, String]]], - Map[Map[AThing[Int, String], AThing[Int, String]], Map[AThing[Int, String], AThing[Int, String]]]]] - ) - } - - test("Map of parameterized trait as key") { - val m1: Map[Thing[String, Int], Thing[String, Int]] = - Map(AThing("one", 1) -> AThing("two", 2)) - val m2: Map[Thing[String, Int], Thing[String, Int]] = - Map(AThing("four", 4) -> AThing("three", 3)) - val inst = Map(Map(m1 -> m2) -> Map(m2 -> m1)) - val yaml = sj.render(inst) - val comparison = """? ? ? _hint: co.blocke.scalajack.yaml.mapkeys.AThing - | a: one - | b: 1 - | : _hint: co.blocke.scalajack.yaml.mapkeys.AThing - | a: two - | b: 2 - | : ? _hint: co.blocke.scalajack.yaml.mapkeys.AThing - | a: four - | b: 4 - | : _hint: co.blocke.scalajack.yaml.mapkeys.AThing - | a: three - | b: 3 - |: ? ? _hint: co.blocke.scalajack.yaml.mapkeys.AThing - | a: four - | b: 4 - | : _hint: co.blocke.scalajack.yaml.mapkeys.AThing - | a: three - | b: 3 - | : ? _hint: co.blocke.scalajack.yaml.mapkeys.AThing - | a: one - | b: 1 - | : _hint: co.blocke.scalajack.yaml.mapkeys.AThing - | a: two - | b: 2 - |""".stripMargin.asInstanceOf[YAML] - assertEquals(comparison, yaml ) - assertEquals(true, - sj.read[Map[Map[Map[Thing[String, Int], Thing[String, Int]], Map[Thing[String, Int], Thing[String, Int]]], - Map[Map[Thing[String, Int], Thing[String, Int]], Map[Thing[String, Int], Thing[String, Int]]]]](yaml) - .isInstanceOf[Map[Map[Map[Thing[String, Int], Thing[String, Int]], Map[Thing[String, Int], Thing[String, Int]]], - Map[Map[Thing[String, Int], Thing[String, Int]], Map[Thing[String, Int], Thing[String, Int]]]]] - ) - } - - test("Map of Optional as key") { - val m1: Map[Option[Int], Option[Int]] = Map(Some(3) -> None) - val m2: Map[Option[Int], Option[Int]] = - Map(None -> Some(2), Some(5) -> null) - val inst = Map(Map(m1 -> m2) -> Map(m2 -> m1)) - val yaml = sj.render(inst) - val comparison = """? {}: - | 5: null - |: ? 5: null - | : {} - |""".stripMargin.asInstanceOf[YAML] - assertEquals(comparison, yaml ) - assert( - Map( - Map(Map() -> Map(Some(5) -> None)) -> Map( - Map(Some(5) -> None) -> Map() - ) - ) == - sj.read[Map[Map[Map[Option[Int], Option[Int]], Map[Option[Int], Option[Int]]], Map[Map[Option[Int], Option[Int]], Map[Option[Int], Option[Int]]]]](yaml) - ) - } - - test("Map of Option as key where Option is null must fail") { - val m1: Map[Option[Int], Option[Int]] = Map(Some(3) -> None) - val m0 = Map.empty[Option[Int], Option[Int]] - val bad: Option[Int] = null - val m2 = m0 + (bad -> Some(99)) - val inst = Map(Map(m1 -> m2) -> Map(m2 -> m1)) - val msg = "Map keys cannot be null." - interceptMessage[ScalaJackError](msg){ - sj.render(inst) - } - } - - test("Map of ValueClass as key") { - val m1: Map[VCChar, VCChar] = Map(VCChar('Z') -> VCChar('z')) - val m2: Map[VCChar, VCChar] = Map(VCChar('A') -> VCChar('a')) - val inst = Map(Map(m1 -> m2) -> Map(m2 -> m1)) - val yaml = sj.render(inst) - val comparison = """? ? Z: z - | : A: a - |: ? A: a - | : Z: z - |""".stripMargin.asInstanceOf[YAML] - assertEquals(comparison, yaml ) - assertEquals(inst, sj.read[Map[Map[Map[VCChar, VCChar], Map[VCChar, VCChar]], Map[Map[VCChar, VCChar], Map[VCChar, VCChar]]]](yaml)) - } \ No newline at end of file diff --git a/core/src/test/scala/co.blocke.scalajack/yaml/mapkeys/Model.scala b/core/src/test/scala/co.blocke.scalajack/yaml/mapkeys/Model.scala deleted file mode 100644 index c6cebbae..00000000 --- a/core/src/test/scala/co.blocke.scalajack/yaml/mapkeys/Model.scala +++ /dev/null @@ -1,175 +0,0 @@ -package co.blocke.scalajack -package yaml.mapkeys - -import java.util.UUID -import java.lang.{ - Boolean => JBoolean, - Byte => JByte, - Character => JChar, - Double => JDouble, - Float => JFloat, - Integer => JInteger, - Long => JLong, - Number => JNumber, - Short => JShort -} -import java.math.{BigDecimal => JBigDecimal, BigInteger => JBigInteger} -import java.time._ - -object Size extends Enumeration { - val Small, Medium, Large = Value -} - -trait Thing[A, B] { val a: A; val b: B } -case class AThing[Y, X](a: X, b: Y) extends Thing[X, Y] -trait Part[A] { val p: A } -case class APart[A](p: A) extends Part[A] - -// === Scala Primitive Keys -case class SampleBigDecimal(m: Map[BigDecimal, BigDecimal]) -case class SampleBigInt(m: Map[BigInt, BigInt]) -case class SampleBoolean(m: Map[Boolean, Boolean]) -case class SampleByte(m: Map[Byte, Byte]) -case class SampleChar(m: Map[Char, Char]) -case class SampleDouble(m: Map[Double, Double]) -case class SampleEnumeration(m: Map[Size.Value, Size.Value]) -case class SampleFloat(m: Map[Float, Float]) -case class SampleInt(m: Map[Int, Int]) -case class SampleLong(m: Map[Long, Long]) -case class SampleShort(m: Map[Short, Short]) - -// === Java Primitive Keys -case class SampleJBigDecimal(m: Map[JBigDecimal, JBigDecimal]) -case class SampleJBigInteger(m: Map[JBigInteger, JBigInteger]) -case class SampleJBoolean(m: Map[JBoolean, JBoolean]) -case class SampleJByte(m: Map[JByte, JByte]) -case class SampleJChar(m: Map[JChar, JChar]) -case class SampleJDouble(m: Map[JDouble, JDouble]) -case class SampleJFloat(m: Map[JFloat, JFloat]) -case class SampleJInteger(m: Map[JInteger, JInteger]) -case class SampleJLong(m: Map[JLong, JLong]) -case class SampleJNumber(m: Map[JNumber, JNumber]) -case class SampleJShort(m: Map[JShort, JShort]) - -// === Java Time Keys -case class SampleDuration(m: Map[Duration, Duration]) -case class SampleInstant(m: Map[Instant, Instant]) -case class SampleLocalDateTime(m: Map[LocalDateTime, LocalDateTime]) -case class SampleLocalDate(m: Map[LocalDate, LocalDate]) -case class SampleLocalTime(m: Map[LocalTime, LocalTime]) -case class SampleOffsetDateTime(m: Map[OffsetDateTime, OffsetDateTime]) -case class SampleOffsetTime(m: Map[OffsetTime, OffsetTime]) -case class SamplePeriod(m: Map[Period, Period]) -case class SampleZonedDateTime(m: Map[ZonedDateTime, ZonedDateTime]) - -// === Any primitives -case class AnyShell(m: Map[Any, Any]) - -// === Class Keys -case class SimpleClass(name: String, age: Int, isOk: Boolean, favorite: Any) -case class SampleSimple(m: Map[SimpleClass, SimpleClass]) -case class ComplexClass(id: UUID, simple: SimpleClass, allDone: Boolean) -case class SampleComplex(m: Map[ComplexClass, ComplexClass]) - -object Food extends Enumeration { - val Seeds, Meat, Pellets, Veggies = Value -} -trait Pet { - val name: String - val food: Food.Value -} -case class FishPet(name: String, food: Food.Value, waterTemp: Double) extends Pet -case class DogPet(name: String, food: Food.Value, numLegs: Int) extends Pet -case class CompoundPet(name: String, food: Food.Value, pet: Pet) extends Pet -trait PetHolder { - val address: String - val pet: Pet -} -case class ShinyPetHolder(address: String, pet: Pet) extends PetHolder -case class SamplePet(m: Map[Pet, Pet]) -case class PolyClass(lookup: Map[String, Int], favs: List[String]) -case class SamplePolyClass(m: Map[PolyClass, PolyClass]) -case class SampleShiny(m: Map[PetHolder, PetHolder]) -case class NCKey(nc: Map[Int, Boolean], name: String) -case class SampleNCKey(m: Map[NCKey, NCKey]) - -// === Collections - Tuple -case class SampleTuple(m: Map[(Int, String, Char), (String, Boolean)]) -case class SampleTupleList( - m: Map[(List[String], List[Int]), (List[String], List[Int])] -) -case class SampleTupleMap( - m: Map[(Map[String, Int], Map[Int, String]), (Map[String, Int], Map[Int, String])] -) -case class SampleTupleTuple( - m: Map[((String, Boolean), (Int, Double)), ((String, Boolean), (Int, Double))] -) -case class SampleTupleClass( - m: Map[(SampleChar, SampleInt), (SampleChar, SampleInt)] -) -case class SampleTupleTrait(m: Map[(Pet, Pet), (Pet, Pet)]) -case class SampleTupleAny(m: Map[(Any, Any), (Any, Any)]) -case class SampleTupleOptional( - m: Map[(Option[Int], Option[String]), (Option[Boolean], Option[Food.Value])] -) -case class SampleTupleVC(m: Map[(VCChar, VCChar), (VCChar, VCChar)]) -case class SampleTupleComplex( - m: Map[(ComplexClass, ComplexClass), (ComplexClass, ComplexClass)] -) -case class SampleTuplePolyClass( - m: Map[(PolyClass, PolyClass), (PolyClass, PolyClass)] -) - -// === Value Classes -case class VCBigDecimal(vc: BigDecimal) extends AnyVal -case class SampleVCBigDecimal(m: Map[VCBigDecimal, VCBigDecimal]) -case class VCBigInt(vc: BigInt) extends AnyVal -case class SampleVCBigInt(m: Map[VCBigInt, VCBigInt]) -case class VCBoolean(vc: Boolean) extends AnyVal -case class SampleVCBoolean(m: Map[VCBoolean, VCBoolean]) -case class VCByte(vc: Byte) extends AnyVal -case class SampleVCByte(m: Map[VCByte, VCByte]) -case class VCChar(vc: Char) extends AnyVal -case class SampleVCChar(m: Map[VCChar, VCChar]) -case class VCDouble(vc: Double) extends AnyVal -case class SampleVCDouble(m: Map[VCDouble, VCDouble]) -case class VCEnumeration(vc: Food.Value) extends AnyVal -case class SampleVCEnumeration(m: Map[VCEnumeration, VCEnumeration]) -case class VCFloat(vc: Float) extends AnyVal -case class SampleVCFloat(m: Map[VCFloat, VCFloat]) -case class VCInt(vc: Int) extends AnyVal -case class SampleVCInt(m: Map[VCInt, VCInt]) -case class VCLong(vc: Long) extends AnyVal -case class SampleVCLong(m: Map[VCLong, VCLong]) -case class VCShort(vc: Short) extends AnyVal -case class SampleVCShort(m: Map[VCShort, VCShort]) -case class VCString(vc: String) extends AnyVal -case class SampleVCString(m: Map[VCString, VCString]) -case class VCUUID(vc: UUID) extends AnyVal -case class SampleVCUUID(m: Map[VCUUID, VCUUID]) -case class VCNumber(vc: Number) extends AnyVal -case class SampleVCNumber(m: Map[VCNumber, VCNumber]) -case class VCList(vc: List[Int]) extends AnyVal -case class SampleVCList(m: Map[VCList, VCList]) -case class VCMap(vc: Map[Int, Int]) extends AnyVal -case class SampleVCMap(m: Map[VCMap, VCMap]) -case class VCTuple(vc: Tuple3[Int, String, Boolean]) extends AnyVal -case class SampleVCTuple(m: Map[VCTuple, VCTuple]) - -case class VCClass(vc: ComplexClass) extends AnyVal -case class SampleVCClass(m: Map[VCClass, VCClass]) -case class VCTrait(vc: Pet) extends AnyVal -case class SampleVCTrait(m: Map[VCTrait, VCTrait]) -case class VCOption(vc: Option[String]) extends AnyVal -case class SampleVCOption(m: Map[VCOption, VCOption]) -case class VCNested(vc: List[Map[String, String]]) extends AnyVal -case class SampleVCNested(m: Map[VCNested, VCNested]) - -case class VCParamClass[A, B](vc: AThing[A, B]) extends AnyVal -case class SampleVCParamClass[A, B]( - m: Map[VCParamClass[A, B], VCParamClass[A, B]] -) -case class VCParamTrait[A, B](vc: Thing[A, B]) extends AnyVal -case class SampleVCParamTrait[A, B]( - m: Map[VCParamTrait[A, B], VCParamTrait[A, B]] -) \ No newline at end of file diff --git a/core/src/test/scala/co.blocke.scalajack/yaml/mapkeys/ScalaPrimKeys.scala b/core/src/test/scala/co.blocke.scalajack/yaml/mapkeys/ScalaPrimKeys.scala deleted file mode 100644 index fa838092..00000000 --- a/core/src/test/scala/co.blocke.scalajack/yaml/mapkeys/ScalaPrimKeys.scala +++ /dev/null @@ -1,308 +0,0 @@ -package co.blocke.scalajack -package yaml -package mapkeys - -import TestUtil._ -import munit._ -import munit.internal.console - -class ScalaPrimKeys() extends FunSuite: - - val sj = ScalaJack(YamlFlavor()) - - test("With Any Key") { - describe( - "------------------------------------------\n: Scala Primitive Map Key Tests (YAML) :\n------------------------------------------", Console.BLUE - ) - describe("+++ Positive Tests +++") - val inst = AnyShell( - Map( - List(1, 2, 3) -> List("a", "b", "c"), - DogPet("Fido", Food.Meat, 4) -> DogPet("Fifi", Food.Meat, 4), - Size.Small -> "ok", - 123.456 -> true, - 293845 -> "Greg", - false -> "16", - "Fred" -> "Wilma", - 16.toByte -> null - ) - ) - val yaml = sj.render(inst) - val comparison = """m: - | Fred: Wilma - | 293845: Greg - | ? _hint: co.blocke.scalajack.yaml.mapkeys.DogPet - | name: Fido - | food: Meat - | numLegs: 4 - | : _hint: co.blocke.scalajack.yaml.mapkeys.DogPet - | name: Fifi - | food: Meat - | numLegs: 4 - | 16: null - | false: '16' - | Small: ok - | 123.456: true - | ? - 1 - | - 2 - | - 3 - | : - a - | - b - | - c - |""".stripMargin.asInstanceOf[YAML] - assertEquals(yaml, comparison) - val read = sj.read[AnyShell](yaml).m.keySet.map(z => (z, z.getClass.getName)) - assertEquals(read.contains((16, "java.lang.Integer")), true) - assertEquals(read.contains((293845, "java.lang.Integer")), true) - assertEquals(read.contains((123.456, "java.lang.Double")), true) - assertEquals(read.contains(("Small", "java.lang.String")), true) - assertEquals(read.contains(("Fred", "java.lang.String")), true) - assertEquals(read.contains((false, "java.lang.Boolean")), true) - assertEquals(read.contains( - ( - DogPet("Fido", Food.Meat, 4), - "co.blocke.scalajack.yaml.mapkeys.DogPet" - ) - ), true) - } - - test("With BigDecimal Key") { - val inst = SampleBigDecimal( - Map( - BigDecimal(123.456) -> BigDecimal(1), - BigDecimal(789.123) -> BigDecimal(2) - ) - ) - val yaml = sj.render(inst) - val comparison = """m: - | 123.456: !!float '1' - | 789.123: !!float '2' - |""".stripMargin.asInstanceOf[YAML] - assertEquals(comparison, yaml ) - assertEquals(inst, sj.read[SampleBigDecimal](yaml)) - } - - test("With BigInt Key") { - val inst = - SampleBigInt(Map(BigInt(123) -> BigInt(1), BigInt(789) -> BigInt(2))) - val yaml = sj.render(inst) - val comparison = """m: - | 123: 1 - | 789: 2 - |""".stripMargin.asInstanceOf[YAML] - assertEquals(comparison, yaml ) - assertEquals(inst, sj.read[SampleBigInt](yaml)) - } - - test("With Boolean Key") { - val inst = SampleBoolean(Map(true -> false, false -> true)) - val yaml = sj.render(inst) - val comparison = """m: - | true: false - | false: true - |""".stripMargin.asInstanceOf[YAML] - assertEquals(comparison, yaml ) - assertEquals(inst, sj.read[SampleBoolean](yaml)) - } - - test("With Byte Key") { - val inst = SampleByte(Map(16.toByte -> 2.toByte, 48.toByte -> 9.toByte)) - val yaml = sj.render(inst) - val comparison = """m: - | 16: 2 - | 48: 9 - |""".stripMargin.asInstanceOf[YAML] - assertEquals(comparison, yaml ) - assertEquals(inst, sj.read[SampleByte](yaml)) - } - - test("With Char Key") { - val inst = SampleChar(Map('a' -> 'A', 'z' -> 'Z')) - val yaml = sj.render(inst) - val comparison = """m: - | a: A - | z: Z - |""".stripMargin.asInstanceOf[YAML] - assertEquals(comparison, yaml ) - assertEquals(inst, sj.read[SampleChar](yaml)) - } - - test("With Double Key") { - val inst = SampleDouble(Map(12.34 -> 56.78, 90.12 -> 34.56)) - val yaml = sj.render(inst) - val comparison = """m: - | 12.34: 56.78 - | 90.12: 34.56 - |""".stripMargin.asInstanceOf[YAML] - assertEquals(comparison, yaml ) - assertEquals(inst, sj.read[SampleDouble](yaml)) - } - - test("With Enumeration Key") { - val inst = SampleEnumeration( - Map(Size.Small -> Size.Large, Size.Large -> Size.Medium) - ) - val yaml = sj.render(inst) - val comparison = """m: - | Small: Large - | Large: Medium - |""".stripMargin.asInstanceOf[YAML] - assertEquals(comparison, yaml ) - assertEquals(inst, sj.read[SampleEnumeration](yaml)) - } - - test("With Float Key") { - val inst = SampleFloat(Map(12.34F -> 56.78F, 90.12F -> 34.56F)) - val yaml = sj.render(inst) - val comparison = """m: - | 12.34: 56.78 - | 90.12: 34.56 - |""".stripMargin.asInstanceOf[YAML] - assertEquals(comparison, yaml ) - assertEquals(inst, sj.read[SampleFloat](yaml)) - } - - test("With Int Key") { - val inst = SampleInt(Map(12 -> 56, 90 -> 34)) - val yaml = sj.render(inst) - val comparison = """m: - | 12: 56 - | 90: 34 - |""".stripMargin.asInstanceOf[YAML] - assertEquals(comparison, yaml ) - assertEquals(inst, sj.read[SampleInt](yaml)) - } - - test("With Long Key") { - val inst = SampleLong(Map(12L -> 56L, 90L -> 34L)) - val yaml = sj.render(inst) - val comparison = """m: - | 12: 56 - | 90: 34 - |""".stripMargin.asInstanceOf[YAML] - assertEquals(comparison, yaml ) - assertEquals(inst, sj.read[SampleLong](yaml)) - } - - test("With Short Key") { - val inst = - SampleShort(Map(12.toShort -> 56.toShort, 90.toShort -> 34.toShort)) - val yaml = sj.render(inst) - val comparison = """m: - | 12: 56 - | 90: 34 - |""".stripMargin.asInstanceOf[YAML] - assertEquals(comparison, yaml ) - assertEquals(inst, sj.read[SampleShort](yaml)) - } - - test("Bad BigDecimal Key") { - describe("--- Negative Tests ---") - val yaml = - """m: - | 789.123: 1 - | fred: 2""".stripMargin.asInstanceOf[YAML] - interceptMessage[ScalaJackError]("Line 2: Expected a Number value here: =VAL :fred"){ - sj.read[SampleBigDecimal](yaml) - } - } - - test("Bad BigInt Key") { - val yaml = - """m: - | fred: 1 - | 789: 2""".stripMargin.asInstanceOf[YAML] - interceptMessage[ScalaJackError]("Line 1: Expected a Number value here: =VAL :fred"){ - sj.read[SampleBigInt](yaml) - } - } - - test("Bad Boolean Key") { - val yaml = - """m: - | true: false - | 123: true""".stripMargin.asInstanceOf[YAML] - interceptMessage[ScalaJackError]("Line 2: Expected a Boolean value here: =VAL :123"){ - sj.read[SampleBoolean](yaml) - } - } - - test("Bad Byte Key") { - val yaml = - """m: - | 16: 2 - | x48: 9""".stripMargin.asInstanceOf[YAML] - interceptMessage[ScalaJackError]("Line 2: Expected a Number value here: =VAL :x48"){ - sj.read[SampleByte](yaml) - } - } - - test("Bad Char Key") { // NOTE: This comprehensively tests for any null keyed Map - val yaml = - """m: - | null: A - | z: Z""".stripMargin.asInstanceOf[YAML] - interceptMessage[ScalaJackError]("Line 1: A Char typed value cannot be null"){ - sj.read[SampleChar](yaml) - } - } - - test("Bad Double Key") { - val yaml = - """m: - | 12.34: 56.78 - | true: 34.56""".stripMargin.asInstanceOf[YAML] - interceptMessage[ScalaJackError]("Line 2: Expected a Number value here: =VAL :true"){ - sj.read[SampleDouble](yaml) - } - } - - test("Bad Enumeration Key") { - val yaml = - """m: - | Small: Large - | Bogus: Medium""".stripMargin.asInstanceOf[YAML] - interceptMessage[ScalaJackError]("Line 2: No value found in enumeration co.blocke.scalajack.yaml.mapkeys.Size$ for Bogus"){ - sj.read[SampleEnumeration](yaml) - } - } - - test("Bad Float Key") { - val yaml = - """m: - | 12.34: 56.78 - | 90.12.3: 34.56""".stripMargin.asInstanceOf[YAML] - interceptMessage[ScalaJackError]("Line 2: Cannot parse an Float from value"){ - sj.read[SampleFloat](yaml) - } - } - - test("Bad Int Key") { - val yaml = - """m: - | 12.0: 56 - | 90: 34""".stripMargin.asInstanceOf[YAML] - interceptMessage[ScalaJackError]("Line 1: Cannot parse an Int from value"){ - sj.read[SampleInt](yaml) - } - } - - test("Bad Long Key") { - val yaml = - """m: - | 12: 56 - | hey: 34""".stripMargin.asInstanceOf[YAML] - interceptMessage[ScalaJackError]("Line 2: Expected a Number value here: =VAL :hey"){ - sj.read[SampleLong](yaml) - } - } - - test("Bad Short Key") { - val yaml = - """m: - | p99999: 56 - | 90: 34""".stripMargin.asInstanceOf[YAML] - interceptMessage[ScalaJackError]("Line 1: Expected a Number value here: =VAL :p99999"){ - sj.read[SampleShort](yaml) - } - } \ No newline at end of file diff --git a/core/src/test/scala/co.blocke.scalajack/yaml/mapkeys/TupleCollKeys.scala b/core/src/test/scala/co.blocke.scalajack/yaml/mapkeys/TupleCollKeys.scala deleted file mode 100644 index 9220a090..00000000 --- a/core/src/test/scala/co.blocke.scalajack/yaml/mapkeys/TupleCollKeys.scala +++ /dev/null @@ -1,450 +0,0 @@ -package co.blocke.scalajack -package yaml -package mapkeys - -import TestUtil._ -import munit._ -import munit.internal.console -import java.util.UUID -import model.StringMatchHintModifier -import co.blocke.scala_reflection.RType - -class TupleCollKeys() extends FunSuite: - - val sj = ScalaJack(YamlFlavor()) - - test("Tuple as key") { - describe( - "--------------------------------\n: Tuple Map Key Tests (YAML) :\n--------------------------------", Console.BLUE - ) - val a = (2, "Blather", 'Q') - val b = ("Foo", true) - val inst = SampleTuple(Map(a -> b)) - val yaml = sj.render(inst) - val comparison = """m: - | ? [2, Blather, Q] - | : [Foo, true] - |""".stripMargin.asInstanceOf[YAML] - assertEquals(comparison, yaml ) - assertEquals(inst, sj.read[SampleTuple](yaml)) - } - - test("Tuple as key, null tuple as value") { - val a = (2, "Blather", 'Q') - val b: (String, Boolean) = null - val inst = SampleTuple(Map(a -> b)) - val yaml = sj.render(inst) - val comparison = """m: - | ? [2, Blather, Q] - | : null - |""".stripMargin.asInstanceOf[YAML] - assertEquals(comparison, yaml ) - assertEquals(inst, sj.read[SampleTuple](yaml)) - } - - test("Tuple of Lists as key") { - val a = (List("one", "two", "three"), List(1, 2, 3)) - val b = (List("four", "five", "six"), List(4, 5, 6)) - val inst = SampleTupleList(Map(a -> b)) - val yaml = sj.render(inst) - val comparison = """m: - | ? - [one, two, three] - | - [1, 2, 3] - | : - [four, five, six] - | - [4, 5, 6] - |""".stripMargin.asInstanceOf[YAML] - assertEquals(comparison, yaml ) - assertEquals(inst, sj.read[SampleTupleList](yaml)) - } - - test("Tuple of Maps as key") { - val a = (Map("one" -> 1), Map(2 -> "two")) - val b = (Map("three" -> 3), Map(4 -> "four")) - val inst = SampleTupleMap(Map(a -> b)) - val yaml = sj.render(inst) - val comparison = """m: - | ? - one: 1 - | - 2: two - | : - three: 3 - | - 4: four - |""".stripMargin.asInstanceOf[YAML] - assertEquals(comparison, yaml ) - assertEquals(inst, sj.read[SampleTupleMap](yaml)) - } - - test("Tuple of Tuples as key") { - val a = (("yes", true), (1, 0.25)) - val b = (("no", false), (2, 0.5)) - val inst = SampleTupleTuple(Map(a -> b)) - val yaml = sj.render(inst) - val comparison = """m: - | ? - [yes, true] - | - [1, 0.25] - | : - [no, false] - | - [2, 0.5] - |""".stripMargin.asInstanceOf[YAML] - assertEquals(comparison, yaml ) - assertEquals(inst, sj.read[SampleTupleTuple](yaml)) - } - - test("Tuple of Case Class as key") { - val a = (SampleChar(Map('a' -> 'A')), SampleInt(Map(1 -> 2))) - val b = (SampleChar(Map('z' -> 'Z')), SampleInt(Map(99 -> 100))) - val inst = SampleTupleClass(Map(a -> b)) - val yaml = sj.render(inst) - val comparison = """m: - | ? - m: - | a: A - | - m: - | 1: 2 - | : - m: - | z: Z - | - m: - | 99: 100 - |""".stripMargin.asInstanceOf[YAML] - assertEquals(comparison, yaml ) - assertEquals(inst, sj.read[SampleTupleClass](yaml)) - } - - test("Tuple of Trait as key") { - val a = (DogPet("Fido", Food.Meat, 4), FishPet("Jaws", Food.Meat, 69.8)) - val b = - (DogPet("Chey", Food.Meat, 3), FishPet("Flipper", Food.Seeds, 80.1)) - val inst = SampleTupleTrait(Map(a -> b)) - val yaml = sj.render(inst) - val comparison = """m: - | ? - _hint: co.blocke.scalajack.yaml.mapkeys.DogPet - | name: Fido - | food: Meat - | numLegs: 4 - | - _hint: co.blocke.scalajack.yaml.mapkeys.FishPet - | name: Jaws - | food: Meat - | waterTemp: 69.8 - | : - _hint: co.blocke.scalajack.yaml.mapkeys.DogPet - | name: Chey - | food: Meat - | numLegs: 3 - | - _hint: co.blocke.scalajack.yaml.mapkeys.FishPet - | name: Flipper - | food: Seeds - | waterTemp: 80.1 - |""".stripMargin.asInstanceOf[YAML] - assertEquals(comparison, yaml ) - assertEquals(inst, sj.read[SampleTupleTrait](yaml)) - } - - test("Tuple of Any as key") { - val a = (DogPet("Fido", Food.Meat, 4), FishPet("Jaws", Food.Meat, 69.8)) - val b = - (DogPet("Chey", Food.Meat, 3), FishPet("Flipper", Food.Seeds, 80.1)) - val inst = SampleTupleTrait(Map(a -> b)) - val yaml = sj.render(inst) - val comparison = """m: - | ? - _hint: co.blocke.scalajack.yaml.mapkeys.DogPet - | name: Fido - | food: Meat - | numLegs: 4 - | - _hint: co.blocke.scalajack.yaml.mapkeys.FishPet - | name: Jaws - | food: Meat - | waterTemp: 69.8 - | : - _hint: co.blocke.scalajack.yaml.mapkeys.DogPet - | name: Chey - | food: Meat - | numLegs: 3 - | - _hint: co.blocke.scalajack.yaml.mapkeys.FishPet - | name: Flipper - | food: Seeds - | waterTemp: 80.1 - |""".stripMargin.asInstanceOf[YAML] - assertEquals(comparison, yaml ) - assertEquals(inst, sj.read[SampleTupleTrait](yaml)) - } - - test("Tuple of Optional as key") { - val a = (Some(5), Some("Fred")) - val b = (None, Some(Food.Meat)) - val inst = SampleTupleOptional(Map(a -> b)) - val yaml = sj.render(inst) - val comparison = """m: - | ? - 5 - | - Fred - | : - null - | - Meat - |""".stripMargin.asInstanceOf[YAML] - assertEquals(comparison, yaml ) - assertEquals(inst, sj.read[SampleTupleOptional](yaml)) - } - - test("Tuple of ValueClass as key") { - val a = (VCChar('a'), VCChar('A')) - val b = (VCChar('z'), VCChar('Z')) - val inst = SampleTupleVC(Map(a -> b)) - val yaml = sj.render(inst) - val comparison = """m: - | ? - a - | - A - | : - z - | - Z - |""".stripMargin.asInstanceOf[YAML] - assertEquals(comparison, yaml ) - assertEquals(inst, sj.read[SampleTupleVC](yaml)) - } - - test("Complex class (having members that are classes) as key") { - val a = SimpleClass("Larry", 32, isOk = true, "golf") - val b = SimpleClass("Mike", 27, isOk = false, 125) - val c1 = ComplexClass( - UUID.fromString("1e6c2b31-4dfe-4bf6-a0a0-882caaff0e9c"), - a, - allDone = true - ) - val c2 = ComplexClass( - UUID.fromString("1e6c2b31-4dfe-4bf6-a0a0-882caaff0e9d"), - b, - allDone = false - ) - val t1 = (c1, c2) - val t2 = (c2, c1) - val inst = SampleTupleComplex(Map(t1 -> t2)) - val yaml = sj.render(inst) - val comparison = """m: - | ? - id: 1e6c2b31-4dfe-4bf6-a0a0-882caaff0e9c - | simple: - | name: Larry - | age: 32 - | isOk: true - | favorite: golf - | allDone: true - | - id: 1e6c2b31-4dfe-4bf6-a0a0-882caaff0e9d - | simple: - | name: Mike - | age: 27 - | isOk: false - | favorite: 125 - | allDone: false - | : - id: 1e6c2b31-4dfe-4bf6-a0a0-882caaff0e9d - | simple: - | name: Mike - | age: 27 - | isOk: false - | favorite: 125 - | allDone: false - | - id: 1e6c2b31-4dfe-4bf6-a0a0-882caaff0e9c - | simple: - | name: Larry - | age: 32 - | isOk: true - | favorite: golf - | allDone: true - |""".stripMargin.asInstanceOf[YAML] - assertEquals(comparison, yaml ) - assertEquals(inst, sj.read[SampleTupleComplex](yaml)) - } - - test("Class having collections as members") { - val a = PolyClass(Map("a" -> 1, "b" -> 2), List("one", "two")) - val b = PolyClass(Map("x" -> 9, "y" -> 10), List("aye", "you")) - val t1 = (a, b) - val t2 = (b, a) - val inst = SampleTuplePolyClass(Map(t1 -> t2)) - val yaml = sj.render(inst) - val comparison = """m: - | ? - lookup: - | a: 1 - | b: 2 - | favs: [one, two] - | - lookup: - | x: 9 - | y: 10 - | favs: [aye, you] - | : - lookup: - | x: 9 - | y: 10 - | favs: [aye, you] - | - lookup: - | a: 1 - | b: 2 - | favs: [one, two] - |""".stripMargin.asInstanceOf[YAML] - assertEquals(comparison, yaml ) - assertEquals(inst, sj.read[SampleTuplePolyClass](yaml)) - } - - test("Class having collections as members (empty collections") { - val a = PolyClass(Map.empty[String, Int], List.empty[String]) - val b = PolyClass(Map.empty[String, Int], List.empty[String]) - val t1 = (a, b) - val t2 = (b, a) - val inst = Map(t1 -> t2) - val yaml = sj.render(inst) - val comparison = """? - lookup: {} - | favs: [] - | - lookup: {} - | favs: [] - |: - lookup: {} - | favs: [] - | - lookup: {} - | favs: [] - |""".stripMargin.asInstanceOf[YAML] - assertEquals(comparison, yaml ) - assertEquals(inst, sj.read[Map[(PolyClass, PolyClass), (PolyClass, PolyClass)]](yaml)) - } - - test("Custom trait hint field and value for key trait") { - val petHintMod = StringMatchHintModifier( - Map("BreathsWater" -> "co.blocke.scalajack.yaml.mapkeys.FishPet", "BreathsAir" -> "co.blocke.scalajack.yaml.mapkeys.DogPet") - ) - val sj2 = sj - .withHints(RType.of[Pet] -> "kind") - .withHintModifiers(RType.of[Pet] -> petHintMod) - val a: Pet = FishPet("Flipper", Food.Veggies, 74.33) - val b: Pet = DogPet("Fido", Food.Meat, 3) - val t1 = (a, b) - val t2 = (b, a) - val inst = Map(t1 -> t2) - val yaml = sj2.render(inst) - val comparison = """? - kind: BreathsWater - | name: Flipper - | food: Veggies - | waterTemp: 74.33 - | - kind: BreathsAir - | name: Fido - | food: Meat - | numLegs: 3 - |: - kind: BreathsAir - | name: Fido - | food: Meat - | numLegs: 3 - | - kind: BreathsWater - | name: Flipper - | food: Veggies - | waterTemp: 74.33 - |""".stripMargin.asInstanceOf[YAML] - assertEquals(comparison, yaml ) - assertEquals(inst, sj2.read[Map[(Pet, Pet), (Pet, Pet)]](yaml)) - } - - test("Custom trait hint field and value for key member's trait") { - val petHintMod = StringMatchHintModifier( - Map("BreathsWater" -> "co.blocke.scalajack.yaml.mapkeys.FishPet", "BreathsAir" -> "co.blocke.scalajack.yaml.mapkeys.DogPet") - ) - val sj2 = sj - .withHints(RType.of[Pet] -> "kind") - .withHintModifiers(RType.of[Pet] -> petHintMod) - - val a: PetHolder = - ShinyPetHolder("123 Main", FishPet("Flipper", Food.Veggies, 74.33)) - val b: PetHolder = - ShinyPetHolder("210 North", DogPet("Fido", Food.Meat, 3)) - val t1 = (a, b) - val t2 = (b, a) - val inst = Map(t1 -> t2) - val yaml = sj2.render(inst) - val comparison = """? - _hint: co.blocke.scalajack.yaml.mapkeys.ShinyPetHolder - | address: 123 Main - | pet: - | kind: BreathsWater - | name: Flipper - | food: Veggies - | waterTemp: 74.33 - | - _hint: co.blocke.scalajack.yaml.mapkeys.ShinyPetHolder - | address: 210 North - | pet: - | kind: BreathsAir - | name: Fido - | food: Meat - | numLegs: 3 - |: - _hint: co.blocke.scalajack.yaml.mapkeys.ShinyPetHolder - | address: 210 North - | pet: - | kind: BreathsAir - | name: Fido - | food: Meat - | numLegs: 3 - | - _hint: co.blocke.scalajack.yaml.mapkeys.ShinyPetHolder - | address: 123 Main - | pet: - | kind: BreathsWater - | name: Flipper - | food: Veggies - | waterTemp: 74.33 - |""".stripMargin.asInstanceOf[YAML] - assertEquals(comparison, yaml ) - assertEquals(inst, sj2.read[Map[(PetHolder, PetHolder), (PetHolder, PetHolder)]](yaml)) - } - - test("Parameterized class") { - val t1 = (AThing("wow", 4), AThing("boom", 1)) - val t2 = (AThing("yep", 3), AThing("yikes", 11)) - val inst = Map(t1 -> t2) - val yaml = sj.render(inst) - val comparison = """? - a: wow - | b: 4 - | - a: boom - | b: 1 - |: - a: yep - | b: 3 - | - a: yikes - | b: 11 - |""".stripMargin.asInstanceOf[YAML] - assertEquals(comparison, yaml ) - assertEquals(inst, sj.read[Map[(AThing[Int, String], AThing[Int, String]), (AThing[Int, String], AThing[Int, String])]](yaml)) - } - - test("Parameterized trait") { - val t1: (Thing[String, Int], Thing[String, Int]) = - (AThing("wow", 4), AThing("boom", 1)) - val t2: (Thing[String, Int], Thing[String, Int]) = - (AThing("yep", 3), AThing("yikes", 11)) - val inst = Map(t1 -> t2) - val yaml = sj.render(inst) - val comparison = """? - _hint: co.blocke.scalajack.yaml.mapkeys.AThing - | a: wow - | b: 4 - | - _hint: co.blocke.scalajack.yaml.mapkeys.AThing - | a: boom - | b: 1 - |: - _hint: co.blocke.scalajack.yaml.mapkeys.AThing - | a: yep - | b: 3 - | - _hint: co.blocke.scalajack.yaml.mapkeys.AThing - | a: yikes - | b: 11 - |""".stripMargin.asInstanceOf[YAML] - assertEquals(comparison, yaml ) - assertEquals(inst, sj.read[Map[(Thing[String, Int], Thing[String, Int]), (Thing[String, Int], Thing[String, Int])]](yaml)) - } - - test("Parameterized trait having parameterized trait members") { - val t1: (Thing[String, Part[Double]], Thing[String, Part[Double]]) = - (AThing("wow", APart(1.2)), AThing("boom", APart(2.3))) - val t2: (Thing[String, Part[Double]], Thing[String, Part[Double]]) = - (AThing("yep", APart(4.5)), AThing("yikes", APart(6.7))) - val inst = Map(t1 -> t2) - val yaml = sj.render(inst) - val comparison = """? - _hint: co.blocke.scalajack.yaml.mapkeys.AThing - | a: wow - | b: - | _hint: co.blocke.scalajack.yaml.mapkeys.APart - | p: 1.2 - | - _hint: co.blocke.scalajack.yaml.mapkeys.AThing - | a: boom - | b: - | _hint: co.blocke.scalajack.yaml.mapkeys.APart - | p: 2.3 - |: - _hint: co.blocke.scalajack.yaml.mapkeys.AThing - | a: yep - | b: - | _hint: co.blocke.scalajack.yaml.mapkeys.APart - | p: 4.5 - | - _hint: co.blocke.scalajack.yaml.mapkeys.AThing - | a: yikes - | b: - | _hint: co.blocke.scalajack.yaml.mapkeys.APart - | p: 6.7 - |""".stripMargin.asInstanceOf[YAML] - assertEquals(comparison, yaml ) - assertEquals(inst, sj.read[Map[(Thing[String, Part[Double]], Thing[String, Part[Double]]), (Thing[String, Part[Double]], Thing[String, Part[Double]])]](yaml)) - } \ No newline at end of file diff --git a/core/src/test/scala/co.blocke.scalajack/yaml/mapkeys/ValueClassKeys.scala b/core/src/test/scala/co.blocke.scalajack/yaml/mapkeys/ValueClassKeys.scala deleted file mode 100644 index f8937334..00000000 --- a/core/src/test/scala/co.blocke.scalajack/yaml/mapkeys/ValueClassKeys.scala +++ /dev/null @@ -1,569 +0,0 @@ -package co.blocke.scalajack -package yaml -package mapkeys - -import TestUtil._ -import munit._ -import munit.internal.console -import java.util.UUID - -class ValueClassKeys() extends FunSuite: - - val sj = ScalaJack(YamlFlavor()) - - test("Value class of BigDecimal") { - describe( - "-------------------------------------\n: ValueClass Map Key Tests (YAML) :\n-------------------------------------", Console.BLUE - ) - describe("+++ Positive Primitive Tests +++") - val inst = SampleVCBigDecimal( - Map( - VCBigDecimal(BigDecimal(12.34)) -> VCBigDecimal(BigDecimal(56.78)) - ) - ) - val yaml = sj.render(inst) - val comparison = """m: - | 12.34: 56.78 - |""".stripMargin.asInstanceOf[YAML] - assertEquals(comparison, yaml ) - assertEquals(inst, sj.read[SampleVCBigDecimal](yaml)) - } - - test("Value class of BigInt") { - val inst = - SampleVCBigInt(Map(VCBigInt(BigInt(12)) -> VCBigInt(BigInt(56)))) - val yaml = sj.render(inst) - val comparison = """m: - | 12: 56 - |""".stripMargin.asInstanceOf[YAML] - assertEquals(comparison, yaml ) - assertEquals(inst, sj.read[SampleVCBigInt](yaml)) - } - - test("Value class of Byte") { - val inst = SampleVCByte( - Map(VCByte(12.asInstanceOf[Byte]) -> VCByte(56.asInstanceOf[Byte])) - ) - val yaml = sj.render(inst) - val comparison = """m: - | 12: 56 - |""".stripMargin.asInstanceOf[YAML] - assertEquals(comparison, yaml ) - assertEquals(inst, sj.read[SampleVCByte](yaml)) - } - - test("Value class of Boolean") { - val inst = SampleVCBoolean(Map(VCBoolean(true) -> VCBoolean(false))) - val yaml = sj.render(inst) - val comparison = """m: - | true: false - |""".stripMargin.asInstanceOf[YAML] - assertEquals(comparison, yaml ) - assertEquals(inst, sj.read[SampleVCBoolean](yaml)) - } - - test("Value class of Char") { - val inst = SampleVCChar(Map(VCChar('a') -> VCChar('A'))) - val yaml = sj.render(inst) - val comparison = """m: - | a: A - |""".stripMargin.asInstanceOf[YAML] - assertEquals(comparison, yaml ) - assertEquals(inst, sj.read[SampleVCChar](yaml)) - } - - test("Value class of Double") { - val inst = SampleVCDouble(Map(VCDouble(12.34) -> VCDouble(56.78))) - val yaml = sj.render(inst) - val comparison = """m: - | 12.34: 56.78 - |""".stripMargin.asInstanceOf[YAML] - assertEquals(comparison, yaml ) - assertEquals(inst, sj.read[SampleVCDouble](yaml)) - } - - test("Value class of Enumeration") { - val inst = SampleVCEnumeration( - Map(VCEnumeration(Food.Veggies) -> VCEnumeration(Food.Meat)) - ) - val yaml = sj.render(inst) - val comparison = """m: - | Veggies: Meat - |""".stripMargin.asInstanceOf[YAML] - assertEquals(comparison, yaml ) - assertEquals(inst, sj.read[SampleVCEnumeration](yaml)) - } - - test("Value class of Float") { - val inst = SampleVCFloat(Map(VCFloat(12.34F) -> VCFloat(56.2F))) - val yaml = sj.render(inst) - val comparison = """m: - | 12.34: 56.2 - |""".stripMargin.asInstanceOf[YAML] - assertEquals(comparison, yaml ) - assertEquals(inst, sj.read[SampleVCFloat](yaml)) - } - - test("Value class of Int") { - val inst = SampleVCInt(Map(VCInt(12) -> VCInt(56))) - val yaml = sj.render(inst) - val comparison = """m: - | 12: 56 - |""".stripMargin.asInstanceOf[YAML] - assertEquals(comparison, yaml ) - assertEquals(inst, sj.read[SampleVCInt](yaml)) - } - - test("Value class of Long") { - val inst = SampleVCLong(Map(VCLong(12L) -> VCLong(56L))) - val yaml = sj.render(inst) - val comparison = """m: - | 12: 56 - |""".stripMargin.asInstanceOf[YAML] - assertEquals(comparison, yaml ) - assertEquals(inst, sj.read[SampleVCLong](yaml)) - } - - test("Value class of Short") { - val inst = SampleVCShort( - Map( - VCShort(12.asInstanceOf[Short]) -> VCShort(56.asInstanceOf[Short]) - ) - ) - val yaml = sj.render(inst) - val comparison = """m: - | 12: 56 - |""".stripMargin.asInstanceOf[YAML] - assertEquals(comparison, yaml ) - assertEquals(inst, sj.read[SampleVCShort](yaml)) - } - - test("Value class of String") { - val inst = SampleVCString(Map(VCString("A") -> VCString("B"))) - val yaml = sj.render(inst) - val comparison = """m: - | A: B - |""".stripMargin.asInstanceOf[YAML] - assertEquals(comparison, yaml ) - assertEquals(inst, sj.read[SampleVCString](yaml)) - } - - test("Value class of UUID") { - val inst = SampleVCUUID( - Map( - VCUUID(UUID.fromString("54cab778-7b9e-4b07-9d37-87b97a011e56")) -> VCUUID( - UUID.fromString("54cab778-7b9e-4b07-9d37-87b97a011e55") - ) - ) - ) - val yaml = sj.render(inst) - val comparison = """m: - | 54cab778-7b9e-4b07-9d37-87b97a011e56: 54cab778-7b9e-4b07-9d37-87b97a011e55 - |""".stripMargin.asInstanceOf[YAML] - assertEquals(comparison, yaml ) - assertEquals(inst, sj.read[SampleVCUUID](yaml)) - } - - test("Value class of List") { - describe("+++ Positive Collection Tests +++") - val inst = - SampleVCList(Map(VCList(List(1, 2, 3)) -> VCList(List(4, 5, 6)))) - val yaml = sj.render(inst) - val comparison = """m: - | ? [1, 2, 3] - | : [4, 5, 6] - |""".stripMargin.asInstanceOf[YAML] - assertEquals(comparison, yaml ) - assertEquals(inst, sj.read[SampleVCList](yaml)) - } - - test("Value class of List (empty List)") { - val inst = - SampleVCList(Map(VCList(List.empty[Int]) -> VCList(List.empty[Int]))) - val yaml = sj.render(inst) - val comparison = """m: - | []: [] - |""".stripMargin.asInstanceOf[YAML] - assertEquals(comparison, yaml ) - assertEquals(inst, sj.read[SampleVCList](yaml)) - } - - test("Value class of Map") { - val inst = SampleVCMap(Map(VCMap(Map(1 -> 2)) -> VCMap(Map(3 -> 4)))) - val yaml = sj.render(inst) - val comparison = """m: - | ? 1: 2 - | : 3: 4 - |""".stripMargin.asInstanceOf[YAML] - assertEquals(comparison, yaml ) - assertEquals(inst, sj.read[SampleVCMap](yaml)) - } - - test("Value class of Map (empty Map") { - val inst = SampleVCMap( - Map(VCMap(Map.empty[Int, Int]) -> VCMap(Map.empty[Int, Int])) - ) - val yaml = sj.render(inst) - val comparison = """m: - | {}: {} - |""".stripMargin.asInstanceOf[YAML] - assertEquals(comparison, yaml ) - assertEquals(inst, sj.read[SampleVCMap](yaml)) - } - - test("Value class of Tupple") { - val inst = SampleVCTuple( - Map(VCTuple((1, "one", true)) -> VCTuple((2, "two", false))) - ) - val yaml = sj.render(inst) - val comparison = """m: - | ? [1, one, true] - | : [2, two, false] - |""".stripMargin.asInstanceOf[YAML] - assertEquals(comparison, yaml ) - assertEquals(inst, sj.read[SampleVCTuple](yaml)) - } - - test("Value class of Case Class") { - describe("+++ Positive Complex Tests +++") - val a = SimpleClass("Larry", 32, isOk = true, "golf") - val b = SimpleClass("Mike", 27, isOk = false, 125) - val c1 = ComplexClass( - UUID.fromString("1e6c2b31-4dfe-4bf6-a0a0-882caaff0e9c"), - a, - allDone = true - ) - val c2 = ComplexClass( - UUID.fromString("1e6c2b31-4dfe-4bf6-a0a0-882caaff0e9d"), - b, - allDone = false - ) - val inst = SampleVCClass(Map(VCClass(c1) -> VCClass(c2))) - val yaml = sj.render(inst) - val comparison = """m: - | ? id: 1e6c2b31-4dfe-4bf6-a0a0-882caaff0e9c - | simple: - | name: Larry - | age: 32 - | isOk: true - | favorite: golf - | allDone: true - | : id: 1e6c2b31-4dfe-4bf6-a0a0-882caaff0e9d - | simple: - | name: Mike - | age: 27 - | isOk: false - | favorite: 125 - | allDone: false - |""".stripMargin.asInstanceOf[YAML] - assertEquals(comparison, yaml ) - assertEquals(inst, sj.read[SampleVCClass](yaml)) - } - - test("Value class of Trait") { - val a: Pet = FishPet("Flipper", Food.Veggies, 74.33) - val b: Pet = DogPet("Fido", Food.Meat, 3) - val inst = SampleVCTrait(Map(VCTrait(a) -> VCTrait(b))) - val yaml = sj.render(inst) - val comparison = """m: - | ? _hint: co.blocke.scalajack.yaml.mapkeys.FishPet - | name: Flipper - | food: Veggies - | waterTemp: 74.33 - | : _hint: co.blocke.scalajack.yaml.mapkeys.DogPet - | name: Fido - | food: Meat - | numLegs: 3 - |""".stripMargin.asInstanceOf[YAML] - assertEquals(comparison, yaml ) - assertEquals(inst, sj.read[SampleVCTrait](yaml)) - } - - test("Value class of Parameterized Case Class") { - val a = AThing(5, "wow") - val b = AThing(6, "zoom") - val inst = SampleVCParamClass(Map(VCParamClass(a) -> VCParamClass(b))) - val yaml = sj.render(inst) - val comparison = """m: - | ? a: 5 - | b: wow - | : a: 6 - | b: zoom - |""".stripMargin.asInstanceOf[YAML] - assertEquals(comparison, yaml ) - assertEquals(inst, sj.read[SampleVCParamClass[String, Int]](yaml)) - } - - test("Value class of Parameterized Trait") { - val a: Thing[Int, String] = AThing(5, "wow") - val b: Thing[Int, String] = AThing(6, "zoom") - val inst = SampleVCParamTrait(Map(VCParamTrait(a) -> VCParamTrait(b))) - val yaml = sj.render(inst) - val comparison = """m: - | ? _hint: co.blocke.scalajack.yaml.mapkeys.AThing - | a: 5 - | b: wow - | : _hint: co.blocke.scalajack.yaml.mapkeys.AThing - | a: 6 - | b: zoom - |""".stripMargin.asInstanceOf[YAML] - assertEquals(comparison, yaml ) - assertEquals(inst, sj.read[SampleVCParamTrait[Int, String]](yaml)) - } - - test("Value class of Option") { - val inst = - SampleVCOption(Map(VCOption(Some("here")) -> VCOption(Some("there")))) - val yaml = sj.render(inst) - val comparison = """m: - | here: there - |""".stripMargin.asInstanceOf[YAML] - assertEquals(comparison, yaml ) - assertEquals(inst, sj.read[SampleVCOption](yaml)) - } - - test("Value class of nested collection") { - val a = VCNested(List(Map("a" -> "b"), Map("c" -> "d"))) - val b = VCNested(List(Map("t" -> "u"), Map("x" -> "y"))) - val inst = SampleVCNested(Map(a -> b)) - val yaml = sj.render(inst) - val comparison = """m: - | ? - a: b - | - c: d - | : - t: u - | - x: y - |""".stripMargin.asInstanceOf[YAML] - assertEquals(comparison, yaml ) - assertEquals(inst, sj.read[SampleVCNested](yaml)) - } - - test("Bad BigDecimal Key") { - describe("--- Negative Primitive Tests ---") - val yaml = """m: - | true: 56.78""".stripMargin.asInstanceOf[YAML] - interceptMessage[ScalaJackError]("Line 1: Expected a Number value here: =VAL :true"){ - sj.read[SampleVCBigDecimal](yaml) - } - } - - test("Bad BigInt Key") { - val yaml = """m: - | "12.5": 56""".stripMargin.asInstanceOf[YAML] - interceptMessage[java.lang.NumberFormatException]("For input string: \"12.5\""){ - sj.read[SampleVCBigInt](yaml) - } - } - - test("Bad Boolean Key") { - val yaml = """m: - | 1: false""".stripMargin.asInstanceOf[YAML] - interceptMessage[ScalaJackError]("Line 1: Expected a Boolean value here: =VAL :1"){ - sj.read[SampleVCBoolean](yaml) - } - } - - test("Bad Char Key") { - val yaml = """m: - | null: A""".stripMargin.asInstanceOf[YAML] - interceptMessage[ScalaJackError]("Line 1: A Char typed value cannot be null"){ - sj.read[SampleVCChar](yaml) - } - } - - test("Bad Double Key") { - val yaml = """m: - | false: 56.78""".stripMargin.asInstanceOf[YAML] - interceptMessage[ScalaJackError]("Line 1: Expected a Number value here: =VAL :false"){ - sj.read[SampleVCDouble](yaml) - } - } - - test("Bad Enumeration Key") { - val yaml = """m: - | Bogus: Meat""".stripMargin.asInstanceOf[YAML] - interceptMessage[ScalaJackError]("Line 1: No value found in enumeration co.blocke.scalajack.yaml.mapkeys.Food$ for Bogus"){ - sj.read[SampleVCEnumeration](yaml) - } - } - - test("Bad Float Key") { - val yaml = """m: - | hey: 56.2""".stripMargin.asInstanceOf[YAML] - interceptMessage[ScalaJackError]("Line 1: Expected a Number value here: =VAL :hey"){ - sj.read[SampleVCFloat](yaml) - } - } - - test("Bad Int Key") { - val yaml = """m: - | "12.5": 56""".stripMargin.asInstanceOf[YAML] - interceptMessage[ScalaJackError]("Line 1: Cannot parse an Int from value"){ - sj.read[SampleVCInt](yaml) - } - } - - test("Bad Long Key") { - val yaml = """m: - | "12.5": 56""".stripMargin.asInstanceOf[YAML] - interceptMessage[ScalaJackError]("Line 1: Cannot parse an Long from value"){ - sj.read[SampleVCLong](yaml) - } - } - - test("Bad Short Key") { - val yaml = """m: - | "12.5": 56""".stripMargin.asInstanceOf[YAML] - interceptMessage[ScalaJackError]("Line 1: Cannot parse an Short from value"){ - sj.read[SampleVCShort](yaml) - } - } - - test("Bad String Key") { - val yaml = """m: - | [1,2]: B""".stripMargin.asInstanceOf[YAML] - interceptMessage[ScalaJackError]("Line 1: Expected a String here: +SEQ"){ - sj.read[SampleVCString](yaml) - } - } - - test("Bad UUID Key") { - val yaml = """m: - | bogus: "54cab778-7b9e-4b07-9d37-87b97a011e55"""".stripMargin.asInstanceOf[YAML] - interceptMessage[ScalaJackError]("Line 1: Failed to create UUID value from parsed text bogus"){ - sj.read[SampleVCUUID](yaml) - } - } - - test("Bad List Key") { - describe("--- Negative Collection Tests ---") - val yaml = - """m: - | [1,2,"a"]: [4,5,6]""".stripMargin.asInstanceOf[YAML] - interceptMessage[ScalaJackError]("Line 1: Expected a Number value here: =VAL \"a"){ - sj.read[SampleVCList](yaml) - } - } - - test("Bad Map Key") { - val yaml = - """m: - | ? - | true: 2 - | : - | 3: 4""".stripMargin.asInstanceOf[YAML] - interceptMessage[ScalaJackError]("Line 2: Expected a Number value here: =VAL :true"){ - sj.read[SampleVCMap](yaml) - } - } - - test("Bad Tuple Key") { - val yaml = - """m: - | ? - | [1,one,true,1] - | : [2,two,false]""".stripMargin.asInstanceOf[YAML] - interceptMessage[ScalaJackError]("Line 2: Expected a String here: -SEQ"){ - sj.read[SampleVCTuple](yaml) - } - } - - test("Bad Case Class Key") { - describe("--- Negative Complex Tests ---") - val yaml = - """m: - | ? - | id: "1e6c2b31-4dfe-4bf6-a0a0-882caaff0e9c" - | simple: - | bogus: Larry - | age: 32 - | isOk: true - | favorite: golf - | allDone: true - | : - | id: "1e6c2b31-4dfe-4bf6-a0a0-882caaff0e9d" - | simple: - | name: Mike - | age: 27 - | isOk: false - | favorite: 125 - | allDone: false""".stripMargin.asInstanceOf[YAML] - interceptMessage[ScalaJackError]("Line 8: Class co.blocke.scalajack.yaml.mapkeys.SimpleClass missing required fields: name"){ - sj.read[SampleVCClass](yaml) - } - } - - test("Bad Trait Key") { - val yaml = - """m: - | ? - | _hint: "co.blocke.scalajack.yaml.mapkeys.Bogus" - | name: Flipper - | food: Veggies - | waterTemp: 74.33 - | : - | _hint: "co.blocke.scalajack.yaml.mapkeys.DogPet" - | name: Fido - | food: Meat - | numLegs: 3""".stripMargin.asInstanceOf[YAML] - interceptMessage[java.lang.ClassNotFoundException]("co.blocke.scalajack.yaml.mapkeys.Bogus"){ - sj.read[SampleVCTrait](yaml) - } - } - - test("Bad Parameterized Case Class Key") { - val yaml = - """m: - | ? - | a: 5.5 - | b: wow - | : - | a: 6 - | b: zoom""".stripMargin.asInstanceOf[YAML] - interceptMessage[ScalaJackError]("Line 2: Cannot parse an Int from value"){ - sj.read[SampleVCParamClass[String, Int]](yaml) - } - } - - test("Bad Parameterized Trait Key") { - val yaml = - """m: - | ? - | _hint: co.blocke.scalajack.yaml.mapkeys.ZThing - | a: 5 - | b: wow - | : - | _hint: "co.blocke.scalajack.mapkeys.AThing" - | a: 6 - | b: zoom""".stripMargin.asInstanceOf[YAML] - interceptMessage[java.lang.ClassNotFoundException]("co.blocke.scalajack.yaml.mapkeys.ZThing"){ - sj.read[SampleVCParamTrait[Int, String]](yaml) - } - - } - - test("Bad Option Key") { - val yaml = - """m: - | [1,2]: there""".stripMargin.asInstanceOf[YAML] - interceptMessage[ScalaJackError]("Line 1: Expected a String here: +SEQ"){ - sj.read[SampleVCOption](yaml) - } - } - - test("Bad Nested Collection Key") { - val yaml = - """m: - | ? - | - - | a: b - | - - | c: [1,2] - | : - | - - | t: u - | - - | x: y""".stripMargin.asInstanceOf[YAML] - interceptMessage[ScalaJackError]("Line 5: Expected a String here: +SEQ"){ - sj.read[SampleVCNested](yaml) - } - } \ No newline at end of file diff --git a/core/src/test/scala/co.blocke.scalajack/yaml/misc/ReadWriterSpec.scala b/core/src/test/scala/co.blocke.scalajack/yaml/misc/ReadWriterSpec.scala deleted file mode 100644 index 7da266b2..00000000 --- a/core/src/test/scala/co.blocke.scalajack/yaml/misc/ReadWriterSpec.scala +++ /dev/null @@ -1,95 +0,0 @@ -package co.blocke.scalajack -package yaml -package misc - -import TestUtil._ -import munit._ -import munit.internal.console -import scala.math.BigInt - -trait Human -case class Person(name: String, age: Int) extends Human -case class Typey[T](thing: T) { - type foom = T -} - -class ReadWriterSpec extends FunSuite: - - val sj = ScalaJack(YamlFlavor()) - - test("Multiline string | and >") { - describe( - "-----------------------------\n: YamlReader Tests (YAML) :\n-----------------------------", Console.BLUE - ) - val yaml = - """a: | - | This - | is - | a - | test - |""".stripMargin.asInstanceOf[YAML] - assertEquals(sj.read[Map[String, String]](yaml), Map("a" -> "This\nis\na\ntest")) - val yaml2 = - """a: > - | This - | is - | a - | test - |""".stripMargin.asInstanceOf[YAML] - assertEquals(sj.read[Map[String, String]](yaml2), Map("a" -> "This is a test")) - } - - test("expectCollection nextText fails") { - val yaml = """a: foo""".asInstanceOf[YAML] - interceptMessage[ScalaJackError]("Line 0: Expected a List here: =VAL :foo"){ - sj.read[Map[String, List[Int]]](yaml) - } - } - - test("Scan for hint on non-object") { - interceptMessage[ScalaJackError]("Line 0: Expected an Object here: =VAL :12"){ - sj.read[Person]("12".asInstanceOf[YAML]) - } - } - - test("Type hint not found in scan") { - val yaml = - """name: Fred - |age: 35 - |""".stripMargin.asInstanceOf[YAML] - interceptMessage[ScalaJackError]("Line 2: Type hint '_hint' not found"){ - sj.read[Human](yaml) - } - } - - test("Resovle type members on non-object") { - interceptMessage[ScalaJackError]("Line 0: Expected an Object here: =VAL :12"){ - sj.read[Typey[Person]]("12".asInstanceOf[YAML]) - } - } - - test("Null Array") { - describe( - "-----------------------------\n: YamlWriter Tests (YAML) :\n-----------------------------", Console.BLUE - ) - val yaml = - """one: [1,2,3] - |two: null - |""".stripMargin.asInstanceOf[YAML] - assertEquals(sj.read[Map[String, List[Int]]](yaml), Map("one" -> List(1, 2, 3), "two" -> null)) - } - - test("Null BigInt") { - val yaml = - """one: 123 - |two: null - |""".stripMargin.asInstanceOf[YAML] - assertEquals(sj.read[Map[String, BigInt]](yaml), Map("one" -> BigInt(123), "two" -> null)) - } - - test("Empty YamlBuilder") { - val b = YamlBuilder() - interceptMessage[ScalaJackError]("No value set for internal yaml builder"){ - b.result() - } - } \ No newline at end of file diff --git a/core/src/test/scala/co.blocke.scalajack/yaml/misc/TypeMembers.scala b/core/src/test/scala/co.blocke.scalajack/yaml/misc/TypeMembers.scala deleted file mode 100644 index acf07ffd..00000000 --- a/core/src/test/scala/co.blocke.scalajack/yaml/misc/TypeMembers.scala +++ /dev/null @@ -1,164 +0,0 @@ -package co.blocke.scalajack -package yaml -package misc - -import TestUtil._ -import munit._ -import munit.internal.console -import co.blocke.scala_reflection.RType -import model._ - - -trait Body -case class FancyBody(message: String) extends Body -case class DefaultBody(message: String = "Unknown body") extends Body -case class AnyBody(stuff: Any) extends Body - -trait Hobby -case class InsideHobby(desc: String) extends Hobby - -case class Envelope[T <: Body](id: String, body: T) { - type Giraffe = T -} - -// Type member X should be ignored! Only used internally -case class BigEnvelope[T <: Body, H <: Hobby, X](id: String, body: T, hobby: H) { - type Giraffe = T - type Hippo = H - type IgnoreMe = X - - val x: IgnoreMe = null.asInstanceOf[IgnoreMe] -} - -case class Bigger(foo: Int, env: Envelope[FancyBody]) - -class TypeMembers extends FunSuite: - - val sj = ScalaJack(YamlFlavor()) - val sj2 = ScalaJack(YamlFlavor()).parseOrElse((RType.of[Body] -> RType.of[DefaultBody])) - - test("Read and match") { - describe( - "------------------------------------\n: Externalized Type Tests (YAML) :\n------------------------------------", Console.BLUE - ) - val yaml = - """Giraffe: co.blocke.scalajack.yaml.misc.FancyBody - |id: ABC - |body: - | message: Hello""".stripMargin.asInstanceOf[YAML] - val expected: Envelope[Body] = Envelope("ABC", FancyBody("Hello")) - assertEquals((expected, 1), { - val x = sj.read[Envelope[Body]](yaml) - // Test match functionality - val num = x.body match { - case _: FancyBody => 1 - case _ => 2 - } - (x, num) - }) - } - - test("Write -- Concrete T value") { - val value: Envelope[FancyBody] = Envelope("DEF", FancyBody("BOO")) - val expected = - """Giraffe: co.blocke.scalajack.yaml.misc.FancyBody - |id: DEF - |body: - | message: BOO - |""".stripMargin.asInstanceOf[YAML] - assertEquals(expected, sj.render[Envelope[FancyBody]](value)) - } - - test("Write -- Trait T value") { - val value: Envelope[Body] = Envelope("DEF", FancyBody("BOO")) - val expected = - """Giraffe: co.blocke.scalajack.yaml.misc.FancyBody - |id: DEF - |body: - | message: BOO - |""".stripMargin.asInstanceOf[YAML] - assertEquals(expected, sj.render[Envelope[Body]](value)) - } - - test("Wrapped") { - val inst = Bigger(25, Envelope("abc", FancyBody("msg here"))) - val yaml = sj.render(inst) - assertEquals( - """foo: 25 - |env: - | Giraffe: co.blocke.scalajack.yaml.misc.FancyBody - | id: abc - | body: - | message: msg here - |""".stripMargin.asInstanceOf[YAML], yaml) - assertEquals(inst, sj.read[Bigger](yaml)) - } - - test("Type modifier works") { - val sjm = sj.withTypeValueModifier( - ClassNameHintModifier( - (hint: String) => "co.blocke.scalajack.yaml.misc." + hint, - (cname: String) => cname.split('.').last - ) - ) - val value: Envelope[Body] = Envelope("DEF", FancyBody("BOO")) - val yaml = sjm.render[Envelope[Body]](value) - assertEquals( - """Giraffe: FancyBody - |id: DEF - |body: - | message: BOO - |""".stripMargin.asInstanceOf[YAML], yaml) - assertEquals(value, sjm.read[Envelope[Body]](yaml)) - } - - test("Handles mutliple externalized types (bonus: with modifier)") { - val sjm = sj.withTypeValueModifier( - ClassNameHintModifier( - (hint: String) => "co.blocke.scalajack.yaml.misc." + hint, - (cname: String) => cname.split('.').last - ) - ) - val value: BigEnvelope[Body, Hobby, Int] = - BigEnvelope("DEF", FancyBody("BOO"), InsideHobby("stamps")) - val yaml = sjm.render[BigEnvelope[Body, Hobby, Int]](value) - assertEquals(Set.empty[String], - """hobby: - | desc: stamps - |body: - | message: BOO - |Hippo: InsideHobby - |Giraffe: FancyBody - |id: DEF - |""".stripMargin.linesIterator.toSet.diff(yaml.asInstanceOf[String].linesIterator.toSet) - ) - assertEquals(value, sjm.read[BigEnvelope[Body, Hobby, Int]](yaml)) - // Test out-of-order type hint--test skipping element logic in parser - val yaml2 = - """hobby: - | _hint: co.blocke.scalajack.yaml.misc.InsideHobby - | desc: stamps - |body: - | extra: - | - a - | - b - | - c: foom - | _hint: co.blocke.scalajack.yaml.misc.FancyBody - | message: BOO - |Hippo: Hobby - |Giraffe: Body - |id: DEF - |""".stripMargin.asInstanceOf[YAML] - assertEquals(value, sjm.read[BigEnvelope[Body, Hobby, Int]](yaml2)) - } - - test("Works with ParseOrElse") { - val yaml = - """Giraffe: co.blocke.scalajack.yaml.misc.FancyBody - |id: DEF - |body: - | bogus: BOO""".stripMargin.asInstanceOf[YAML] - val expected: Envelope[Body] = - Envelope("DEF", DefaultBody("Unknown body")) - assertEquals(expected, sj2.read[Envelope[Body]](yaml)) - } \ No newline at end of file diff --git a/core/src/test/scala/co.blocke.scalajack/yaml/misc/YamlFlavorSpec.scala b/core/src/test/scala/co.blocke.scalajack/yaml/misc/YamlFlavorSpec.scala deleted file mode 100644 index e71eff6b..00000000 --- a/core/src/test/scala/co.blocke.scalajack/yaml/misc/YamlFlavorSpec.scala +++ /dev/null @@ -1,89 +0,0 @@ -package co.blocke.scalajack -package yaml -package misc - -import TestUtil._ -import munit._ -import munit.internal.console - -object MyTypes { - type Phone = String -} -import MyTypes._ -import model._ - -import scala.collection.mutable -import co.blocke.scala_reflection.RType -import co.blocke.scala_reflection.info.AliasInfo - -//----------- -opaque type Phone >: Null = String - -case class PersonWithPhone(name: String, phone: Phone) - -// Override just Phone -object PhoneAdapter extends TypeAdapterFactory with TypeAdapter[Phone]: - def matches(concrete: RType): Boolean = - concrete match { - case a: AliasInfo if a.name == "Phone" => true - case _ => false - } - def makeTypeAdapter(concrete: RType)(implicit taCache: TypeAdapterCache): TypeAdapter[Phone] = this - val info = RType.of[Phone] - override def isStringish: Boolean = true - - def read(parser: Parser): Phone = - parser.expectString() match { - case null => null.asInstanceOf[Phone] - case s: String => s.replaceAll("-", "").asInstanceOf[Phone] - } - - def write[WIRE]( - t: Phone, - writer: Writer[WIRE], - out: mutable.Builder[WIRE, WIRE]): Unit = t match { - case null => writer.writeNull(out) - case _ => - writer.writeString( - "%s-%s-%s".format(t.toString.substring(0, 3), t.toString.substring(3, 6), t.toString.substring(6)), - out - ) - } - -class YamlFlavorSpec extends FunSuite: - - val sj = ScalaJack(YamlFlavor()) - - test("parse") { - describe( - "-----------------------------\n: YamlFlavor Tests (YAML) :\n-----------------------------", Console.BLUE - ) - val parser = sj.parse("Foo".asInstanceOf[YAML]) - assertEquals(parser.expectString(), "Foo") - } - - test("allowPermissivePrimitives") { - interceptMessage[ScalaJackError]("Not available for YAML encoding"){ - (sj.allowPermissivePrimitives()) - } - } - - test("withAdapters: Overrides type adapter for specific (given) type") { - val sj2 = sj.withAdapters(PhoneAdapter) - val inst = PersonWithPhone("Bartholomew", "5555555555".asInstanceOf[Phone]) - val yaml = sj2.render(inst) - assertEquals("""name: Bartholomew - |phone: 555-555-5555 - |""".stripMargin.asInstanceOf[YAML], yaml ) - assert(inst == sj2.read[PersonWithPhone](yaml)) - } - - test("withDefaultHint") { - val sj2 = sj.withDefaultHint("foom") - val yaml = - """foom: co.blocke.scalajack.yaml.misc.Person - |name: Fred - |age: 34 - |""".stripMargin.asInstanceOf[YAML] - assertEquals(sj2.render[Human](Person("Fred", 34)), yaml) - } \ No newline at end of file diff --git a/core/src/test/scala/co.blocke.scalajack/yaml/parameters/ClassParams.scala b/core/src/test/scala/co.blocke.scalajack/yaml/parameters/ClassParams.scala deleted file mode 100644 index bf4905f6..00000000 --- a/core/src/test/scala/co.blocke.scalajack/yaml/parameters/ClassParams.scala +++ /dev/null @@ -1,170 +0,0 @@ -package co.blocke.scalajack -package yaml -package parameters - -import TestUtil._ -import munit._ -import munit.internal.console - -class ClassParams extends FunSuite: - - val sj = ScalaJack(YamlFlavor()) - - test("Simple parameters - Foo[A](x:A) where A -> simple type") { - describe( - "----------------------------------------\n: Class Paramterization Tests (YAML) :\n----------------------------------------", Console.BLUE - ) - describe("Basic Parameterized Case Class") - val inst = Foo1(false, 19) - val yaml = sj.render(inst) - val comparison = """x: false - |b: 19 - |""".stripMargin.asInstanceOf[YAML] - assertEquals(comparison, yaml ) - assertEquals(inst, sj.read[Foo1[Boolean]](yaml)) - } - - test( - "Non-parameter case class as a parameter - Foo[A](x:A) where A -> Bar (case clas)" - ) { - val inst = Foo1(Bar1("Fred"), 19) - val yaml = sj.render(inst) - val comparison = """x: - | name: Fred - |b: 19 - |""".stripMargin.asInstanceOf[YAML] - assertEquals(comparison, yaml ) - assertEquals(inst, sj.read[Foo1[Bar1]](yaml)) - } - - test( - "Parameterized case class as parameter - Foo[A](x:A) where A -> Bar[Int]" - ) { - describe("Advanced Parameterized Case Class") - val inst = Foo1(Bar2(123L), 19) - val yaml = sj.render(inst) - val comparison = """x: - | id: 123 - |b: 19 - |""".stripMargin.asInstanceOf[YAML] - assertEquals(comparison, yaml ) - assertEquals(inst, sj.read[Foo1[Bar2[Long]]](yaml)) - } - - test("Value class as parameter - Foo[A](x:A) where A -> value class") { - val inst = Foo1(VC1("Wow"), 19) - val yaml = sj.render(inst) - val comparison = """x: Wow - |b: 19 - |""".stripMargin.asInstanceOf[YAML] - assertEquals(comparison, yaml ) - assertEquals(inst, sj.read[Foo1[VC1]](yaml)) - } - - test("Parameterized case class as a parameter - Foo[A](x:Bar[A])") { - val inst = Foo2(Bar2(123L), 19) - val yaml = sj.render(inst) - val comparison = """x: - | id: 123 - |b: 19 - |""".stripMargin.asInstanceOf[YAML] - assertEquals(comparison, yaml ) - assertEquals(inst, sj.read[Foo2[Long]](yaml)) - } - - test( - "Parameterized case class with parameterized another member - Foo[A](x:Bar[A], y:A)" - ) { - val inst = Foo3(Bar2(List(1, 2, 3)), List(4, 5, 6)) - val yaml = sj.render(inst) - val comparison = """x: - | id: [1, 2, 3] - |b: [4, 5, 6] - |""".stripMargin.asInstanceOf[YAML] - assertEquals(comparison, yaml ) - assertEquals(inst, sj.read[Foo3[List[Int]]](yaml)) - } - - test( - "Class with two parameters, one given one not - Foo[A](x:List[Bar[A, Boolean]])" - ) { - val inst = Foo4( - List( - Bar3(Map(4 -> "yes", 5 -> "no"), true), - Bar3(Map(8 -> "yellow", 9 -> "red"), false) - ), - Map(1 -> "wow", 2 -> "yup") - ) - val yaml = sj.render(inst) - val comparison = """x: - |- id: - | 4: yes - | 5: no - | isIt: true - |- id: - | 8: yellow - | 9: red - | isIt: false - |b: - | 1: wow - | 2: yup - |""".stripMargin.asInstanceOf[YAML] - assertEquals(comparison, yaml ) - assertEquals(inst, sj.read[Foo4[Map[Int, String]]](yaml)) - } - - test("Multiple parameters, in order - Foo[A,B](x:Bar[A,B], y:A)") { - describe("Very Advanced Parameterized Case Class") - val inst = Foo5( - List( - Bar3(Map(4 -> "yes", 5 -> "no"), true), - Bar3(Map(8 -> "yellow", 9 -> "red"), false) - ), - Map(1 -> "wow", 2 -> "yup") - ) - val yaml = sj.render(inst) - val comparison = """x: - |- id: - | 4: yes - | 5: no - | isIt: true - |- id: - | 8: yellow - | 9: red - | isIt: false - |b: - | 1: wow - | 2: yup - |""".stripMargin.asInstanceOf[YAML] - assertEquals(comparison, yaml ) - assertEquals(inst, sj.read[Foo5[Map[Int, String], Boolean]](yaml)) - } - - test("Multiple parameters, out of order - Foo[A,B,C,D](x:Bar[C,D,A], y:B)") { - val inst = Foo6(Bar4(5, 2.5, 'H'), "wow") - val yaml = sj.render(inst) - val comparison = """x: - | id: 5 - | thing1: 2.5 - | thing2: H - |y: wow - |""".stripMargin.asInstanceOf[YAML] - assertEquals(comparison, yaml ) - assertEquals(inst, sj.read[Foo6[Double, String, Int, Char]](yaml)) - } - - test( - "Nested multiple parameters, out of order - Foo[A,B,C,D](x:Bar[C,Blah[D,A]], y:B)" - ) { - val inst = Foo7(Bar5(5, Blah(2.5, 'H')), "wow") - val yaml = sj.render(inst) - val comparison = """x: - | id: 5 - | blah: - | t: 2.5 - | u: H - |y: wow - |""".stripMargin.asInstanceOf[YAML] - assertEquals(comparison, yaml ) - assertEquals(inst, sj.read[Foo7[Double, String, Char, Int]](yaml)) - } diff --git a/core/src/test/scala/co.blocke.scalajack/yaml/parameters/Model.scala b/core/src/test/scala/co.blocke.scalajack/yaml/parameters/Model.scala deleted file mode 100644 index 9f80fadc..00000000 --- a/core/src/test/scala/co.blocke.scalajack/yaml/parameters/Model.scala +++ /dev/null @@ -1,57 +0,0 @@ -package co.blocke.scalajack -package yaml -package parameters - -//--- Basic Parameterized Case Class -case class Foo1[A](x: A, b: Int) -case class Bar1(name: String) - -//--- Advanced Parameterized Case Class -case class Bar2[X](id: X) -case class VC1(s: String) extends AnyVal -case class Foo2[A](x: Bar2[A], b: Int) -case class Foo3[A](x: Bar2[A], b: A) -case class Bar3[X, Y](id: X, isIt: Y) -case class Foo4[A](x: List[Bar3[A, Boolean]], b: A) - -//--- Very Advanced Parameterized Case Class -case class Foo5[A, B](x: List[Bar3[A, B]], b: A) -case class Foo6[A, B, C, D](x: Bar4[C, D, A], y: B) -case class Bar4[X, Y, Z](id: X, thing1: Z, thing2: Y) -case class Blah[T, U](t: T, u: U) -case class Foo7[A, B, C, D](x: Bar5[C, D, A], y: B) -case class Bar5[X, Y, Z](id: Y, blah: Blah[Z, X]) - -//--- Basic Parameterized Trait -trait T1[X] { val x: X } -case class TFoo1[A](x: A, b: Int) extends T1[A] -trait T2 { val name: String } -case class TBar1(name: String) extends T2 - -//--- Advanced Parameterized Trait -trait T3[X] { val thing: X } -case class TBar2(thing: Boolean) extends T3[Boolean] -case class TBar3[T](thing: T) extends T3[T] -trait T4[X] { val x: TBar3[X] } -case class TFoo2[A](x: TBar3[A], b: A) extends T4[A] -trait T5[X, Y] { val thing1: X; val thing2: Y } -case class TBar4[T](thing1: T, thing2: String) extends T5[T, String] -trait T6[X] { val x: List[T5[X, String]] } -case class TFoo3[A](x: List[T5[A, String]]) extends T6[A] - -//--- Very Advanced Parameterized Trait -trait T7[X, Y] { val x: T5[X, Y]; val b: X } -case class TBar5[T, U](thing1: T, thing2: U) extends T5[T, U] -case class TFoo4[A, B](x: T5[A, B], b: A) extends T7[A, B] - -trait T8[W, X, Y, Z] { val x: T9[Y, Z, W]; val y: X } -trait T9[T, U, V] { val pi: T; val po: U; val pu: V } -case class TBar6[A, B, C](pi: A, po: B, pu: C) extends T9[A, B, C] -case class TFoo5[A, B, C, D](x: T9[C, D, A], y: B) extends T8[A, B, C, D] - -// Foo[A,B,C,D](x:Bar[C,Blah[D,A]], y:B) -trait T10[X, Y] { val x: X; val y: Y } -trait T11[W, Z] { val w: W; val z: Z } -case class TBlah1[A, B](w: A, z: B) extends T11[A, B] -case class TBar7[A, B](thing1: A, thing2: B) extends T5[A, B] -case class TFoo6[A, B, C, D](x: T11[C, T5[D, A]], y: B) extends T10[T11[C, T5[D, A]], B] \ No newline at end of file diff --git a/core/src/test/scala/co.blocke.scalajack/yaml/parameters/TraitParams.scala b/core/src/test/scala/co.blocke.scalajack/yaml/parameters/TraitParams.scala deleted file mode 100644 index afc429a0..00000000 --- a/core/src/test/scala/co.blocke.scalajack/yaml/parameters/TraitParams.scala +++ /dev/null @@ -1,161 +0,0 @@ -package co.blocke.scalajack -package yaml -package parameters - -import TestUtil._ -import munit._ -import munit.internal.console - -class TraitParams extends FunSuite: - - val sj = ScalaJack(YamlFlavor()) - - test("Simple parameters - Foo[A](x:A) where A -> simple type") { - describe( - "----------------------------------------\n: Trait Paramterization Tests (YAML) :\n----------------------------------------", Console.BLUE - ) - describe("Basic Parameterized Trait") - val inst: T1[Boolean] = TFoo1(false, 19) - val yaml = sj.render[T1[Boolean]](inst) - val comparison = """_hint: co.blocke.scalajack.yaml.parameters.TFoo1 - |x: false - |b: 19 - |""".stripMargin.asInstanceOf[YAML] - assertEquals(comparison, yaml ) - assertEquals(inst, sj.read[T1[Boolean]](yaml)) - } - - test( - "Non-parameter trait as a parameter - Foo[A](x:A) where A -> Bar (case clas)" - ) { - val inst: T1[T2] = TFoo1(TBar1("Fred"), 19) - val yaml = sj.render[T1[T2]](inst) - val comparison = """_hint: co.blocke.scalajack.yaml.parameters.TFoo1 - |x: - | _hint: co.blocke.scalajack.yaml.parameters.TBar1 - | name: Fred - |b: 19 - |""".stripMargin.asInstanceOf[YAML] - assertEquals(comparison, yaml ) - assert(inst == sj.read[TFoo1[TBar1]](yaml)) - } - - test("Parameterized trait as parameter - Foo[A](x:A) where A -> Bar[Int]") { - describe("Advanced Parameterized trait") - val inst: T1[TBar2] = TFoo1(TBar2(true), 19) - val yaml = sj.render(inst) - val comparison = """_hint: co.blocke.scalajack.yaml.parameters.TFoo1 - |x: - | thing: true - |b: 19 - |""".stripMargin.asInstanceOf[YAML] - assertEquals(comparison, yaml ) - assertEquals(inst, sj.read[T1[TBar2]](yaml)) - } - - test("Value class as parameter - Foo[A](x:A) where A -> value class") { - val inst: T1[VC1] = TFoo1(VC1("Wow"), 19) - val yaml = sj.render(inst) - val comparison = """_hint: co.blocke.scalajack.yaml.parameters.TFoo1 - |x: Wow - |b: 19 - |""".stripMargin.asInstanceOf[YAML] - assertEquals(comparison, yaml ) - assertEquals(inst, sj.read[T1[VC1]](yaml)) - } - - test("Parameterized trait as a parameter - Foo[A](x:Bar[A])") { - val inst: T1[T3[Boolean]] = TFoo1(TBar3(false), 19) - val yaml = sj.render(inst) - val comparison = """_hint: co.blocke.scalajack.yaml.parameters.TFoo1 - |x: - | _hint: co.blocke.scalajack.yaml.parameters.TBar3 - | thing: false - |b: 19 - |""".stripMargin.asInstanceOf[YAML] - assertEquals(comparison, yaml ) - assertEquals(inst, sj.read[T1[T3[Boolean]]](yaml)) - } - - test( - "Parameterized trait with parameterized another member - Foo[A](x:Bar[A], y:A)" - ) { - val inst: T4[Boolean] = TFoo2(TBar3(false), true) - val yaml = sj.render(inst) - val comparison = """_hint: co.blocke.scalajack.yaml.parameters.TFoo2 - |x: - | thing: false - |b: true - |""".stripMargin.asInstanceOf[YAML] - assertEquals(comparison, yaml ) - assertEquals(inst, sj.read[T4[Boolean]](yaml)) - } - - test( - "Trait with two parameters, one given one not - Foo[A](x:List[Bar[A, Boolean]])" - ) { - val inst: T6[Int] = TFoo3(List(TBar4(5, "five"), TBar4(6, "six"))) - val yaml = sj.render(inst) - val comparison = """_hint: co.blocke.scalajack.yaml.parameters.TFoo3 - |x: - |- _hint: co.blocke.scalajack.yaml.parameters.TBar4 - | thing1: 5 - | thing2: five - |- _hint: co.blocke.scalajack.yaml.parameters.TBar4 - | thing1: 6 - | thing2: six - |""".stripMargin.asInstanceOf[YAML] - assertEquals(comparison, yaml ) - assertEquals(inst, sj.read[T6[Int]](yaml)) - } - - test("Multiple parameters, in order - Foo[A,B](x:Bar[A,B], y:A)") { - describe("Very Advanced Parameterized Trait") - val inst: T7[Long, String] = TFoo4(TBar5(123L, "wow"), 456L) - val yaml = sj.render(inst) - val comparison = """_hint: co.blocke.scalajack.yaml.parameters.TFoo4 - |x: - | _hint: co.blocke.scalajack.yaml.parameters.TBar5 - | thing1: 123 - | thing2: wow - |b: 456 - |""".stripMargin.asInstanceOf[YAML] - assertEquals(comparison, yaml ) - assertEquals(inst, sj.read[T7[Long, String]](yaml)) - } - - test("Multiple parameters, out of order - Foo[A,B,C,D](x:Bar[C,D,A], y:B)") { - val inst: T8[Char, String, Int, Double] = - TFoo5(TBar6(5, 2.5, 'H'), "wow") - val yaml = sj.render(inst) - val comparison = """_hint: co.blocke.scalajack.yaml.parameters.TFoo5 - |x: - | _hint: co.blocke.scalajack.yaml.parameters.TBar6 - | pi: 5 - | po: 2.5 - | pu: H - |y: wow - |""".stripMargin.asInstanceOf[YAML] - assertEquals(comparison, yaml ) - assertEquals(inst, sj.read[T8[Char, String, Int, Double]](yaml)) - } - - test( - "Nested multiple parameters, out of order - Foo[A,B,C,D](x:Bar[C,Blah[D,A]], y:B)" - ) { - val inst: T10[T11[Int, T5[Double, Char]], String] = - TFoo6(TBlah1(5, TBar7(1.2, 'Z')), "wow") - val yaml = sj.render(inst) - val comparison = """_hint: co.blocke.scalajack.yaml.parameters.TFoo6 - |x: - | _hint: co.blocke.scalajack.yaml.parameters.TBlah1 - | w: 5 - | z: - | _hint: co.blocke.scalajack.yaml.parameters.TBar7 - | thing1: 1.2 - | thing2: Z - |y: wow - |""".stripMargin.asInstanceOf[YAML] - assertEquals(comparison, yaml ) - assertEquals(inst, sj.read[T10[T11[Int, T5[Double, Char]], String]](yaml)) - } \ No newline at end of file diff --git a/core/src/test/scala/co.blocke.scalajack/yaml/primitives.plain/Misc.scala b/core/src/test/scala/co.blocke.scalajack/yaml/primitives.plain/Misc.scala deleted file mode 100644 index afc144e1..00000000 --- a/core/src/test/scala/co.blocke.scalajack/yaml/primitives.plain/Misc.scala +++ /dev/null @@ -1,84 +0,0 @@ -package co.blocke.scalajack -package yaml -package primitives.plain - -import model.ClassNameHintModifier -import TestUtil._ -import munit._ -import munit.internal.console - -class Misc() extends FunSuite: - - val sj = ScalaJack(YamlFlavor()) - - test("Read/write null into object") { - describe( - "-------------------------------\n: Misc Tests (Plain - YAML) :\n-------------------------------", Console.BLUE - ) - assert(null == sj.read[PlayerMix]("null".asInstanceOf[YAML]) ) - assertEquals("""null - |""".stripMargin, sj.render[PlayerMix](null).asInstanceOf[String] ) - } - - test("Handles type members with modifier") { - val prependHintMod = ClassNameHintModifier( - (hint: String) => "co.blocke.scalajack.yaml.primitives.plain." + hint, - (cname: String) => cname.split('.').last - ) - val sj2 = sj.withTypeValueModifier(prependHintMod) - val yaml = - """flower: Flower - |rose: - | thing: 5 - | other: 6 - |""".stripMargin.asInstanceOf[YAML] - val inst = sj2.read[WrapTrait[TraitBase]](yaml) - assertEquals(inst.rose.isInstanceOf[Flower], true) - assertEquals(sj2.render(inst), yaml) - } - - test("Fails if no hint for type member") { - val yaml = - """rose: - | thing: 5 - | other: 6""".stripMargin.asInstanceOf[YAML] - interceptMessage[ScalaJackError]("Line 0: Did not find required type member(s): flower"){ - sj.read[WrapTrait[TraitBase]](yaml) - } - } - - test("Must accept missing default constructor values") { - val yaml = - """foobar: 3 - |quatro: 4 - |dontForget: 1""".stripMargin.asInstanceOf[YAML] - val inst = sj.read[InheritSimpleBase](yaml) - assertEquals(inst.one, "blather") - } - - test("Must accept missing optional constructor values") { - val yaml = """{}""".asInstanceOf[YAML] - val inst = sj.read[OptConst](yaml) - assertEquals(inst.a, None) - assertEquals(inst.b, Some(3)) - } - - test("Must ignore unneeded type members") { - val inst = new UnneededType[String]() - inst.a = 9 - assertEquals(sj.render(inst).asInstanceOf[String], """a: 9 - |""".stripMargin) - } - - test("Must require Java classes to have an empty constructor") { - val inst = new Unsupported("Foo") - interceptMessage[ScalaJackError]("""ScalaJack does not support Java classes with a non-empty constructor."""){ - sj.render(inst) - } - } - - test("Must handle MapName on Java setter") { - val yaml = "dos: 9".asInstanceOf[YAML] - val inst = sj.read[OnSetter](yaml) - assertEquals(inst.getTwo, 9) - } \ No newline at end of file diff --git a/core/src/test/scala/co.blocke.scalajack/yaml/primitives.plain/Model.scala b/core/src/test/scala/co.blocke.scalajack/yaml/primitives.plain/Model.scala deleted file mode 100644 index 10297591..00000000 --- a/core/src/test/scala/co.blocke.scalajack/yaml/primitives.plain/Model.scala +++ /dev/null @@ -1,114 +0,0 @@ -package co.blocke.scalajack -package yaml.primitives.plain - -import scala.util._ - -class InheritSimpleBase( - @DBKey(index = 50) @Change(name = "bogus") val one: String = "blather" -) { - // Public data member - @DBKey(index = 1) @Change(name = "foobar") var two: Int = 5 - @Optional var three: Boolean = true - - // Private var or val - val notOne: Int = 2 - - @Ignore var dontseeme: Int = 90 - - // Scala-style getter/setter - private var _four: Double = 0.1 - @DBKey(index = 2) @Optional def four: Double = _four - @Change(name = "quatro") def four_=(a: Double): Unit = _four = a - - private var _dontForget: Int = 9 - def dontForget: Int = _dontForget - def dontForget_=(a: Int): Unit = _dontForget = a - - private var _unused: Double = 0.1 - @Ignore def unused: Double = _unused - def unused_=(a: Double): Unit = _unused = a -} - -class InheritSimpleChild(val extra: String, @DBKey @Change(name = "uno") override val one: String) extends InheritSimpleBase(one) { - @DBKey(index = 99) var foo: Int = 39 - @Ignore var bogus: String = "" - - private var _nada: Double = 0.1 - def nada: Double = _nada - @Ignore def nada_=(a: Double): Unit = _nada = a -} - -// --- - -class ParamBase[T](val thing: T) { - var item: T = null.asInstanceOf[T] - - private var _cosa: T = null.asInstanceOf[T] - def cosa: T = _cosa - def cosa_=(a: T): Unit = _cosa = a -} - -class ParamChild[T](override val thing: T) extends ParamBase[T](thing) - -// --- - -trait TraitBase { - val thing: Int - val other: Int -} - -class Flower(val thing: Int, val other: Int) extends TraitBase - -class WrapTrait[T <: TraitBase]() { - type flower = T - var rose: T = null.asInstanceOf[T] - // IMPORTANT! rose must be of type T, not "flower". flower is must the label for the external type in JSON -} - -// --- - -class OptConst(val a: Option[Int]) { - var b: Option[Int] = Some(3) -} - -class UnneededType[T]() { - type item = T - - val m: T = null.asInstanceOf[item] - var a: Int = 5 -} - -//------------------------------------------------------ -case class VCDouble(vc: Double) extends AnyVal -class PlayerMix() { - def someConfusingThing() = true - var name: String = "" // public var member - var maybe: Option[Int] = Some(1) // optional member - - @Ignore var bogus: String = "" - - private var _age: VCDouble = VCDouble(0.0) - @Optional def age: VCDouble = _age // getter/setter member - def age_=(a: VCDouble): Unit = _age = a -} - -class BigPlayer() extends PlayerMix { - var more: Int = 0 -} - -// class NotAllVals(val a: Int, b: Int, val c: Int) - -class Embed() { - var stuff: List[String] = List.empty[String] - var num: Int = 0 -} -class Boom() { - var name: String = "" - var other: Try[Embed] = Success(null) -} - -class Cap() extends SJCapture { - var name: String = "" -} - -case class CaseCap(name: String) extends SJCapture \ No newline at end of file diff --git a/core/src/test/scala/co.blocke.scalajack/yaml/primitives.plain/TryAndCapture.scala b/core/src/test/scala/co.blocke.scalajack/yaml/primitives.plain/TryAndCapture.scala deleted file mode 100644 index 1d81fce0..00000000 --- a/core/src/test/scala/co.blocke.scalajack/yaml/primitives.plain/TryAndCapture.scala +++ /dev/null @@ -1,119 +0,0 @@ -package co.blocke.scalajack -package yaml -package primitives.plain - -import TestUtil._ -import munit._ -import munit.internal.console - -import scala.util._ - -class TryAndCapture() extends FunSuite: - - val sj = ScalaJack(YamlFlavor()) - - test("Try sucess") { - describe( - "------------------------------------------\n: Try and Capture Tests (Plain - YAML) :\n------------------------------------------", Console.BLUE - ) - describe("Try:") - val yaml = - """name: Greg - |other: - | stuff: [a, b, c] - | num: 2""".stripMargin.asInstanceOf[YAML] - val obj = sj.read[Boom](yaml) - assertEquals(0, yaml.asInstanceOf[String].split("\n").toSet.diff(sj.render(obj).asInstanceOf[String].split("\n").toSet).size ) - assertEquals(true, - obj.name == "Greg" && obj.other - .asInstanceOf[Success[Embed]] - .get - .num == 2 - ) - } - - test("Try failure") { - val yaml = - """name: Greg - |other: [1, 2, 3]""".stripMargin.asInstanceOf[YAML] - val obj = sj.read[Boom](yaml) - assertEquals("Line 1: Expected an Object here: +SEQ", obj.other.asInstanceOf[Failure[_]].exception.getMessage ) - assertEquals("""other: - |- 1 - |- 2 - |- 3 - |name: Greg - |""".stripMargin.split("\n").toSet.diff(sj.render(obj).asInstanceOf[String].split("\n").toSet).size, 0) - } - - test("Try failure 2") { - val yaml = - """name: Greg - |other: -12.45 - |num: 2""".stripMargin.asInstanceOf[YAML] - val obj = sj.read[Boom](yaml) - assertEquals("Line 1: Expected an Object here: =VAL :-12.45", obj.other.asInstanceOf[Failure[_]].exception.getMessage ) - assertEquals("""other: -12.45 - |name: Greg - |""".stripMargin.split("\n").toSet.diff(sj.render(obj).asInstanceOf[String].split("\n").toSet).size, 0) - } - - test("Plain-class capture can write semantically equivalent JSON") { - describe("Capture:") - val yaml = - """name: Greg - |foo: - |- 1 - |- 2 - |- t - |zing: - | dot: - | age: 25 - | food: Pizza - |blather: wow - |boo: -29384.34 - |maybe: false - |""".stripMargin.asInstanceOf[YAML] - val h = sj.read[Cap](yaml) - assertEquals(h.name, "Greg") - val yaml2 = sj.render(h) - assertEquals(yaml.asInstanceOf[String].split("\n").toSet.diff(yaml2.asInstanceOf[String].split("\n").toSet).size, 0) - } - - test("Case class capture can write semantically equivalent JSON") { - val yaml = - """name: Greg - |foo: [1, 2, t] - |zing: - | _hint: a.b.com.Hey - | dot: - | age: 25 - | food: Pizza - | blather: wow - | boo: -29384.34 - | maybe: false - |""".stripMargin.asInstanceOf[YAML] - val h = sj.read[CaseCap](yaml) - assertEquals(h.name, "Greg") - val yaml2 = sj.render(h) - assertEquals(yaml.asInstanceOf[String].split("\n").toSet.diff(yaml2.asInstanceOf[String].split("\n").toSet).size, 0) - } - - test("Java class capture can write semantically equivalent JSON") { - val yaml = - """name: Greg - |foo: [1, 2, t] - |zing: - | _hint: a.b.com.Hey - | dot: - | age: 25 - | food: Pizza - | blather: wow - | boo: -29384.34 - | maybe: false - |""".stripMargin.asInstanceOf[YAML] - val h = sj.read[JavaCap](yaml) - assertEquals(h.getName, "Greg") - val yaml2 = sj.render(h) - assertEquals(yaml.asInstanceOf[String].split("\n").toSet.diff(yaml2.asInstanceOf[String].split("\n").toSet).size, 0) - } \ No newline at end of file diff --git a/core/src/test/scala/co.blocke.scalajack/yaml/primitives.plain/ValueClassPrim.scala b/core/src/test/scala/co.blocke.scalajack/yaml/primitives.plain/ValueClassPrim.scala deleted file mode 100644 index 171f9860..00000000 --- a/core/src/test/scala/co.blocke.scalajack/yaml/primitives.plain/ValueClassPrim.scala +++ /dev/null @@ -1,34 +0,0 @@ -package co.blocke.scalajack -package yaml -package primitives.plain - -import TestUtil._ -import munit._ -import munit.internal.console - -class ValueClassPrim() extends FunSuite: - - val sj = ScalaJack(YamlFlavor()) - - test("Value class of Double") { - describe( - "-----------------------------------------------\n: ValueClass DelimSpec Tests (Plain - YAML) :\n-----------------------------------------------", Console.BLUE - ) - val p1 = new PlayerMix() - p1.name = "Mike" - p1.age = VCDouble(BigDecimal("1.23").toDouble) - val yaml = sj.render(p1) - val comparison = """age: 1.23 - |maybe: 1 - |name: Mike - |""".stripMargin - assertEquals(yaml.asInstanceOf[String].split("\n").toSet.diff(comparison.split("\n").toSet).size, 0) - assertEquals(p1.name, { - val r = sj.read[PlayerMix](yaml) - r.name - }) - assertEquals(p1.age, { - val r = sj.read[PlayerMix](yaml) - r.age - }) - } diff --git a/core/src/test/scala/co.blocke.scalajack/yaml/primitives/AnyPrim.scala b/core/src/test/scala/co.blocke.scalajack/yaml/primitives/AnyPrim.scala deleted file mode 100644 index 901842d9..00000000 --- a/core/src/test/scala/co.blocke.scalajack/yaml/primitives/AnyPrim.scala +++ /dev/null @@ -1,163 +0,0 @@ -package co.blocke.scalajack -package yaml -package primitives - -import co.blocke.scalajack.model.JackFlavor -import TestUtil._ -import munit._ -import munit.internal.console - -class AnyPrim() extends FunSuite: - - val sj: JackFlavor[YAML] = ScalaJack(YamlFlavor()) - - test("null works") { - describe( - "--------------------------------\n: Any Primitive Tests (YAML) :\n--------------------------------", Console.BLUE - ) - describe("+++ Positive Tests +++") - val shell = AnyShell(null) - val yaml = sj.render(shell) - assertEquals("""a: null""", yaml.asInstanceOf[String].trim ) - assert(null == sj.read[AnyShell](yaml).a) - } - - test("BigDecimal works") { - val payload = BigDecimal("12345678901234567890.12345678901234567890") - val yaml = sj.render(AnyShell(payload)) - assertEquals("""a: 12345678901234567890.12345678901234567890""", yaml.asInstanceOf[String].trim) - assertEquals(true, { - val parsed = sj.read[AnyShell](yaml).a - (parsed == payload) && (parsed.getClass == payload.getClass) - }) - } - - test("BigInt works") { - val payload = BigInt("12345678901234567890") - val yaml = sj.render(AnyShell(payload)) - assertEquals("""a: 12345678901234567890""", yaml.asInstanceOf[String].trim ) - assertEquals(true, { - val parsed = sj.read[AnyShell](yaml).a - (parsed == payload) && parsed.isInstanceOf[BigInt] - }) - } - - test("Boolean works") { - val payload = true - val yaml = sj.render(AnyShell(payload)) - assertEquals("""a: true""", yaml.asInstanceOf[String].trim ) - assertEquals(true, { - val parsed = sj.read[AnyShell](yaml).a - (parsed == payload) && parsed - .isInstanceOf[Boolean] // boolean becomes Boolean - }) - } - - test("Byte works") { - val payload: Byte = 16 - val yaml = sj.render(AnyShell(payload)) - assertEquals("""a: 16""", yaml.asInstanceOf[String].trim ) - assertEquals(true, { - val parsed = sj.read[AnyShell](yaml).a - (parsed == payload) && parsed - .isInstanceOf[Integer] // byte becomes Integer - }) - } - - test("Char works") { - val payload: Char = 'Z' - val yaml = sj.render(AnyShell(payload)) - assertEquals("""a: Z""", yaml.asInstanceOf[String].trim ) - assertEquals(true, { - val parsed = sj.read[AnyShell](yaml).a - (parsed == payload.toString) && parsed - .isInstanceOf[String] // Char becomes String - }) - } - - test("Double works") { - val payload: Double = 1234.5678 - val yaml = sj.render(AnyShell(payload)) - assertEquals("""a: 1234.5678""", yaml.asInstanceOf[String].trim ) - assertEquals(true, { - val parsed = sj.read[AnyShell](yaml).a - (parsed == payload) && parsed - .isInstanceOf[Double] // double becomes Double - }) - } - - test("Enumeration works") { - val payload: Size.Value = Size.Small - val yaml = sj.render(AnyShell(payload)) - assertEquals("""a: Small""", yaml.asInstanceOf[String].trim ) - assertEquals(true, { - val parsed = sj.read[AnyShell](yaml).a - (parsed == payload.toString) && parsed - .isInstanceOf[String] // enum value becomes String - }) - } - - test("Float works") { - val payload: Float = 1234.5678F - val yaml = sj.render(AnyShell(payload)) - assertEquals("""a: 1234.5677""", yaml.asInstanceOf[String].trim ) - assertEquals(true, { - val parsed = sj.read[AnyShell](yaml).a - (parsed.toString == payload.toString) && parsed - .isInstanceOf[Double] // float becomes Double - }) - } - - test("Int works") { - val payload: Int = 1234567 - val yaml = sj.render(AnyShell(payload)) - assertEquals("""a: 1234567""", yaml.asInstanceOf[String].trim ) - assertEquals(true, { - val parsed = sj.read[AnyShell](yaml).a - (parsed == payload) && parsed.isInstanceOf[Int] // int becomes Int - }) - } - - test("Long works") { - val payload: Long = 123456789012345L - val yaml = sj.render(AnyShell(payload)) - assertEquals("""a: 123456789012345""", yaml.asInstanceOf[String].trim ) - assertEquals(true, { - val parsed = sj.read[AnyShell](yaml).a - (parsed == payload) && parsed - .isInstanceOf[java.lang.Long] // long becomes Long (Note this could become Integer if smaller number parsed) - }) - val payload2: Long = 123L - val yaml2 = sj.render(AnyShell(payload2)) - assertEquals("""a: 123""", yaml2.asInstanceOf[String].trim ) - assertEquals(true, { - val parsed = sj.read[AnyShell](yaml2).a - (parsed == payload2) && parsed - .isInstanceOf[Int] // long becomes Byte due to small number size - }) - } - - test("Short works") { - val payload: Short = 16234 - val yaml = sj.render(AnyShell(payload)) - assertEquals("""a: 16234""", yaml.asInstanceOf[String].trim ) - assertEquals(true, { - val parsed = sj.read[AnyShell](yaml).a - (parsed == payload) && parsed.isInstanceOf[Int] // short becomes Int - }) - } - - test("String works") { - val payload = "something" - val yaml = sj.render(AnyShell(payload)) - assertEquals("""a: something""", yaml.asInstanceOf[String].trim ) - assertEquals(true, { - val parsed = sj.read[AnyShell](yaml).a - (parsed == payload) && (parsed.getClass == payload.getClass) - }) - } - - // describe("--- Negative Tests ---") { - // No real negative tests yet... can't think of how to break Any primitives, given well-formed JSON input. - // It may not infer what you want/expect, but it should always infer something. - // } diff --git a/core/src/test/scala/co.blocke.scalajack/yaml/primitives/JavaPrim.scala b/core/src/test/scala/co.blocke.scalajack/yaml/primitives/JavaPrim.scala deleted file mode 100644 index 10d40b01..00000000 --- a/core/src/test/scala/co.blocke.scalajack/yaml/primitives/JavaPrim.scala +++ /dev/null @@ -1,400 +0,0 @@ -package co.blocke.scalajack -package yaml -package primitives - -import TestUtil._ -import munit._ -import munit.internal.console -import java.lang.{Boolean => JBoolean, Byte => JByte, Double => JDouble, Float => JFloat, Integer => JInt, Long => JLong, Short => JShort} -import java.math.{BigDecimal => JBigDecimal, BigInteger => JBigInteger} - -class JavaPrim() extends FunSuite: - - val sj = ScalaJack(YamlFlavor()) - - test("BigDecimal must work") { - describe( - "---------------------------------\n: Java Primitive Tests (YAML) :\n---------------------------------", Console.BLUE - ) - describe("+++ Positive Tests +++") - val inst = SampleJBigDecimal( - JBigDecimal.ZERO, - JBigDecimal.ONE, - JBigDecimal.TEN, - new JBigDecimal( - "0.1499999999999999944488848768742172978818416595458984375" - ), - JBigDecimal.ZERO - ) - val yaml = sj.render(inst) - val comparison = """bd5: !!float '0' - |bd1: !!float '0' - |bd3: !!float '10' - |bd4: 0.1499999999999999944488848768742172978818416595458984375 - |bd2: !!float '1'""".stripMargin - assertEquals(Set.empty[String], yaml.asInstanceOf[String].split("\n").toSet.diff(comparison.split("\n").toSet) ) - assertEquals(inst, sj.read[SampleJBigDecimal](yaml)) - } - - test("BigInteger must work") { - val inst = SampleJBigInteger( - JBigInteger.ZERO, - JBigInteger.ONE, - JBigInteger.TEN, - new JBigInteger("-90182736451928374653345"), - new JBigInteger("90182736451928374653345"), - new JBigInteger("0"), - JBigInteger.ZERO - ) - val yaml = sj.render(inst) - val comparison = """bi6: 0 - |bi2: 1 - |bi5: 90182736451928374653345 - |bi1: 0 - |bi7: 0 - |bi3: 10 - |bi4: -90182736451928374653345""".stripMargin - assertEquals(Set.empty[String], yaml.asInstanceOf[String].split("\n").toSet.diff(comparison.split("\n").toSet) ) - assertEquals(inst, sj.read[SampleJBigInteger](yaml)) - } - - test("Boolean must work") { - val inst = - SampleJBoolean(JBoolean.TRUE, JBoolean.FALSE, true, false, null) - val yaml = sj.render(inst) - val comparison = """bool5: null - |bool3: true - |bool4: false - |bool2: false - |bool1: true""".stripMargin - assertEquals(Set.empty[String], yaml.asInstanceOf[String].split("\n").toSet.diff(comparison.split("\n").toSet) ) - assertEquals(inst, sj.read[SampleJBoolean](yaml)) - } - - test("Byte must work") { - val inst = SampleJByte( - JByte.MAX_VALUE, - JByte.MIN_VALUE, - 0.asInstanceOf[Byte], - 64.asInstanceOf[Byte], - null - ) - val yaml = sj.render(inst) - val comparison = """b5: null - |b1: 127 - |b3: 0 - |b2: -128 - |b4: 64""".stripMargin - assertEquals(Set.empty[String], yaml.asInstanceOf[String].split("\n").toSet.diff(comparison.split("\n").toSet)) - assertEquals(inst, sj.read[SampleJByte](yaml)) - } - - test("Char must work") { - val inst = SampleJChar('Z', '\u20A0', null) - val yaml = sj.render(inst) - val comparison = """c1: Z - |c2: ₠ - |c3: null""".stripMargin - assertEquals(Set.empty[String], yaml.asInstanceOf[String].split("\n").toSet.diff(comparison.split("\n").toSet)) - assertEquals(inst, sj.read[SampleJChar](yaml)) - } - - test("Double must work") { - val inst = SampleJDouble( - JDouble.MAX_VALUE, - JDouble.MIN_VALUE, - 0.0, - -123.4567, - null - ) - val yaml = sj.render(inst) - val comparison = - """d5: null - |d3: 0.0 - |d4: -123.4567 - |d2: !!float '4.9E-324' - |d1: !!float '1.7976931348623157E308'""".stripMargin - assertEquals(Set.empty[String], yaml.asInstanceOf[String].split("\n").toSet.diff(comparison.split("\n").toSet)) - assertEquals(inst, sj.read[SampleJDouble](yaml)) - } - - test("Float must work") { - val inst = SampleJFloat( - JFloat.MAX_VALUE, - JFloat.MIN_VALUE, - 0.0F, - -123.4567F, - null - ) - val yaml = sj.render(inst) - val comparison = """f4: -123.4567 - |f5: null - |f3: 0.0 - |f2: !!float '1.4E-45' - |f1: !!float '3.4028235E38'""".stripMargin - assertEquals(Set.empty[String], yaml.asInstanceOf[String].split("\n").toSet.diff(comparison.split("\n").toSet) ) - assertEquals(inst, sj.read[SampleJFloat](yaml)) - } - - test("Int must work") { - val inst = SampleJInt(JInt.MAX_VALUE, JInt.MIN_VALUE, 0, 123, null) - val yaml = sj.render(inst) - val comparison = """i2: -2147483648 - |i4: 123 - |i3: 0 - |i1: 2147483647 - |i5: null""".stripMargin - assertEquals(Set.empty[String], yaml.asInstanceOf[String].split("\n").toSet.diff(comparison.split("\n").toSet) ) - assertEquals(inst, sj.read[SampleJInt](yaml)) - } - - test("Long must work") { - val inst = SampleJLong(JLong.MAX_VALUE, JLong.MIN_VALUE, 0L, 123L, null) - val yaml = sj.render(inst) - val comparison = """l2: -9223372036854775808 - |l1: 9223372036854775807 - |l4: 123 - |l3: 0 - |l5: null""".stripMargin - assertEquals(Set.empty[String], yaml.asInstanceOf[String].split("\n").toSet.diff(comparison.split("\n").toSet) ) - assertEquals(inst, sj.read[SampleJLong](yaml)) - } - - test("Number must work") { - val inst = SampleJNumber( - JByte.valueOf("-128"), - JByte.valueOf("127"), - JShort.valueOf("-32768"), - JShort.valueOf("32767"), - JInt.valueOf("-2147483648"), - JInt.valueOf("2147483647"), - JLong.valueOf("-9223372036854775808"), - JLong.valueOf("9223372036854755807"), - null, //new JBigInteger("9923372036854755810"), - JByte.valueOf("0"), - JFloat.valueOf("3.4e-038"), - JFloat.valueOf("3.4e+038"), - JDouble.valueOf("1.7e-308"), - JDouble.valueOf("1.7e+308"), - null, //new JBigDecimal("1.8e+308"), - JFloat.valueOf("0.0"), - null - ) - val yaml = sj.render(inst) - val comparison = """n10: 0 - |n4: 32767 - |n8: 9223372036854755807 - |n16: 0.0 - |n1: -128 - |n3: -32768 - |n15: null - |n14: !!float '1.7E308' - |n12: !!float '3.4E38' - |n13: !!float '1.7E-308' - |n6: 2147483647 - |n5: -2147483648 - |n9: null - |n7: -9223372036854775808 - |n11: !!float '3.4E-38' - |n2: 127 - |n17: null""".stripMargin - assertEquals(Set.empty[String], yaml.asInstanceOf[String].split("\n").toSet.diff(comparison.split("\n").toSet) ) - assertEquals(inst, sj.read[SampleJNumber](yaml)) - } - - test("Short must work") { - val inst = SampleJShort( - JShort.MAX_VALUE, - JShort.MIN_VALUE, - 0.asInstanceOf[Short], - 123.asInstanceOf[Short], - null - ) - val yaml = sj.render(inst) - val comparison = """s4: 123 - |s1: 32767 - |s5: null - |s2: -32768 - |s3: 0""".stripMargin - assertEquals(Set.empty[String], yaml.asInstanceOf[String].split("\n").toSet.diff(comparison.split("\n").toSet) ) - assertEquals(inst, sj.read[SampleJShort](yaml)) - } - - test("BigDecimal must break") { - describe("--- Negative Tests ---") - val yaml = - """bd1: 0 - |bd2: 1 - |bd3: 10 - |bd4: [a,b,c] - |bd5: null""".stripMargin.asInstanceOf[YAML] - interceptMessage[ScalaJackError]("Line 3: Expected a Number value here: +SEQ"){ - sj.read[SampleBigDecimal](yaml) - } - } - - test("BigInteger must break") { - val yaml = - """bi1: [a,b] - |bi2: 1 - |bi3: 10 - |bi4: -90182736451928374653345 - |bi5: 90182736451928374653345 - |bi6: 0 - |bi7: null""".stripMargin.asInstanceOf[YAML] - interceptMessage[ScalaJackError]("Line 0: Expected a Number value here: +SEQ"){ - sj.read[SampleJBigInteger](yaml) - } - } - - test("Boolean must break") { - val yaml = - """bool1: true - |bool2: false - |bool3: true - |bool4: [a,b] - |bool5: null""".stripMargin.asInstanceOf[YAML] - interceptMessage[ScalaJackError]("Line 3: Expected a Boolean value here: +SEQ"){ - sj.read[SampleJBoolean](yaml) - } - } - - test("Byte must break") { - val yaml = - """b1: 127 - |b2: -128 - |b3: false - |b4: 64 - |b5: null""".stripMargin.asInstanceOf[YAML] - interceptMessage[ScalaJackError]("Line 2: Expected a Number value here: =VAL :false"){ - sj.read[SampleJByte](yaml) - } - } - - test("Char must break") { - val yaml = - """c1: "Z" - |c2: [a,b] - |c3: null""".stripMargin.asInstanceOf[YAML] - interceptMessage[ScalaJackError]("Line 1: Expected a String here: +SEQ"){ - sj.read[SampleJChar](yaml) - } - val yaml2 = - """c1: "Z" - |c2: - |c3: null""".stripMargin.asInstanceOf[YAML] - interceptMessage[ScalaJackError]("Line 1: Tried to read a Character but empty string found"){ - sj.read[SampleJChar](yaml2) - } - } - - test("Double must break") { - val yaml = - """d1: 1.7976931348623157E308 - |d2: 4.9E-324 - |d3: fred - |d4: -123.4567 - |d5: null""".stripMargin.asInstanceOf[YAML] - interceptMessage[ScalaJackError]("Line 2: Expected a Number value here: =VAL :fred"){ - sj.read[SampleJDouble](yaml) - } - } - - test("Float must break") { - val yaml = - """f1: 3.4028235E38 - |f2: fred - |f3: 0.0 - |f4: -123.4567 - |f5: null""".stripMargin.asInstanceOf[YAML] - interceptMessage[ScalaJackError]("Line 1: Expected a Number value here: =VAL :fred"){ - sj.read[SampleJFloat](yaml) - } - } - - test("Int must break") { - val yaml = - """i1: 2147483647 - |i2: -2147483648 - |i3: false - |i4: 123 - |i5: null""".stripMargin.asInstanceOf[YAML] - interceptMessage[ScalaJackError]("Line 2: Expected a Number value here: =VAL :false"){ - sj.read[SampleJInt](yaml) - } - val yaml2 = - """i1: 2147483647 - |i2: -2147483648 - |i3: 0.3 - |i4: 123 - |i5: null""".stripMargin.asInstanceOf[YAML] - interceptMessage[java.lang.NumberFormatException]("For input string: \"0.3\""){ - sj.read[SampleJInt](yaml2) - } - } - - test("Long must break") { - val yaml = - """l1: 9223372036854775807 - |l2: -9223372036854775808 - |l3: [a,b] - |l4: 123 - |l5: null""".stripMargin.asInstanceOf[YAML] - interceptMessage[ScalaJackError]("Line 2: Expected a Number value here: +SEQ"){ - sj.read[SampleJLong](yaml) - } - val yaml2 = - """l1: 9223372036854775807 - |l2: -9223372036854775808 - |l3: 0.3 - |l4: 123 - |l5: null""".stripMargin.asInstanceOf[YAML] - interceptMessage[java.lang.NumberFormatException]("For input string: \"0.3\""){ - sj.read[SampleJLong](yaml2) - } - } - - test("Number must break") { - val yaml = - """n1: -128 - |n2: 127 - |n3: [a,b] - |n4: 32767 - |n5: -2147483648 - |n6: 2147483647 - |n7: -9223372036854775808 - |n8: 9223372036854755807 - |n9: 9923372036854755810 - |n10: 0 - |n11: 3.4E-38 - |n12: 3.4E38 - |n13: 1.7E-308 - |n14: 1.7E308 - |n15: 1.8E+308 - |n16: 0.0 - |n17: null""".stripMargin.asInstanceOf[YAML] - interceptMessage[ScalaJackError]("Line 2: Expected a Number value here: +SEQ"){ - sj.read[SampleJNumber](yaml) - } - } - - test("Short must break") { - val yaml = - """s1: false - |s2: -32768 - |s3: 0 - |s4: 123 - |s5: null""".stripMargin.asInstanceOf[YAML] - interceptMessage[ScalaJackError]("Line 0: Expected a Number value here: =VAL :false"){ - sj.read[SampleJShort](yaml) - } - val yaml2 = - """s1: 2.3 - |s2: -32768 - |s3: 0 - |s4: 123 - |s5: null""".stripMargin.asInstanceOf[YAML] - interceptMessage[java.lang.NumberFormatException]("For input string: \"2.3\""){ - sj.read[SampleJShort](yaml2) - } - } diff --git a/core/src/test/scala/co.blocke.scalajack/yaml/primitives/ScalaPrim.scala b/core/src/test/scala/co.blocke.scalajack/yaml/primitives/ScalaPrim.scala deleted file mode 100644 index 4e2e83da..00000000 --- a/core/src/test/scala/co.blocke.scalajack/yaml/primitives/ScalaPrim.scala +++ /dev/null @@ -1,424 +0,0 @@ -package co.blocke.scalajack -package yaml -package primitives - -import scala.math.BigDecimal -import java.util.UUID -import TestUtil._ -import munit._ -import munit.internal.console - -class ScalaPrim() extends FunSuite: - - val sj = ScalaJack(YamlFlavor()) - - test("BigDecimal must work") { - describe( - "----------------------------------\n: Scala Primitive Tests (YAML) :\n----------------------------------", Console.BLUE - ) - describe("+++ Positive Tests +++") - val inst = SampleBigDecimal( - BigDecimal(123L), - BigDecimal(1.23), - BigDecimal(0), - BigDecimal("123.456"), - BigDecimal( - "0.1499999999999999944488848768742172978818416595458984375" - ), - null - ) - val yaml = sj.render(inst) - val comparison = """bd1: !!float '123' - |bd2: 1.23 - |bd3: !!float '0' - |bd4: 123.456 - |bd5: 0.1499999999999999944488848768742172978818416595458984375 - |bd6: null""".stripMargin - assertEquals(Set.empty[String], yaml.asInstanceOf[String].split("\n").toSet.diff(comparison.split("\n").toSet)) - assertEquals(inst, sj.read[SampleBigDecimal](yaml)) - } - - test("BigInt must work") { - val inst = SampleBigInt( - BigInt("-90182736451928374653345"), - BigInt("90182736451928374653345"), - BigInt(0), - null - ) - val yaml = sj.render(inst) - val comparison = - """bi1: -90182736451928374653345 - |bi2: 90182736451928374653345 - |bi3: 0 - |bi4: null""".stripMargin - assertEquals(Set.empty[String], yaml.asInstanceOf[String].split("\n").toSet.diff(comparison.split("\n").toSet) ) - assertEquals(inst, sj.read[SampleBigInt](yaml)) - } - - test("Binary must work") { - val inst = SampleBinary( - null, - hexStringToByteArray("e04fd020ea3a6910a2d808002b30309d") - ) - val yaml = sj.render(inst) - val comparison = - """b1: null - |b2: 4E/QIOo6aRCi2AgAKzAwnQ==""".stripMargin - assertEquals(Set.empty[String], yaml.asInstanceOf[String].split("\n").toSet.diff(comparison.split("\n").toSet) ) - val inst2 = sj.read[SampleBinary](yaml) - assert(null == inst2.b1 ) - assertEquals(true, inst.b2.toList == inst2.b2.toList) - } - - test("Boolean must work (not nullable)") { - val inst = SampleBoolean(true, false) - val yaml = sj.render(inst) - val comparison = - """bool1: true - |bool2: false""".stripMargin - assertEquals(Set.empty[String], yaml.asInstanceOf[String].split("\n").toSet.diff(comparison.split("\n").toSet) ) - assertEquals(inst, sj.read[SampleBoolean](yaml)) - } - - test("Byte must work (not nullable)") { - val inst = SampleByte(Byte.MaxValue, Byte.MinValue, 0, 64) - val yaml = sj.render(inst) - val comparison = """b1: 127 - |b2: -128 - |b3: 0 - |b4: 64""".stripMargin - assertEquals(Set.empty[String], yaml.asInstanceOf[String].split("\n").toSet.diff(comparison.split("\n").toSet) ) - assertEquals(inst, sj.read[SampleByte](yaml)) - } - - test("Char must work (not nullable)") { - val inst = SampleChar(Char.MaxValue, 'Z', '\u20A0') - val yaml = sj.render(inst) - val insert = "\\uffff" - val comparison = s"""c1: "$insert" - |c2: Z - |c3: ₠""".stripMargin - assertEquals(Set.empty[String], yaml.asInstanceOf[String].split("\n").toSet.diff(comparison.split("\n").toSet) ) - assertEquals(inst,sj.read[SampleChar](yaml)) - } - - test("Double must work (not nullable)") { - val inst = - SampleDouble(Double.MaxValue, Double.MinValue, 0.0, -123.4567) - val yaml = sj.render(inst) - val comparison = """d1: !!float '1.7976931348623157E308' - |d2: !!float '-1.7976931348623157E308' - |d3: 0.0 - |d4: -123.4567""".stripMargin - assertEquals(Set.empty[String], yaml.asInstanceOf[String].split("\n").toSet.diff(comparison.split("\n").toSet) ) - assertEquals(inst, sj.read[SampleDouble](yaml)) - } - - test("Enumeration must work (not nullable)") { - val inst = - SampleEnum(Size.Small, Size.Medium, Size.Large, null, Size.Medium) - val yaml = sj.render(inst) - val comparison = """e1: Small - |e2: Medium - |e3: Large - |e4: null - |e5: Medium""".stripMargin - assertEquals(Set.empty[String], yaml.asInstanceOf[String].split("\n").toSet.diff(comparison.split("\n").toSet) ) - // mutate e5 into an ordinal... - val yaml2 = yaml.asInstanceOf[String].replace("e5: Medium", "e5: 1").asInstanceOf[YAML] - assertEquals(inst, sj.read[SampleEnum](yaml2)) - } - - test("Enumerations as Ints must work") { - val sj2 = sj.enumsAsInts() - val inst = - SampleEnum(Size.Small, Size.Medium, Size.Large, null, Size.Medium) - val yaml = sj2.render(inst) - val comparison = """e1: 0 - |e2: 1 - |e3: 2 - |e4: null - |e5: 1""".stripMargin - assertEquals(Set.empty[String], yaml.asInstanceOf[String].split("\n").toSet.diff(comparison.split("\n").toSet) ) - assertEquals(inst, sj2.read[SampleEnum](yaml)) - } - - test("Float must work") { - val inst = SampleFloat(Float.MaxValue, Float.MinValue, 0.0F, -123.4567F) - val yaml = sj.render(inst) - val comparison = """f1: !!float '3.4028235E38' - |f2: !!float '-3.4028235E38' - |f3: 0.0 - |f4: -123.4567""".stripMargin - assertEquals(Set.empty[String], yaml.asInstanceOf[String].split("\n").toSet.diff(comparison.split("\n").toSet) ) - assertEquals(inst, sj.read[SampleFloat](yaml)) - } - - test("Int must work (not nullable)") { - val inst = SampleInt(Int.MaxValue, Int.MinValue, 0, 123) - val yaml = sj.render(inst) - val comparison = """i1: 2147483647 - |i2: -2147483648 - |i3: 0 - |i4: 123""".stripMargin - assertEquals(Set.empty[String], yaml.asInstanceOf[String].split("\n").toSet.diff(comparison.split("\n").toSet) ) - assertEquals(inst, sj.read[SampleInt](yaml)) - } - - test("Long must work (not nullable)") { - val inst = SampleLong(Long.MaxValue, Long.MinValue, 0L, 123L) - val yaml = sj.render(inst) - val comparison = """l1: 9223372036854775807 - |l2: -9223372036854775808 - |l3: 0 - |l4: 123""".stripMargin - assertEquals(Set.empty[String], yaml.asInstanceOf[String].split("\n").toSet.diff(comparison.split("\n").toSet) ) - assertEquals(inst, sj.read[SampleLong](yaml)) - } - - test("Short must work (not nullable)") { - val inst = SampleShort(Short.MaxValue, Short.MinValue, 0, 123) - val yaml = sj.render(inst) - val comparison = """s1: 32767 - |s2: -32768 - |s3: 0 - |s4: 123""".stripMargin - assertEquals(Set.empty[String], yaml.asInstanceOf[String].split("\n").toSet.diff(comparison.split("\n").toSet) ) - assertEquals(inst, sj.read[SampleShort](yaml)) - } - - test("String must work") { - val inst = SampleString("something\b\n\f\r\t☆", "", null) - val yaml = sj.render(inst) - val comparison = """s1: "something\b\n\f\r\t☆" - |s2: '' - |s3: null""".stripMargin - assertEquals(Set.empty[String], yaml.asInstanceOf[String].split("\n").toSet.diff(comparison.split("\n").toSet) ) - assertEquals(inst, sj.read[SampleString](yaml)) - } - - test("UUID must work") { - val inst = SampleUUID( - null, - UUID.fromString("580afe0d-81c0-458f-9e09-4486c7af0fe9") - ) - val yaml = sj.render(inst) - val comparison = """u1: null - |u2: 580afe0d-81c0-458f-9e09-4486c7af0fe9""".stripMargin - assertEquals(Set.empty[String], yaml.asInstanceOf[String].split("\n").toSet.diff(comparison.split("\n").toSet) ) - assertEquals(inst, sj.read[SampleUUID](yaml)) - } - - test("BigDecimal must break") { - describe("--- Negative Tests ---") - val yaml = - """bd1: 123 - |bd2: 1.23 - |bd3: 0 - |bd4: 123.456 - |bd5: [1,2,3] - |bd6: null - |""".stripMargin.asInstanceOf[YAML] - interceptMessage[co.blocke.scalajack.ScalaJackError]("Line 4: Expected a Number value here: +SEQ"){ - sj.read[SampleBigDecimal](yaml) - } - } - - test("BigInt must break") { - val yaml = - """bi1: [1,2,3] - |bi2: 90182736451928374653345 - |bi3: 0 - |bi4: null - |""".stripMargin.asInstanceOf[YAML] - interceptMessage[co.blocke.scalajack.ScalaJackError]("Line 0: Expected a Number value here: +SEQ"){ - sj.read[SampleBigInt](yaml) - } - } - - test("Boolean must break") { - val yaml = - """bool1: true - |bool2: 15""".stripMargin.asInstanceOf[YAML] - interceptMessage[co.blocke.scalajack.ScalaJackError]("Line 1: Expected a Boolean value here: =VAL :15"){ - sj.read[SampleBoolean](yaml) - } - val yaml2 = - """bool1: true - |bool2: null""".stripMargin.asInstanceOf[YAML] - interceptMessage[co.blocke.scalajack.ScalaJackError]("Line 1: Expected a Boolean value here: =VAL :null"){ - sj.read[SampleBoolean](yaml2) - } - } - - test("Byte must break") { - val yaml = - """b1: true - |b2: -128 - |b3: 0 - |b4: 64""".stripMargin.asInstanceOf[YAML] - interceptMessage[co.blocke.scalajack.ScalaJackError]("Line 0: Expected a Number value here: =VAL :true"){ - sj.read[SampleByte](yaml) - } - val yaml2 = - """b1: 12 - |b2: -128 - |b3: 0 - |b4: null""".stripMargin.asInstanceOf[YAML] - interceptMessage[co.blocke.scalajack.ScalaJackError]("Line 3: Cannot parse an Byte from value"){ - sj.read[SampleByte](yaml2) - } - } - - test("Char must break") { - val yaml = - """c1: null - |c2: Y - |c3: Z""".stripMargin.asInstanceOf[YAML] - interceptMessage[co.blocke.scalajack.ScalaJackError]("Line 0: A Char typed value cannot be null"){ - sj.read[SampleChar](yaml) - } - val yaml2 = - """c1: G - |c2: - |c3: Z""".stripMargin.asInstanceOf[YAML] - interceptMessage[co.blocke.scalajack.ScalaJackError]("Line 1: Tried to read a Char but empty string found"){ - sj.read[SampleChar](yaml2) - } - val yaml3 = - """c1: G - |c2: - | - a - |c3: Z""".stripMargin.asInstanceOf[YAML] - interceptMessage[co.blocke.scalajack.ScalaJackError]("Line 2: Expected a String here: +SEQ"){ - sj.read[SampleChar](yaml3) - } - } - - test("Double must break") { - val yaml = - """d1: 1.79769313486E23157E308 - |d2: -1.7976931348623157E308 - |d3: 0.0 - |d4: -123.4567""".stripMargin.asInstanceOf[YAML] - interceptMessage[co.blocke.scalajack.ScalaJackError]("Line 0: Cannot parse an Double from value"){ - sj.read[SampleDouble](yaml) - } - } - - test("Enumeration must break") { - val yaml = - """e1: Small - |e2: Bogus - |e3: Large - |e4: null - |e5: Medium""".stripMargin.asInstanceOf[YAML] - interceptMessage[co.blocke.scalajack.ScalaJackError]( "Line 1: No value found in enumeration co.blocke.scalajack.yaml.primitives.Size$ for Bogus"){ - sj.read[SampleEnum](yaml) - } - val yaml2 = - """e1: Small - |e2: Medium - |e3: Large - |e4: null - |e5: 9""".stripMargin.asInstanceOf[YAML] - interceptMessage[co.blocke.scalajack.ScalaJackError]("Line 4: No value found in enumeration co.blocke.scalajack.yaml.primitives.Size$ for 9"){ - sj.read[SampleEnum](yaml2) - } - val yaml3 = - """e1: Small - |e2: Medium - |e3: Large - |e4: null - |e5: false""".stripMargin.asInstanceOf[YAML] - interceptMessage[co.blocke.scalajack.ScalaJackError]("Line 4: No value found in enumeration co.blocke.scalajack.yaml.primitives.Size$ for false"){ - sj.read[SampleEnum](yaml3) - } - } - - test("Float must break") { - val yaml = - """f1: 3.4028235E38 - |f2: [a,b] - |f3: 0.0 - |f4: -123.4567""".stripMargin.asInstanceOf[YAML] - interceptMessage[co.blocke.scalajack.ScalaJackError]("Line 1: Expected a Number value here: +SEQ"){ - sj.read[SampleFloat](yaml) - } - } - - test("Int must break") { - val yaml = - """i1: 2147483647 - |i2: -2147483648 - |i3: [a,b] - |i4: 123""".stripMargin.asInstanceOf[YAML] - interceptMessage[co.blocke.scalajack.ScalaJackError]( "Line 2: Expected a Number value here: +SEQ"){ - sj.read[SampleInt](yaml) - } - val yaml2 = - """i1: 2147483647 - |i2: -2147483648 - |i3: 2.3 - |i4: 123""".stripMargin.asInstanceOf[YAML] - interceptMessage[co.blocke.scalajack.ScalaJackError]("Line 2: Cannot parse an Int from value"){ - sj.read[SampleInt](yaml2) - } - } - - test("Long must break") { - val yaml = - """l1: 9223372036854775807 - |l2: -9223372036854775808 - |l3: true - |l4: 123""".stripMargin.asInstanceOf[YAML] - interceptMessage[co.blocke.scalajack.ScalaJackError]("Line 2: Expected a Number value here: =VAL :true"){ - sj.read[SampleLong](yaml) - } - val yaml2 = - """l1: 9223372036854775807 - |l2: -9223372036854775808 - |l3: 0.3 - |l4: 123""".stripMargin.asInstanceOf[YAML] - interceptMessage[co.blocke.scalajack.ScalaJackError]("Line 2: Cannot parse an Long from value"){ - sj.read[SampleLong](yaml2) - } - } - - test("Short must break") { - val yaml = - """s1: 32767 - |s2: true - |s3: 0 - |s4: 123""".stripMargin.asInstanceOf[YAML] - interceptMessage[co.blocke.scalajack.ScalaJackError]("Line 1: Expected a Number value here: =VAL :true"){ - sj.read[SampleShort](yaml) - } - val yaml2 = - """s1: 32767 - |s2: 3.4 - |s3: 0 - |s4: 123""".stripMargin.asInstanceOf[YAML] - interceptMessage[co.blocke.scalajack.ScalaJackError]("Line 1: Cannot parse an Short from value"){ - sj.read[SampleShort](yaml2) - } - } - - test("String must break") { - val yaml = - """s1: something - |s2: [a,b] - |s3: null""".stripMargin.asInstanceOf[YAML] - interceptMessage[co.blocke.scalajack.ScalaJackError]("Line 1: Expected a String here: +SEQ"){ - sj.read[SampleString](yaml) - } - } - - test("UUID must break") { - val yaml = - """u1: bogus - |u2: 580afe0d-81c0-458f-9e09-4486c7af0fe9""".stripMargin.asInstanceOf[YAML] - interceptMessage[co.blocke.scalajack.ScalaJackError]("Line 0: Failed to create UUID value from parsed text bogus"){ - sj.read[SampleUUID](yaml) - } - } diff --git a/core/src/test/scala/co.blocke.scalajack/yaml/primitives/TimePrim.scala b/core/src/test/scala/co.blocke.scalajack/yaml/primitives/TimePrim.scala deleted file mode 100644 index 13d67d08..00000000 --- a/core/src/test/scala/co.blocke.scalajack/yaml/primitives/TimePrim.scala +++ /dev/null @@ -1,321 +0,0 @@ -package co.blocke.scalajack -package yaml -package primitives - -import TestUtil._ -import munit._ -import munit.internal.console -import java.time._ - -class TimePrim() extends FunSuite: - - val sj = ScalaJack(YamlFlavor()) - - test("Duration must work") { - describe( - "---------------------------------\n: Time Primitive Tests (YAML) :\n---------------------------------", Console.BLUE - ) - describe("+++ Positive Tests +++") - val inst = - SampleDuration(Duration.ZERO, Duration.parse("P2DT3H4M"), null) - val yaml = sj.render(inst) - val comparison = """d1: PT0S - |d2: PT51H4M - |d3: null""".stripMargin - assertEquals(Set.empty[String], yaml.asInstanceOf[String].split("\n").toSet.diff(comparison.split("\n").toSet) ) - assertEquals(inst, sj.read[SampleDuration](yaml)) - } - - test("Instant must work") { - val inst = SampleInstant( - Instant.EPOCH, - Instant.MAX, - Instant.MIN, - Instant.parse("2007-12-03T10:15:30.00Z"), - null - ) - val yaml = sj.render(inst) - val comparison = """i1: 1970-01-01T00:00:00Z - |i5: null - |i2: +1000000000-12-31T23:59:59.999999999Z - |i3: -1000000000-01-01T00:00:00Z - |i4: 2007-12-03T10:15:30Z""".stripMargin - assertEquals(Set.empty[String], yaml.asInstanceOf[String].split("\n").toSet.diff(comparison.split("\n").toSet) ) - assertEquals(inst, sj.read[SampleInstant](yaml)) - } - - test("LocalDateTime must work") { - val inst = SampleLocalDateTime( - LocalDateTime.MAX, - LocalDateTime.MIN, - LocalDateTime.parse("2007-12-03T10:15:30"), - null - ) - val yaml = sj.render(inst) - val comparison = """d1: +999999999-12-31T23:59:59.999999999 - |d2: -999999999-01-01T00:00:00 - |d3: 2007-12-03T10:15:30 - |d4: null""".stripMargin - assertEquals(Set.empty[String], yaml.asInstanceOf[String].split("\n").toSet.diff(comparison.split("\n").toSet) ) - assertEquals(inst, sj.read[SampleLocalDateTime](yaml)) - } - - test("LocalDate must work") { - val inst = SampleLocalDate( - LocalDate.MAX, - LocalDate.MIN, - LocalDate.parse("2007-12-03"), - null - ) - val yaml = sj.render(inst) - val comparison = """d1: +999999999-12-31 - |d2: -999999999-01-01 - |d3: 2007-12-03 - |d4: null""".stripMargin - assertEquals(Set.empty[String], yaml.asInstanceOf[String].split("\n").toSet.diff(comparison.split("\n").toSet) ) - assertEquals(inst, sj.read[SampleLocalDate](yaml)) - } - - test("LocalTime must work") { - val inst = SampleLocalTime( - LocalTime.MAX, - LocalTime.MIN, - LocalTime.MIDNIGHT, - LocalTime.NOON, - LocalTime.parse("10:15:30"), - null - ) - val yaml = sj.render(inst) - val comparison = """d1: 23:59:59.999999999 - |d6: null - |d2: 00:00:00 - |d5: 10:15:30 - |d3: 00:00:00 - |d4: 12:00:00""".stripMargin - assertEquals(Set.empty[String], yaml.asInstanceOf[String].split("\n").toSet.diff(comparison.split("\n").toSet) ) - assertEquals(inst, sj.read[SampleLocalTime](yaml)) - } - - test("OffsetDateTime must work") { - val inst = SampleOffsetDateTime( - OffsetDateTime.MAX, - OffsetDateTime.MIN, - OffsetDateTime.parse("2007-12-03T10:15:30+01:00"), - null - ) - val yaml = sj.render(inst) - val comparison = """o1: +999999999-12-31T23:59:59.999999999-18:00 - |o2: -999999999-01-01T00:00:00+18:00 - |o3: 2007-12-03T10:15:30+01:00 - |o4: null""".stripMargin - assertEquals(Set.empty[String], yaml.asInstanceOf[String].split("\n").toSet.diff(comparison.split("\n").toSet) ) - assertEquals(inst, sj.read[SampleOffsetDateTime](yaml)) - } - - test("OffsetTime must work") { - val inst = SampleOffsetTime( - OffsetTime.MAX, - OffsetTime.MIN, - OffsetTime.parse("10:15:30+01:00"), - null - ) - val yaml = sj.render(inst) - val comparison = """o1: 23:59:59.999999999-18:00 - |o2: 00:00:00+18:00 - |o3: 10:15:30+01:00 - |o4: null""".stripMargin - assertEquals(Set.empty[String], yaml.asInstanceOf[String].split("\n").toSet.diff(comparison.split("\n").toSet) ) - assertEquals(inst, sj.read[SampleOffsetTime](yaml)) - } - - test("Period must work") { - val inst = SamplePeriod(Period.ZERO, Period.parse("P1Y2M3D"), null) - val yaml = sj.render(inst) - val comparison = """p1: P0D - |p2: P1Y2M3D - |p3: null""".stripMargin - assertEquals(Set.empty[String], yaml.asInstanceOf[String].split("\n").toSet.diff(comparison.split("\n").toSet) ) - assertEquals(inst, sj.read[SamplePeriod](yaml)) - } - - test("ZonedDateTime must work") { - val inst = SampleZonedDateTime( - ZonedDateTime.parse("2007-12-03T10:15:30+01:00[Europe/Paris]"), - null - ) - val yaml = sj.render(inst) - val comparison = """o1: 2007-12-03T10:15:30+01:00[Europe/Paris] - |o2: null""".stripMargin - assertEquals(Set.empty[String], yaml.asInstanceOf[String].split("\n").toSet.diff(comparison.split("\n").toSet) ) - assertEquals(inst, sj.read[SampleZonedDateTime](yaml)) - } - - test("Duration must break") { - describe("--- Negative Tests ---") - val yaml = - """d1: PT0S - |d2: 21 - |d3: null""".stripMargin.asInstanceOf[YAML] - interceptMessage[ScalaJackError]("Line 1: Failed to parse Duration from input '21'"){ - sj.read[SampleDuration](yaml) - } - val yaml2 = - """d1: PT0S - |d2: bogus - |d3: null""".stripMargin.asInstanceOf[YAML] - interceptMessage[ScalaJackError]("Line 1: Failed to parse Duration from input 'bogus'"){ - sj.read[SampleDuration](yaml2) - } - } - - test("Instant must break") { - val yaml = - """i1: 1970-01-01T00:00:00Z - |i2: false - |i3: -1000000000-01-01T00:00:00Z - |i4: 2007-12-03T10:15:30Z - |i5: null""".stripMargin.asInstanceOf[YAML] - interceptMessage[ScalaJackError]("Line 1: Failed to parse Instant from input 'false'"){ - sj.read[SampleInstant](yaml) - } - val yaml2 = - """i1: 1970-01-01T00:00:00Z - |i2: bogus - |i3: -1000000000-01-01T00:00:00Z - |i4: 2007-12-03T10:15:30Z - |i5: null""".stripMargin.asInstanceOf[YAML] - interceptMessage[ScalaJackError]("Line 1: Failed to parse Instant from input 'bogus'"){ - sj.read[SampleInstant](yaml2) - } - } - - test("LocalDateTime must break") { - val yaml = - """d1: -1 - |d2: "-999999999-01-01T00:00:00" - |d3: 2007-12-03T10:15:30 - |d4: null""".stripMargin.asInstanceOf[YAML] - interceptMessage[ScalaJackError]("Line 0: Failed to parse LocalDateTime from input '-1'"){ - sj.read[SampleLocalDateTime](yaml) - } - val yaml2 = - """d1: bogus - |d2: "-999999999-01-01T00:00:00" - |d3: 2007-12-03T10:15:30 - |d4: null""".stripMargin.asInstanceOf[YAML] - interceptMessage[ScalaJackError]("Line 0: Failed to parse LocalDateTime from input 'bogus'"){ - sj.read[SampleLocalDateTime](yaml2) - } - } - - test("LocalDate must break") { - val yaml = - """d1: -1 - |d2: "-999999999-01-01" - |d3: "2007-12-03" - |d4: null""".stripMargin.asInstanceOf[YAML] - interceptMessage[ScalaJackError]("Line 0: Failed to parse LocalDate from input '-1'"){ - sj.read[SampleLocalDate](yaml) - } - val yaml2 = - """d1: bogus - |d2: "-999999999-01-01" - |d3: "2007-12-03" - |d4: null""".stripMargin.asInstanceOf[YAML] - interceptMessage[ScalaJackError]("Line 0: Failed to parse LocalDate from input 'bogus'"){ - sj.read[SampleLocalDate](yaml2) - } - } - - test("LocalTime must break") { - val yaml = - """d1: "23:59:59.999999999" - |d2: "00:00:00" - |d3: "00:00:00" - |d4: "12:00:00" - |d5: false - |d6: null""".stripMargin.asInstanceOf[YAML] - interceptMessage[ScalaJackError]("Line 4: Failed to parse LocalTime from input 'false'"){ - sj.read[SampleLocalTime](yaml) - } - val yaml2 = - """d1: "23:59:59.999999999" - |d2: "00:00:00" - |d3: "00:00:00" - |d4: "12:00:00" - |d5: bogus - |d6: null""".stripMargin.asInstanceOf[YAML] - interceptMessage[ScalaJackError]("Line 4: Failed to parse LocalTime from input 'bogus'"){ - sj.read[SampleLocalTime](yaml2) - } - } - - test("OffsetDateTime must break") { - val yaml = - """o1: +999999999-12-31T23:59:59.999999999-18:00 - |o2: 2 - |o3: 2007-12-03T10:15:30+01:00 - |o4: null""".stripMargin.asInstanceOf[YAML] - interceptMessage[ScalaJackError]("Line 1: Failed to parse OffsetDateTime from input '2'"){ - sj.read[SampleOffsetDateTime](yaml) - } - val yaml2 = - """o1: +999999999-12-31T23:59:59.999999999-18:00 - |o2: -999999999-01T00:00:00+18:00 - |o3: 2007-12-03T10:15:30+01:00 - |o4: null""".stripMargin.asInstanceOf[YAML] - interceptMessage[ScalaJackError]("Line 1: Failed to parse OffsetDateTime from input '-999999999-01T00:00:00+18:00'"){ - sj.read[SampleOffsetDateTime](yaml2) - } - } - - test("OffsetTime must break") { - val yaml = - """o1: "23:59:59.999999999-18:00" - |o2: false - |o3: "10:15:30+01:00" - |o4: null""".stripMargin.asInstanceOf[YAML] - interceptMessage[ScalaJackError]("Line 1: Failed to parse OffsetTime from input 'false'"){ - sj.read[SampleOffsetTime](yaml) - } - val yaml2 = - """o1: "23:59:59.999999999-18:00" - |o2: 00:00:00:00+18:00 - |o3: "10:15:30+01:00" - |o4: null""".stripMargin.asInstanceOf[YAML] - interceptMessage[ScalaJackError]("Line 1: Failed to parse OffsetTime from input '00:00:00:00+18:00'"){ - sj.read[SampleOffsetTime](yaml2) - } - } - - test("Period must break") { - val yaml = - """p1: P0D - |p2: 5 - |p3: null""".stripMargin.asInstanceOf[YAML] - interceptMessage[ScalaJackError]("Line 1: Failed to parse Period from input '5'"){ - sj.read[SamplePeriod](yaml) - } - val yaml2 = - """p1: P0D - |p2: bogus - |p3: null""".stripMargin.asInstanceOf[YAML] - interceptMessage[ScalaJackError]("Line 1: Failed to parse Period from input 'bogus'"){ - sj.read[SamplePeriod](yaml2) - } - } - - test("ZonedDateTime must break") { - val yaml = - """o1: true - |o2: null""".stripMargin.asInstanceOf[YAML] - interceptMessage[ScalaJackError]("Line 0: Failed to parse ZonedDateTime from input 'true'"){ - sj.read[SampleZonedDateTime](yaml) - } - val yaml2 = - """o1: "2007-12-03T10:15:30+01:00 Earth" - |o2: null""".stripMargin.asInstanceOf[YAML] - interceptMessage[ScalaJackError]("Line 0: Failed to parse ZonedDateTime from input '2007-12-03T10:15:30+01:00 Earth'"){ - sj.read[SampleZonedDateTime](yaml2) - } - } \ No newline at end of file diff --git a/core/src/test/scala/co.blocke.scalajack/yaml/primitives/ValueClassPrim.scala b/core/src/test/scala/co.blocke.scalajack/yaml/primitives/ValueClassPrim.scala deleted file mode 100644 index b1d66cdc..00000000 --- a/core/src/test/scala/co.blocke.scalajack/yaml/primitives/ValueClassPrim.scala +++ /dev/null @@ -1,161 +0,0 @@ -package co.blocke.scalajack -package yaml -package primitives - -import TestUtil._ -import munit._ -import munit.internal.console -import java.util.UUID - -class ValueClassPrim() extends FunSuite: - - val sj = ScalaJack(YamlFlavor()) - - test("Value class of BigDecimal") { - describe( - "---------------------------------------\n: ValueClass Primitive Tests (YAML) :\n---------------------------------------", Console.BLUE - ) - describe("+++ Positive Tests +++") - val inst = VCBigDecimal(BigDecimal(12.34)) - val yaml = sj.render(inst) - assertEquals("""12.34""", yaml.asInstanceOf[String].trim ) - assertEquals(inst, sj.read[VCBigDecimal](yaml)) - } - - test("Value class of BigDecimal with null") { - val inst = VCBigDecimal(null) - val yaml = sj.render(inst) - assertEquals("""null""", yaml.asInstanceOf[String].trim ) - assertEquals(inst, sj.read[VCBigDecimal](yaml)) - } - - test("Value class of BigInt") { - val inst = VCBigInt(BigInt(1)) - val yaml = sj.render(inst) - assertEquals("""1""", yaml.asInstanceOf[String].trim ) - assertEquals(inst, sj.read[VCBigInt](yaml)) - } - - test("Value class of BigInt with null") { - val inst = VCBigInt(null) - val yaml = sj.render(inst) - assertEquals("""null""", yaml.asInstanceOf[String].trim ) - assertEquals(inst, sj.read[VCBigInt](yaml)) - } - - test("Value class of Byte") { - val inst = VCByte(100.asInstanceOf[Byte]) - val yaml = sj.render(inst) - assertEquals("""100""", yaml.asInstanceOf[String].trim ) - assertEquals(inst, sj.read[VCByte](yaml)) - } - - test("Value class of Boolean") { - val inst = VCBoolean(false) - val yaml = sj.render(inst) - assertEquals("""false""", yaml.asInstanceOf[String].trim ) - assertEquals(inst, sj.read[VCBoolean](yaml)) - } - - test("Value class of Char") { - val inst = VCChar('Z') - val yaml = sj.render(inst) - assertEquals("""Z""", yaml.asInstanceOf[String].trim ) - assertEquals(inst, sj.read[VCChar](yaml)) - } - - test("Value class of Double") { - val inst = VCDouble(100.5) - val yaml = sj.render(inst) - assertEquals("""100.5""", yaml.asInstanceOf[String].trim ) - assertEquals(inst, sj.read[VCDouble](yaml)) - } - - test("Value class of Enumeration") { - val inst = VCEnumeration(Size.Medium) - val yaml = sj.render(inst) - assertEquals("""Medium""", yaml.asInstanceOf[String].trim ) - assertEquals(inst, sj.read[VCEnumeration](yaml)) - } - - test("Value class of Enumeration with null") { - val inst = VCEnumeration(null) - val yaml = sj.render(inst) - assertEquals("""null""", yaml.asInstanceOf[String].trim ) - assertEquals(inst, sj.read[VCEnumeration](yaml)) - } - - test("Value class of Float") { - val inst = VCFloat(100.5F) - val yaml = sj.render(inst) - assertEquals("""100.5""", yaml.asInstanceOf[String].trim ) - assertEquals(inst, sj.read[VCFloat](yaml)) - } - - test("Value class of Int") { - val inst = VCInt(100) - val yaml = sj.render(inst) - assertEquals("""100""", yaml.asInstanceOf[String].trim ) - assertEquals(inst, sj.read[VCInt](yaml)) - } - - test("Value class of Long") { - val inst = VCLong(100L) - val yaml = sj.render(inst) - assertEquals("""100""", yaml.asInstanceOf[String].trim ) - assertEquals(inst, sj.read[VCLong](yaml)) - } - - test("Value class of Short") { - val inst = VCShort(100.asInstanceOf[Short]) - val yaml = sj.render(inst) - assertEquals("""100""", yaml.asInstanceOf[String].trim ) - assertEquals(inst, sj.read[VCShort](yaml)) - } - - test("Value class of String") { - val inst = VCString("foo") - val yaml = sj.render(inst) - assertEquals("""foo""", yaml.asInstanceOf[String].trim ) - assertEquals(inst, sj.read[VCString](yaml)) - } - - test("Value class of String with null") { - val inst = VCString(null) - val yaml = sj.render(inst) - assertEquals("""null""", yaml.asInstanceOf[String].trim ) - assertEquals(inst, sj.read[VCString](yaml)) - } - - test("Value class of UUID") { - val inst = - VCUUID(UUID.fromString("54cab778-7b9e-4b07-9d37-87b97a011e55")) - val yaml = sj.render(inst) - assertEquals("""54cab778-7b9e-4b07-9d37-87b97a011e55""", yaml.asInstanceOf[String].trim ) - assertEquals(inst, sj.read[VCUUID](yaml)) - } - - test("Value class of UUID with null") { - val inst = VCUUID(null) - val yaml = sj.render(inst) - assertEquals("""null""", yaml.asInstanceOf[String].trim ) - assertEquals(inst, sj.read[VCUUID](yaml)) - } - - test("Value class of Number") { - val inst = VCNumber(25) - val yaml = sj.render(inst) - assertEquals("""25""", yaml.asInstanceOf[String].trim ) - assertEquals((inst, true), { - val r = sj.read[VCNumber](yaml) - (r, r.vc.isInstanceOf[Byte]) - }) - } - - test("Wrong YAML for wrapped type") { - describe("--- Negative Tests ---") - val yaml = """100.25""".asInstanceOf[YAML] - interceptMessage[ScalaJackError]("Line 0: Cannot parse an Short from value"){ - sj.read[VCShort](yaml) - } - } diff --git a/doc/.DS_Store b/doc/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..5008ddfcf53c02e82d7eee2e57c38e5672ef89f6 GIT binary patch literal 6148 zcmeH~Jr2S!425mzP>H1@V-^m;4Wg<&0T*E43hX&L&p$$qDprKhvt+--jT7}7np#A3 zem<@ulZcFPQ@L2!n>{z**++&mCkOWA81W14cNZlEfg7;MkzE(HCqgga^y>{tEnwC%0;vJ&^%eQ zLs35+`xjp>T01,"b"->2)), Amorphous(null), Amorphous(Small(99)) - ) +) -sj.render(all) -``` +sjAmorphousList.toJson(all) +``` This renders: - ```JSON [ {"thing":true}, @@ -35,30 +39,28 @@ This renders: {"thing":[1,2,3]}, {"thing":{"a":1,"b":2}}, {"thing":null}, - {"thing":{"_hint":"com.me.Small","num":99}} + {"thing":{"num":99}} ] ``` -So far, so good, right? It gets a bit more complicated... +So far, so good, right? All the values seem to be rendered well. Now it gets a bit more complicated... ### Classes and Maps -You can see from the sample above that when an Any-typed value is populated with an object, it is rendered like a trait, with its type hint (only the default type hint "_hint" is supported for now). This is so ScalaJack knows what class to materialize upon reading this JSON. -Without a type hint, the JSON object will be inferred to be just a key/value Map in Scala. +You can see from the sample above that when an Any-typed value is populated with an object, it is rendered like any class. However there's no way for ScalaJack to know what class to reconstruct when parsing an Any value. Any JSON value in braces '{ }' will be presumed to be a Map, regardless of what they actually were when the JSON was rendered. + +### Options -**Note:** Option[] values cannot be inferred as an Any value. Rendering Some(thing) would always be read as thing. ScalaJack would never be able to infer Some(thing) vs thing from JSON. +Option[] values cannot be inferred as an Any value. Rendering Some(thing) would always be read as simply 'thing'. ScalaJack has no information this value was originally an Option, in order to wrap it in Some(). + ### Numbers -When reading a numerical value, ScalaJack must infer what kind of numerical type to use. There's no right/wrong answer here, so ScalaJack uses a simple fitting mechanism. The fittings are shown in the table below, but one important thing to keep in mind: If you render an Any numerical value and read it back in, the value read in may be a different type than you rendered! ScalaJack takes great pains to try to ensure a read object matches the rendered original, but for Any this promise is not always possible to keep. - -|Scala render() Type for Any|ScalaJack read() Type for Any| -|-------|-------| -|Byte |Long -|Short |Long -|Int |Long -|Long |Long -|BigInt |Long if it fits, else BigInt -|Float |Double -|Double |Double -|BigDecimal |Double if it fits, else BigDecimal - ->**Remember that when processing Any, there is no wrong answer--any returned value, in any type, is an Any! There's just expected and unexpected on your part.** + +When reading a numerical value, ScalaJack must infer what kind of numerical type to use. There's no right/wrong answer here, so ScalaJack uses a simple fitting mechanism. If ScalaJack's parser detects a numerical value for Any, it reads it in as a BigDecimal value, then attempts to "fit" the number in this order: + +1. Int if value is a valid Int +2. Long if value is a valid Long +3. Double if value is a valid Double +4. BigInt if value is valid BigInt +5. otherwise leave it as BigDecimal + +>**Remember that when processing Any, there is no wrong answer--any returned value, in any type, is a valid Any value. There's just expected and unexpected on your part.** \ No newline at end of file diff --git a/doc/classesAndTraits.md b/doc/classesAndTraits.md index 1199d5fc..a20507d7 100644 --- a/doc/classesAndTraits.md +++ b/doc/classesAndTraits.md @@ -1,12 +1,16 @@ + ## Case Classes and Traits -Parsing and serializing case classes is as easy as it gets with ScalaJack. Serializing is simply a call to render(): +Parsing and serializing case classes is vey straightforward with ScalaJack. Serializing is simply a call to toJson(): ```scala -case class Person(name:String, age:Int) -val sj = ScalaJack() -val js = sj.render(Person("Mike",32)) +// Classes must be defined in separate file. Sorry--it's a macro thing +// case class Person(name:String, age:Int) + +given sjPerson: ScalaJack[Person] = sjCodecOf[Person] + +val js = sjPerson.toJson(Person("Mike",32)) // renders {"name":"Mike","age":32} ``` @@ -15,66 +19,87 @@ All basic Scala primitive data types are supported in addition to Java primitive Of course you can nest collections and case classes as you like: ```scala -case class Hobby(desc:String, cost:Double) -case class Person(name:String, age:Int, hobbies:List[Hobby]) -sj.render(Person("Mike",32,List(Hobby("surfing",1000.0)))) +// case class Hobby(desc:String, cost:Double) +// case class Person(name:String, age:Int, hobbies:List[Hobby]) + +given sjPerson: ScalaJack[Person] = sjCodecOf[Person] + +val js = sjPerson.toJson(Person("Mike",32,List(Hobby("surfing",1000.0)))) // renders {"name":"Mike","age":32,"hobbies":[{"desc":"surfing","cost":1000.0}]} ``` -Parsing JSON into a case class is similarly easy, with the addition of a given type parameter to tell ScalaJack what kind of class you're trying to construct: +Parsing JSON into a case class is also simple: ```scala val js = """{"name":"Mike","age":32}""" -val person = sj.read[Person](js) +val person = sjPerson.fromJson(js) ``` This works for collections too: ```scala +given sjListOfPerson: ScalaJack[List[Person]] = sjCodecOf[List[Person]] +given sjMapOfPerson: ScalaJack[Map[Person]] = sjCodecOf[Map[Person]] + val js = """[ {"name":"Mike","age":32}, {"name":"Sarah","age":29} ]""" -val person = sj.read[List[Person]](js) +val persons = sjListOfPersion.fromJson(js) val js2 = """{ "surfers":[{"name":"Mike","age":32}] }""" -val byHobby = sj.read[Map[String,List[Person]]](js) +val byHobby = sjMapOfPersion.fromJson(js) ``` So you can see the combinations can be as complex as you need. ### Traits -Traits work the same way but with the small addition of a type hint. This is to instruct ScalaJack what concrete class to construct from a given trait type. Consider this example: - +ScalaJack 8 works with sealed traits only. + +> This is a significant departure from previous versions of ScalaJack, +> which handled any trait, sealed or not. ScalaJack 8 is almost entirely +> a macro, which means everything about serialization must be known at +> compile-time. For a non-sealed trait, we can add a type hint to +> rendered JSON, but upon parsing that JSON we would have to retrieve +> the type hint and materialize the concrete class given by the hint. +> Problem is: we won't know what class that is until runtime, so +> ScalaJack 8's macro infrastructure has nothing to work with at +> compile-time. In order to support non-sealed traits, like older +> ScalaJack, we would need to create an entire complimentary mirror of +> ScalaJack 8 that ran only at runtime. At this time, we're not +> convinced the level of work required to accomplish this would be worth +> the use case. + +Example of sealed trait use: + +**Pet.scala** ```scala -package com.me - -trait Pet{ val name:String; val numLegs:Int } +sealed trait Pet{ val name:String; val numLegs:Int } case class Dog(name:String, numLegs:Int) extends Pet +case class Cat(name:String, numLegs:Int, numberOfNaps: Int) extends Pet +``` + +**MyProg.scala** +```scala +given sjDog: ScalaJack[Dog] = sjCodecOf[Dog] // not needed to serialize Pet--only here to show difference! +given sjPet: ScalaJack[Pet] = sjCodecOf[Pet] // render trait Pet (any of its sealed children: Cat or Dog) val inst:Pet = Dog("Fido",4) -// Render as a case class -val jsDog = sj.render(inst) // renders {"name":"Fido","numLegs":4} -sj.read[Dog](jsDog) +// Render Dog as a case class +val jsDog = sjDog.toJson(inst) // renders {"name":"Fido","numLegs":4} +sjDog.fromJson(jsDog) // Render as a trait -val jsPet = sj.render[Pet](inst) // renders {"_hint":"com.me.Dog","name":"Fido","numLegs":4} -sj.read[Pet](jsPet) +val jsPet = sjPet.toJson(inst) // renders {"_hint":"Dog","name":"Fido","numLegs":4} +sjPet.fromJson(jsPet) // Oops! -sj.read[Pet](jsDog) // Explodes! No type hint means ScalaJack doesn't know which Pet to make +sjDog.fromJson(jsDog) // Fails. No type hint means ScalaJack doesn't know which Pet to make ``` -OK, there's a few things going on here. Let's work backwards. Notice I can render my instance either as a case class (Dog) or a trait (Pet). I "force" the trait rendering by providing the type of the trait, Pet, in the render() call, which tells ScalaJack to add a type hint field in the rendered output. That's because you rendered a Pet, not a Dog. Without the hint, ScalaJack has no idea that this Pet should, in fact, be a Dog when read back in, so we have to leave it a hint. - -Reading a properly serialized trait back in is simple: -```scala -val js = """{"_hint":"com.me.Dog","name":"Fido","numLegs":4}""" -sj.read[Pet](js) -``` The given type hint will allow ScalaJack to construct a Dog object and return it as a Pet, which is what you want. -We'll see in a later section how you can customize both the type hint label and the the hint value in your JSON. +We'll see in a later section how you can customize both the type hint label and the the hint value in your JSON. \ No newline at end of file diff --git a/doc/config.md b/doc/config.md deleted file mode 100644 index f1010e33..00000000 --- a/doc/config.md +++ /dev/null @@ -1,108 +0,0 @@ - -## ScalaJack Configuration and Usage - -### ScalaJack Instantiation -All ScalaJack usage starts with creating a ScalaJack instance. - -```scala -val sj = ScalaJack() -``` - -This instance is used for all serialization activities. - -### Flavors - -ScalaJack supports several "flavors": JSON, Delimited (e.g. CSV), MongoDB, and DynamoDB out of the box. Others may create new flavors by extending the pattern. - -Here's how to get the right flavor of ScalaJack for your needs: - -|Flavor |ScalaJack instantiation -|-----|-------- -|JSON | val sj = ScalaJack() // JSON is default if no flavor given -|Delimited | val sj = ScalaJack(DelimitedFlavor()) -|MongoDB | val sj = ScalaJack(MongoFlavor()) -|DynamoDB | val sj = ScalaJack(DynamoFlavor()) - -Note that MongoDB and DynamoDB require a separate library, and are not included in the core ScalaJack package. - -### ScalaJack Configuration Methods -ScalaJack can be configured using "chain" methods, i.e. you chain them together, for example: - -```scala -val sj = ScalaJack() - .parseOrElse((typeOf[Person],typeOf[DefaultPerson])) - .withDefaultHint("kind") -``` -Most of these configurations are JSON-only, as they don't make sense for the other formats. An exception will be thrown if a configuration method is used that isn't supported for a particular ScalaJack flavor. - -#### allowPermissivePrimitives() -Third party JSON can be pretty messy! They may make booleans and numbers into strings. allowPermissivePrimitives() allows ScalaJack to be a little less strict and try to accept "true" as true, for example. It's a little adaptation to an imperfect world. - -```scala -val js = """["true","false"] -val sj1 = ScalaJack() -val sj2 = ScalaJack().allowPermissivePrimitives() - -sj1.read[List[Boolean]](js) // explodes with error: expected Boolean not String - -sj2.read[List[Boolean]](js) // reads List(true,false) -``` - - -#### enumsAsInts() -You can think of an Enumeration as either a label (String) or an integer (position within the list of allowed values). When parsing, ScalaJack will try and accept either a String or an Int as a valid value for an Enumeration-typed field. By default, when rendering, ScalaJack renders the String label. If you want to render the Int, the set this configuration. - -```scala -object Size extends Enumeration { -val Small, Medium, Large = Value -} -case class SampleEnum(e1: Size.Value, e2: Size.Value) -import Size._ - -val sj1 = ScalaJack() -val sj2 = ScalaJack().enumsAsInts() -val inst = SampleEnum(Large, null) -sj1.render(inst) // renders {"e1":"Large","e2":null} -sj2.render(inst) // renders {"e1":2,"e2":null} -``` - -#### parseOrElse(poe: (Type, Type)*) -Configures a default object to be returned if any object of the particular given type can't be parsed successfully. This is a mapping, so multiple specifications are possible: (match_type, default_object_type) - -```scala -val sj = ScalaJack() - .parseOrElse( - (typeOf[Address], typeof[DefaultAddress]), - (typeOf[Command], typeOf[DoNothingCmd]) - ) -``` -So in this example, whenever ScalaJack fails to parse an Address-typed value it will substitute a DefaultAddress object in its place. - - -#### withAdapters(ta: TypeAdapterFactory*) -Register a list of custom TypeAdapters with ScalaJack. This is to allow you to offer custom serialization handling of your own types. - -[See an example.](custom.md) - -#### withDefaultHint(hint: String) -This simply changes the globally default type hint key strings (ScalaJack default hint key is "_hint"). This can be mixed with withHints(). Any type not specified in withHints will pick up your re-set default hint. - -#### withHints(h: (Type, String)*) -Specify per-type hints to override the global default type hint of "_hint". -__Tip:__ To get the type of a class, for example to use with this function, you can use: -```scala -typeOf[MyClass] -``` - -[See an example.](typeHint.md) - -#### withHintModifiers(hm: (Type, HintModifier)*) -Where withHints modifies the key String for a type hint, withHintModifiers modifies the hint's value. Particiularly handy for 3rd party JSON where you don't own/control the values and you want a function to line it up with your internal representation. - -[See an example.](typeHint.md) - -#### withTypeModifier(tm: HintModifier) -This is used to apply a hint value modifier (function) the string identifying the externalized type hint (type member) of a class. This is global, I'm afraid, so any modifier you specify here will be applied to all type members serialized with this instance of ScalaJack. - -[See an example.](externalTypes.md) - diff --git a/doc/custom.md b/doc/custom.md deleted file mode 100644 index 9e4e6841..00000000 --- a/doc/custom.md +++ /dev/null @@ -1,99 +0,0 @@ -## Custom Type Adapters - -ScalaJack does a great job of reading and rendering stock data types, but sometimes you just need something custom. Let's use an example. Imagine you have a phone number of type String that you want formatted like a US phone number (XXX-XXX-XXXX) but stored as a simple String (no dashes). - -To do this ScalaJack allows you to create a custom type adapter and link it into its own chain of adapters. Let's walk through the process step-by-step. - -### Step 1: Create a Type - -```scala -object MyTypes { - type Phone = String -} -import MyTypes._ -``` - -In this case we create a Phone type to differentiate Phone, which is a String, from any other String value. - -### Step 2: Create the TypeAdapter -There are 3 essential functional pieces to a ScalaJack TypeAdapter. -1. Something that matches the type we want -2. Something to read input of that type -3. Something to output an object of that type - -Let's look at a straightforward example then unpack some nuances. - -```scala -object PhoneAdapter extends TypeAdapter.===[Phone] with Stringish { - def read(parser: Parser): Phone = - parser.expectString() match { - case null => null - case s: String => s.replaceAll("-", "") - } - - def write[WIRE](t: Phone, writer: Writer[WIRE], out: Builder[WIRE, WIRE]): Unit = t match { - case null => writer.writeNull(out) - case _ => writer.writeString("%s-%s-%s".format(t.substring(0, 3), t.substring(3, 6), t.substring(6)), out) - } -} -``` -Here you'll see all three essential pieces. The type matching is accomplished with ```extends TypeAdapter.===[Phone]```. The read and write functions speak for themselves. Note the use of WIRE here (not, for example, JSON). This is because ScalaJack is a general-purpose serializer so we don't presume JSON. Each "flavor" of ScalaJack (JSON being one flavor) implements the Reader and Writer traits, which define a nice set of primitive operations we can use for our TypeAdapters. - -In this example we see that read() strips out all the dashes from the phone numbers, while write() re-inserts them in a US format. - -Phone numbers are Strings, so they're nullable, so we handle null for read/write as well. (Most of your TypeAdapters will also be nullable unless you are wrapping a non-nullable type like Int.) - -Also note the "Stringish" mixin in our TypeAdapter. This tells ScalaJack that your type is String-encoded. This is an oddment of Map key handling for WIRE formats like JSON. Map/Object keys in JSON must be Strings, yet Scala imposes no such limitation. Therefore ScalaJack must wrap some primitive types in String quotes when used as JSON map keys. String-encoded (Stringish) types, like Phone, require no such special handling so we notate that in the TypeAdapter to avoid double quoting. For example: - -```scala -sj.render(Map(true -> true, false -> false)) -// renders string-wrapped map keys: {"true":true,"false":false} -``` - -### Step 3: Create the PhoneAdapter - -```scala -// Override just Phone -object PhoneAdapter extends TypeAdapter.===[Phone] { - override val irTransceiver: IRTransceiver[Phone] = new PhoneIRTransceiver() -} -``` -You can see we specify our IRTransceiver. One thing that may not be clear is the Typeadapter.===[Phone] notation. This matches exactly on Phone type, so ScalaJack doesn't confuse other String values with Phone and try to serialize/deserialize them with your custom code. - -If what you want is to treat Phone and all subclasses as Phone (with your custom code), then extend TypeAdapter.=:=[Phone] instead. If we did that in this case, every String would be treated as a Phone, with likely dissastrous results. - - -### Step 3: Wire your PhoneAdapter into ScalaJack's list of type adapters - -To use your new type adapter, hook it into ScalaJack: - -```scala -val sj = ScalaJack().withAdapters(PhoneAdapter) -``` -Now anything you parse with a Phone in it will receive your specified special handling via your custom adapter. - -**TIP:** withAdapters() is varargs, so you can pass a chain of adapter: ScalaJack().withAdapters(a,b,c,...) - -### Nuance: Matching Types -When creating custom TypeAdapters in ScalaJack you have a number of options when deciding how to match Types. ScalaJack basically works like this: it looks for a give type (reading or writing) by iterating through a list of TypeAdapterFactories (with Type matchers) until one matches. *How* types are matched is the question. - -In the Phone example above we used a simple way to create a TypeAdapterFactory using: -```scala -object PhoneAdapter extends TypeAdapter.===[Phone] with Stringish { -.. -} -``` -ScalaJack allows 3 kinds of type comparisons when matching using this method: - -```scala -type Phone = String -trait Foo -class Bar() extends Foo -``` -|Comparator|Meaning -|-------|-------| -|A === B|Type A exactly matches another. Phone === String is false -|A =:= B|Type A can be implicitly converted to type B. Phone =:= String is true -|A <:< B|Type A is a subtype of type B. Bar <:< Foo is true - -These 3 comparators will likely provide most of your needed type matching, but if you have a really special case you can write your own custom TypeAdapterFactory. The TypeAdapterFactory class provides a number of matchings by implementing typeAdapterOf[T]. The CaseClassTypeAdapterFactory class in the ScalaJack code is a good example of usage. diff --git a/doc/delimited.md b/doc/delimited.md deleted file mode 100644 index 73bd661e..00000000 --- a/doc/delimited.md +++ /dev/null @@ -1,44 +0,0 @@ - -## Delimited Format (e.g. CSV) - -ScalaJack offers support for delimited serialization (e.g. CSV). It must be said clearly that there are structural limitations to the support available because delimited format can't support advanced nested structures. Here are some of the features and limitations imposed by ScalaJack on delimited-format serialization: - -* Each input string (presumably a line of a larger input file) shall be parsed to one given object -* Map structures are not supported (how would you do key-value in a flat format?) -* Lists are implemented by a nested field, escaped in quotes. This is handy for very simple lists, but can get messy quickly. Use with caution! -* Scala Either is supported but delimited input is very likely to be read as a String, which can mess up Left/Right of an Either if both are String-like -* Enumerations are supported -* Traits are **not** supported (where would you put the type hint in CSV format?) -* You must enclose a field with double-qutoes if it contains double quotes, the delimiter character ('comma), or line breaks. -* Don't try reading Any types! In delimited input, everything will be interpreted as a String, so if that's not what you want you'll be disappointed. -* Double quotes within a string/field must be escaped using double-double quotes, "" (not the more JSON style \") -```scala -val sj = ScalaJack(DelimitedFlavor) // CSV default -// val sj = ScalaJack(DelimitedFlavor('|')) <-- to set a non-comma delimiter character -val inst = StringHolder("Hey, you", "This \"\"life\"\"", "And now, \"\"Mike\"\" will sing.") -val csv = sj.render(inst) -// renders: "Hey, you","This "life"","And now, "Mike" will sing." -``` - -### Lists -Simple list support is provided. -```scala -val sj = ScalaJack(DelimitedFlavor) -case class Foo(a:Int, b:List[String]) -val f = Foo(1,List("a","b","c")) -sj.render(f) -// renders 1,"a,b,c" -``` -Note that while clever, list representation can get ugly fast. Consider the case where the strings in the list need to themselves be escaped because they contain a delimiter character. -```scala -val f = Foo(1,List("a","b,x","c")) -sj.render(f) -// renders 1,"a,""b,x"",c" -``` -Not awful, but you can see where the double-quotes could multiply very quickly for any more sophisticated structure. - -### Handling None and Null -Empty values in delimited input are read in as null in most cases, with the exception of an Optional field, where a null is read as None. If a case class field is read as empty, and the field has a defined default value, the default value is returned. - -### Takeaway -Generally it's best to treat delimited format as "fragile", meaning don't get too clever or cute with it. Use simple, flat or nearly-flat objects and you'll be fine. \ No newline at end of file diff --git a/doc/dynamo.md b/doc/dynamo.md deleted file mode 100644 index b74ccfa5..00000000 --- a/doc/dynamo.md +++ /dev/null @@ -1,41 +0,0 @@ -## DynamoDB Support -ScalaJack provides some limited support for DynamoDB. To be very clear, ScalaJack is a serialization product, and does not involve itself in database CRUD calls, i.e. it isn't a Dynamo library or wrapper. What ScalaJack does do is try to make the process of calling Dynamo easier and contain less boilerplate by using its powerful reflection abilities to create appropriate objects for Dynamo. - -First of all you'll need to include a separate scalajack_dynamo library as shown in the main README file. (We separated it so non-Dynamo users won't have to include Dynamo library dependencies.) - -#### Table Creation -DynamoDB table creation is accomplished (in Dynamo's native Java API) via method calls accepting a CreateTableRequest object. ScalaJack provides a feature to create a CreateTableRequest. - -```scala -case class Misc(wow: Double, bing: String) - -@Collection(name = "people") -case class Person( - @DBKey(index = 1) name:String, - @DBKey(index = 0) age:Int, - likes: List[String], - stuff: Misc, - foo: Option[Boolean] = None -) - -val sj = ScalaJack(DynamoFlavor()) -val req = sj.asInstanceOf[DynamoFlavor].createTableRequest[PersonOneKey](new ProvisionedThroughput(12L, 5L)) -``` -First let's look at the Person class. Notice the annotations. The @Collection annotation marks the class with a given table name in Dynamo. The two @DBKey annotations mark the primary key and sort key. Notice the index values in the @DBKey annotation. Index 0 is the primary key and index 1 is the sort key. Just like in Dynamo, the sort key is optional. - -Note that Dynamo's rules apply, meaning a key must be either String or Number. - -Once constructed, you can use the CreateTableRequest as you usually would, adding more to it via its built-in methods, then using it to actually create the table. - -**NOTE:** Do you see the .asInstanceOf in the code above? We need that. createTableRequest is not a method on stock ScalaJack--only the Dynamo flavor so we need to cast it first. - -#### Item -The other feature ScalaJack is very helpful for in Dynamo is Item. Rendering an Item (or reading from it) is exactly like ScalaJack for JSON, except instead of JSON you have a Dynamo Item object: - -```scala -val inst: Person = Person("Greg", 50, List("Woodworking", "Diet Coke"), Misc(1.23, "boom")) -val item:Item = sj.render(inst) -// { Item: {name=Greg, age=50, likes=[Woodworking, Diet Coke], stuff={wow=1.23, bing=boom}} } - -sj.read[Person](item) // == original Person object -``` diff --git a/doc/externalTypes.md b/doc/externalTypes.md deleted file mode 100644 index 0396e3b0..00000000 --- a/doc/externalTypes.md +++ /dev/null @@ -1,69 +0,0 @@ -## Externalized Types - -When parsing some JSON into a Scala trait we use a type hint to know what the concrete case class should be. Normally this type hint is self-contained within the JSON for the class: -```json - {"_hint":"com.me.Customer", "name":"Fred", "acct":"abc123"} -``` -What if the hint needs to be externalized, i.e. the type is specified *outside* the JSON block for the class? Perhaps we have a message router that knows how to parse a Message class containing a Payload (trait). Perhaps the router doesn't need to read the payloads but it does need to know their type to decide where to send the message. It would be very convenient to "promote" the payload type hint to the outer Message wrapper. - -Maybe we need something like this: -```scala - package com.me - - trait Command - case class FieldCommand(ping:String) extends Command - - case class Message[T <: Command](id:String, payload:T) -``` -Let's further assume the JSON we want would look like this: -```json - {"payloadKind":"com.me.FieldCommand", "id":"abc123", "payload":{"ping":"pong"}} -``` -Notice that the type hint is outside the object it describes (payload). How can we accomplish this? How can we move the type hint outside the described object? - -ScalaJack uses a type member in your class to specify a type hint of a contained object. Like this: -```scala - case class Message[T <: Command](id:String, payload:T) { - type payloadKind = T - } -``` -How would we use this feature? -```scala - val inst: Message[Command] = Message("abc123", FieldCommand("pong")) - sj.render(inst) - // {"payloadKind":"com.me.FieldCommand", "id":"abc123", "payload":{"ping":"pong"}} -``` -This is the same render usage as normal, however notice the special treatment of the type hint. Not only is it outside in the "outer" Message wrapper, but it is called payloadKind, not "_hint". For external type hints (Scala type members), ScalaJack uses the name of the type member field as the hint. - -#### Reading when you have all the class/jar files -Here's an example of reading using a match statement to switch on the kinds of payload: -```scala -val inbound = sj.read[Message[Command]](js) -inbound.payload match { - case fc:FieldCommand => // stuff here - case ac:AirCommand => // stuff here - case _ => // catch-all -} -``` -In this case ScalaJack actually materialized the payloads, meaning it needed the class files for those objects. What if we don't have those jar files? - -#### Reading when you *don't* have all the class/jar files -```scala -case class MsgWrapper(kind: String, id: String) extends SJCapture -val inbound = sj.read[MsgWrapper](js) -``` -Here we had to create a simplified Message wrapper, that doesn't have a payload field (because we don't have the class files to parse it). In this example inbound.kind now holds the class name for the payload's type, even though we don't have a corresponding class/jar file for this object. SJCapture holds all the "extra" payload information so we can safely re-render the whole original object if needed. - -__Bottom Line:__ If you specify a type member in your case class, ScalaJack will externalize that type hint, packaging it in the outer or wrapping class. Without a type member, ScalaJack will package the type hint normally inside the serialized class. - -### Custom Type Modifiers -Just like for type hints, we may receive 3rd party JSON where having the type value be a fully-qualified Scala class name may not be possible. We have a limited ability to use the same modifiers we use for type hints. -```scala -val sjm = ScalaJack().withTypeModifier(ClassNameHintModifier((hint: String) => "com.me." + hint, (cname: String) => cname.split('.').last)) -val inst: Message[Command] = Message("abc123", FieldCommand("pong")) -val js = sjm.render[Message[Command]](inst) -// {"payloadKind":"FieldCommand", "id":"abc123", "payload":{"ping":"pong"}} -``` -Note the class path has been modified and we now only see the trailing class name. The other out-of-the-box modifier, StringMatchHintModifier, works fine too, in case you need to completely divorce the value in the JSON from any notion of the class name. - -**WARNING:** There is a pretty big limitation on type member modifiers: You can only specify one type modifier globally, i.e. at this time they are not class-specific. That means the type modifier you specifiy will apply to *all* your type member values! Perhaps a future version of ScalaJack will support multiple modifiers and make them class/member specific. diff --git a/doc/filter.md b/doc/filter.md deleted file mode 100644 index b707528a..00000000 --- a/doc/filter.md +++ /dev/null @@ -1,30 +0,0 @@ -## Filter - -Earlier in this documentation we saw a number of ways to process input through ScalaJack, including ways using SJCapture and externalized type hints to build filtering capabilities. Wouldn't it be nice if there was a cleaner, Scala-like way of filtering on a type? - -```scala -trait Comm -case class Event(happening: Int) extends Comm -trait Command extends Comm { val goDo: String } -case class SimpleCommand(goDo: String, public: Boolean) extends Command -case class CommMessage[T <: Comm](id: Int, payload: T) { - type kind = T - } - -val js = "123" -val isMap = sj.filter[Map[String, Int]]() -val isInt = sj.filter[Int]() -val isCmd = sj.filter[CommMessage[Command]]("kind") -val isEvt = sj.filter[CommMessage[Event]]("kind") - -sj.parse(js) match { - case isMap(x) => println(x) - case isInt(x) => println("Int: " + x) - case isCmd(x) => println("Cmd: " + x) - case isEvt(x) => println("Evt: " + x) - case _ => - } -``` -sj.filter() returns a Filter object that can be used as a type-safe Scala extractor. - -Nice! diff --git a/doc/json4s.md b/doc/json4s.md deleted file mode 100644 index c110ae1c..00000000 --- a/doc/json4s.md +++ /dev/null @@ -1,10 +0,0 @@ -## Json4s Support -Json4s is a common and convenient way to query and manipulate raw JSON elements. ScalaJack includes support for Json4s by creating a flavor for it, meaning Json4s is a target serialization format. - -```scala -import co.blocke.scalajack.json4s._ -val sj = ScalaJack(Json4sFlavor()) -``` -Then you can use all the usual ScalaJack capabilities. - -Json4sFlavor is very handy if you want to do more than straight object serialization/deserialization, for instance re-process parsed JSON before materializing it into a Scala object. diff --git a/doc/map.md b/doc/map.md deleted file mode 100644 index 0ecb0557..00000000 --- a/doc/map.md +++ /dev/null @@ -1,86 +0,0 @@ -## Converters - -As of version 6.2.0, ScalaJack includes a Converters package to add some syntax sugar making it easier to move between wire formats. - -### Case 1: Simple conversion between two wire formats -```scala -import co.blocke.scalajack.Converters._ -case class Person(name: String, age: Int) - - val js = """{"name":"Greg","age":53}""" - println(js.jsonToYaml) -``` - -Note: For Delimited converers (delimitedToJson, delimitedToJson4s, delimitedToYaml, jsonToDelimited, json4sToDelimted, yamlToDelimited), you must specify a type parameter. -This is because Delimited format is so representation-poor, it can't represent a Map required for the conversion. An example: - -```scala -import co.blocke.scalajack.Converters._ -case class Person(name: String, age: Int) - - val js = """{"name":"Greg","age":53}""" - println(js.jsonToDelimited[Person]) - // Greg,53 -``` - -### Case 2: Map serialized object across the same wire format -```scala -import co.blocke.scalajack.Converters._ -case class Person(name: String, age: Int) - - val js = """{"name":"Greg","age":53}""" - println(js.mapJson[Person](person => person.copy(age=35))) - // {"name":"Greg","age":35} -``` - -### Case 3: Convert between wire formats while modifying serialized object -```scala -import co.blocke.scalajack.Converters._ -case class Person(name: String, age: Int) - - val sjYaml = ScalaJack(YamlFlavor()) - val js = """{"name":"Greg","age":53}""" - println(js.mapJsonTo[Person,YAML](sjYaml)(person => person.copy(age=35))) - // name: Greg - // age: 35 -``` - -### *Bonus:* to/from convenience implicits - -In addition to the normal ScalaJack read/render functions, the Converters package provides a set of implicit convenience functions -to perform the same functionality, for those who perfer this style. - -```scala -import co.blocke.scalajack.Converters._ - - trait Human - case class Person(name: String, age: Int) extends Human - - val js = """{"name":"Greg","age":53}""" - - val person = js.fromJson[Person] - val yamlWithHint = person.toYaml[Human] // trait generates type hint - val yamlWithoutHint = person.toYaml[Person] -``` - -For these convenience functions you always need to supply the type of the serialized object. - -#### Configuration - -The Converters can be configured just like the normal ScalaJack flavors: -```scala -import co.blocke.scalajack.Converters._ - - trait Human - case class Person(name: String, age: Int) extends Human - - val js = """{"name":"Greg","age":53}""" - - // Chain together configuration directives like any other flavor - val config = Configuration().withDefaultHint("kind").enumsAsInt() - withConfig(config) - - // Now any subsequent Converters activity will use your provided configuration -``` - - diff --git a/doc/mapname.md b/doc/mapname.md index a26b3496..ba6233ee 100644 --- a/doc/mapname.md +++ b/doc/mapname.md @@ -1,42 +1,18 @@ -## Change Field Names - -If you are using ScalaJack with 3rd party JSON or Mongo documents, you may be in a situation where you don't own or control the names of the fields. You may wish the field names of your classes to be different than the names in the 3rd party format. -ScalaJack provides the @Change annotation. Marking a field with @Change in your case class allows you to map its name to a different target name in JSON or Mongo. +## Change Field Names +If you are using ScalaJack with 3rd party JSON you may be in a situation where you don't own or control the names of fields. You may wish the field names of your classes to be different than the names in the 3rd party format. ScalaJack provides the Marking a field with a @Change annotation in your class allows you to change the names of fields in-flight to/from JSON. + ```scala -case class MapFactor( +case class MapFactor( @Change(name = "foo_bar") fooBar:String, - @Change(name = "a_b") thingy: Long, - count: Int, + @Change(name = "a_b") thingy: Long, + count: Int, @Change(name = "big_mac") bigMac:String ) ``` - If you serialize an instance of this class to JSON you'd get something like: - ```JSON {"foo_bar":"hey","a_b":25,"count":3,"big_mac":"hungry"} ``` - Notice that several of these field names are re-mapped to new values, presumably to match the format required by a 3rd party JSON provider. - -This works in Mongo too, including mixing it up with the @DBKey annotation: - -```scala -case class MapFactorId2( - @DBKey @Change(name = "foo_bar") fooBar:String, - @DBKey @Change(name = "a_b") thingy: Long, - @DBKey hey: Int, - count: Int, - @Change(name = "big_mac") bigMac: String -) -``` - -A serialized instance of this class might look like this: - -```json -{ "_id" : { "foo_bar" : "wonder", "a_b" : { "$numberLong" : "25" }, "hey" : 1 }, "count" : 3, "big_mac" : "hungry" } -``` - -Again you can see the @Change annotated fields had their name re-mapped to the specified values. diff --git a/doc/mongo.md b/doc/mongo.md deleted file mode 100644 index 9d9534a9..00000000 --- a/doc/mongo.md +++ /dev/null @@ -1,47 +0,0 @@ -## MongoDB Support - -ScalaJack doesn't wrap the MongoDB persistence libraries--that's not its mission. It does provide a way to convert classes to/from Mongo BsonDocuments. You'll need to include the mongo support package as shown in the Use section of the main Readme. Then you need to create a Mongo-specific ScalaJack object: - -```scala - import co.blocke.scalajack._ - import mongo._ - - val sjMongo = ScalaJack(MongoFlavor()) // produce a Mongo-flavored ScalaJack - val mydbo = sjMongo.render( myCaseClass ) - val myCaseClass = sjMongo.read[MyClass]( mydbo ) -``` - -The trait type hint label and value modifiers (.withAdapters, .withDefaultHint, .withHints) work like they do for JSON documents. - -### Keys - -You must specify your Mongo collection's keys using annotations in your Scala classes. Both single and compound keys are supported. - -```scala - case class OneKey( - @DBKey customerNum: String, - address: Address - ) - - case class TwoKeys( - @DBKey customerNum: String, - @DBKey countyCode: Int, - address: Address - ) - - case class UsingOID( - @DBKey myKey: ObjectID, - otherInfo: String - ) -``` - -ScalaJack will generate the appropriate key field(s) for Mongo using the DBKey annotation. - -### Documents - -ScalaJack's MongoDB module produces BsonDocument objects upon render -- specifically org.bson.BsonDocument. This BsonDocument type is also expected for read operations. - -> **Note:** Previous versions of ScalaJack produced the Scala driver's Document class, not the BsonDocument class. If you prefer, you can implement an implicit conversion like this: -> ```scala -> implicit def BsonDocument2Document(x: BsonValue) = new Document(x.asInstanceOf[BsonDocument]) -> ``` diff --git a/doc/neotype.md b/doc/neotype.md new file mode 100644 index 00000000..53e32b42 --- /dev/null +++ b/doc/neotype.md @@ -0,0 +1,69 @@ + +## NeoType Support + +ScalaJack now supports Kit Langton's [excellent neotype library](https://github.com/kitlangton/neotype). Neotype allows you to define validated types that ensure the integrity of the values they contain. + +By way of example let's define 3 String-related neotypes: + +**File 1.scala** +```scala +//----- A String that must not be empty +type NonEmptyString = NonEmptyString.Type +given NonEmptyString: Newtype[String] with +override inline def validate(input: String): Boolean = +input.nonEmpty + +//----- A String that must be empty +type EmptyString = EmptyString.Type +given EmptyString: Newtype[String] with +override inline def validate(input: String): Boolean = +input.isEmpty + +//----- A List of String that must be non-empty and the first list element must be "x" +type XList = XList.Type +given XList: Newtype[List[String]] with +override inline def validate(input: List[String]): Boolean = +input.nonEmpty && input(0) == "x" + +//----- A class to exercise these neotypes +case class Validated(name: NonEmptyString, xspot: XList, nada: EmptyString) +``` + +For this example, we're going to focus on fromJson(). The toJson() is presumed to always work because the neotype library won't let you create a Validated() object having invalid neotype values. So the real test here is to see if ScalaJack will correctly detect JSON that violates the defined field validations. + +**File 2.scala** +```scala +given sjValidated: ScalaJack[Validated] = sjCodecOf[Validated] + +val sj_ok = """{"name":"Mike","xspot":["x","other"],"nada":""}""" +val sj_brokenName = """{"name":"","xspot":["x","other"],"nada":""}""" +val sj_brokenNada = """{"name":"Mike","xspot":["x","other"],"nada":"boom"}""" +val sj_xspot = """{"name":"Mike","xspot":["y","other"],"nada":"boom"}""" + +sjValidated.fromJson(sj_ok) +// materializes Validated("Mike",List("x", "other"),"") + +try( sjValidated.fromJson(sj_brokenName) ) +catch { + case jpe: JsonParseError => println(jpe.show) +} +// NeoType validation for NonEmptyString failed at position [9] +// {"name":"","xspot:["x","other"],"nada":""} +// ---------^ + +try( sjValidated.fromJson(sj_brokenNada) ) +catch { + case jpe: JsonParseError => println(jpe.show) +} +// NeoType validation for EmptyString failed at position [49] +// {"name":"Mike","xspot":["x","other"],"nada":"boom"} +// -------------------------------------------------^ + +try( sjValidated.fromJson(sj_xspot) ) +catch { + case jpe: JsonParseError => println(jpe.show) +} +// NeoType validation for XList failed at position [42] +// {"name":"Mike","xspot":["nintendo","other"],"nada":""} +// ------------------------------------------^ +``` \ No newline at end of file diff --git a/doc/noncase.md b/doc/noncase.md index 8490ad53..c948f5da 100644 --- a/doc/noncase.md +++ b/doc/noncase.md @@ -1,11 +1,12 @@ + ## Non-Case and Java Classes -In the last section we saw how easily ScalaJack handles case classes and Scala traits. The Scala case class is a fantastic thing, and does a lot behind the scenes on your behalf, but if you follow certain conventions, ScalaJack can work with non-case and Java classes too. +In the last section we saw how easily ScalaJack handles case classes and Scala traits. If you follow certain conventions, ScalaJack can work with non-case and Java classes too. -The big challenge for ScalaJack is to know what the fields of a class are. In a case class this is easy: they're given in the constructor. In a non-case class they *may* be in the constructor--or not. We can't rely on this, so... +An important challenge for ScalaJack is to know what the fields of a class are. In a case class this is easy: they're given in the constructor. In a non-case class they *may* be in the constructor--or not. We can't rely on this, so... -### Scala Non-Case Class val constructor (most preferred method) +### Scala Non-Case Class val constructor If you have a non-case Scala class, and you specify 'val' for each of the constructor arguments, ScalaJack will treat it just as a case class. But... **all** the arguments but be specified with val! ```scala @@ -17,8 +18,8 @@ class Employer( name:String, val est:java.time.LocalDate ) ``` -### Scala Non-Case Class with getters and setters (ok method) -Another way ScalaJack can detect your class fields using standard Scala getter/setter format with a private var field: +### Scala Non-Case Class with getters and setters +ScalaJack can optionally detect your non-case class fields using standard Scala getter/setter format with a private var field: ```scala class Employer() { @@ -31,31 +32,10 @@ class Employer() { def est_=(e:LocalDate) = _est = e } ``` +When these getter/setter fields are detected they will be serialized, and when such a class is instantiated these fields will be set to their original values by calling the setters. -If you have any fields with either a getter or setter that you do *not* want serialized, put an @Ignore annotation on either the getter or setter and ScalaJack will ignore that field. - -### Scala Non-Case Class with public var (least preferred method) -A final way to specify your Scala class fields is to define them as public var fields, which is bad for the obvious reason of violating data hiding. ScalaJack finds these fields and treats them as class fields. - -```scala -class Employer() { - var name:String = "" - var est:LocalDate = LocalDate.now() - var profitMargin:Double = 21.0 -} -``` - -ScalaJack's auto-detection of all var fields means that *all* public var fields are treated as class members! This may not be what you want, so ScalaJack provides an @Ignore annotation to let you ignore var fields you don't want detected and treated as class members: - -```scala -class Employer() { - var name:String = "" - var est:LocalDate = LocalDate.now() - @Ignore var profitMargin:Double = 21.0 -} -``` - -As you know, exposing all var fields like this is very poor practice, so we strongly recommend against using the public vars method of field detection. +> If you have any fields with either a getter or setter that you do +> *not* want serialized, put an @Ignore annotation on either the getter or setter and ScalaJack will ignore that field. ### Default Values and Option With any of these methods it is possible to supply default values @@ -69,27 +49,9 @@ class Employer( val label:String = "ACME" ) { @Optional var num: Int = 5 } ``` -Here you can see the defaults "ACME", "Fred", and 5 supplied as defaults to the detectable class members. But what's that @Optional annotation? Scala requires a value be provided for non-constructor vars (or the class would be abstract). ScalaJack assumes all fields detected are required, so if a parsed class is missing a field, say name, it wouldn't know whether that's a missing-field error, or whether it should just use "Fred". The @Optional annotation tells ScalaJack that the field is optional in the parsed input, and if it's missing, just use the given value. - -So why not just declare @Optional fields with Scala Option? What happens if you have Option type value with a default? - -```scala - private var _name:Option[String] = Some("Fred") - def name: Option[String] = _name - def name_=(n:Option[String]) = _name = n -``` -In this case, if name was missing from the parsed input, ScalaJack would always read its value as None. What if I want the default Some("Fred")? Using @Optional will work: - -``` -```scala - private var _name:Option[String] = Some("Fred") - @Optional def name: Option[String] = _name - def name_=(n:Option[String]) = _name = n -``` -@Optional gives you more control over how fields are parsed. ### Java Class Support -ScalaJack's support for Java classes is more limited due to Java's looser handling of types and constructors. In order for Scala to detect your class fields your Java class must fit standard JavaBean notation for getters and setters (BeanInfo is used inside ScalaJack to detect your getters and setters): +ScalaJack's support for Java classes is more limited due to Java's looser handling of types and constructors. In order for Scala to detect your class fields your Java class must fit standard JavaBean notation for getters and setters (BeanInfo is used inside ScalaJack to detect your getters and setters). Your class must also specify a zero-argument constructor. ```java public class PlayerJava { @@ -102,4 +64,4 @@ public class PlayerJava { public void setAge(int a) { age = a; } } ``` -As with Scala fields, you can also use the @Ignore annotation on either a getter or setter if you don't want a JavaBean field serialized. +As with Scala fields, you can also use the @Ignore annotation on either a getter or setter if you don't want a JavaBean field serialized. \ No newline at end of file diff --git a/doc/nullAndNone.md b/doc/nullAndNone.md index 7510cc39..f3fc0f7d 100644 --- a/doc/nullAndNone.md +++ b/doc/nullAndNone.md @@ -1,24 +1,55 @@ + ## Null and None Handling -Representing nulls and None is problematic in JSON because while the spec provides for null it offers nothing to represent None. That forces us to sometimes make inconsistent, contextualized assumptions about how best to represent these concepts. Sadly, there is no perfect "right" answer here, so these are the compromises ScalaJack made. +Representing nulls and None is problematic in JSON because while the spec provides for null it offers nothing to represent None. That forces us to sometimes make inconsistent, contextualized assumptions about how best to represent these concepts. Sadly, there is no perfect "right" answer, so here we document the imperfect compromises ScalaJack made. -Note that in some cases ScalaJack's Null/None handling break the general promise that a read/render round-trip of a serialized object will result in the original object! +Note that in some cases ScalaJack's Null/None handling break the general promise that a read/render round-trip of a serialized object will result in the original object. This is because depending on the handling of Null/None, output may be skipped. -|Usage |Example|JSON Representation +|Usage |Example|JSON Representation |-------|-------|------------- -|Class member|```MyClass(None,3)```|{"age":3} *(eliminate None field)* +|Class member|```MyClass(None,3)```|{"age":3} *(eliminate None field)* |List|```List(Some(1),None,Some(3))```|[1,3] *(eliminate None value)* |Map value|```Map("a"->Some(1),"b"->None)```|{"a":1} *(eliminate None value)* |Map key|```Map(Some(1)->"a",None->"b",Some(2)->"c",None->"d")```|{"1":"a","2":"c"} *(eliminate None values)* |Tuple member|```(5,None,"hey")```|[5,null,"hey"] *(None converted to null)* -Tuples represent None as null as a compromise. We can't drop the value from the tuple because it has a fixed size that must be respected in the JSON. JSON can't handle an empty field, i.e. "[1,,3]", so the only choice left is to use null. - +Tuples represent None as null as a compromise. We can't drop the value from the tuple because it has a fixed size that must be respected in the JSON. JSON can't handle an empty field, i.e. "[1,\,3]", so the only choice left is to use null. + **Special Note** -If this isn't already confusing, it gets slightly more terrible. Parsing JSON null back into Scala is complicated by the type of field. -In a tuple, if the member's type is Option[], a null parses as None, otherwise it parses as null. +If this isn't already confusing, it gets slightly more terrible. Parsing JSON null back into Scala is complicated by the type of field. In a tuple, if the member's type is Option[], a null parses as None, otherwise it parses as null. Just be careful! You may not get exactly the object you expect. Clear as mud? + +**Option None and Null** +By default ScalaJack will omit None values in many contexts (see above chart). You can change that behavior and force None values to be converted to null by using a config policy: +```scala +given sjFoo: ScalaJack[Foo] = sjCodecOf[Foo](SJConfig().withNoneAsNull()) +``` + + +**Try Failure Value and Null** + +By default, ScalaJack will output null for a Try value of Failure. Alternative handling for Failure is possible via policy: + +Try Failure policies are: +* TryPolicy.AS_NULL -- output null upon Failure value (default behavior) +* TryPolicy.ERR_MSG_STRING -- put error string into JSON (**WARNING**: This may corrupt the data type, but is useful for debugging during development) +* TryPolicy.THROW_EXCEPTION -- throw an exception upon Failure value + +To set a different policy: +```scala +given sjFoo: ScalaJack[Foo] = sjCodecOf[Foo](SJConfig().withTryFailureHandling(TryPolicy.THROW_EXCEPTION)) +``` + +**Either Left Value and Null** +By default, Scalajack will output the actual value of an Either Left value. Alternative handling for Left is possible via policy: -Just be careful! You may not get exactly the object you expect. +Either Left policies are: +* EitherLeftPolicy.AS_VALUE -- output whatever the wrapped Left value is (default behavior) +* EitherLeftPolicy.AS_NULL - output null upon Left value +* EitherLeftPolicy.ERR_MSG_STRING -- put error string into JSON (**WARNING**: This may corrupt the data type, but is useful for debugging during development) +* EitherLeftPolicy.THROW_EXCEPTION -- throw an exception upon Failure value -Clear as mud? +To set a different policy: +```scala +given sjFoo: ScalaJack[Foo] = sjCodecOf[Foo](SJConfig().withEitherLeftHandling(EitherLeftPolicy.AS_NULL)) +``` diff --git a/doc/parameterized.md b/doc/parameterized.md index 18e4db0b..59b99ad7 100644 --- a/doc/parameterized.md +++ b/doc/parameterized.md @@ -1,43 +1,55 @@ + ## Parameterized Classes ScalaJack is able to handle parameterized classes and traits seamlessly: +**File1.scala** ```scala package com.me - -trait Vehicle[T]{ val transported:T } +sealed trait Vehicle[T]{ val transported:T } case class Car(transported:Person) extends Vehicle[Person] +``` -println(sj.render[Vehicle[Person]](Car(Person("Fred",25)))) -// {"_hint":"com.me.Car","transported":{"name":"Fred","age":25}} +**File2.scala** +```scala +given sjVehicle: ScalaJack[Vehicle[Person]] = sjCodecOf[Vehicle[Person]] +println(sjVehicle.toJson(Car(Person("Fred",25)))) +// {"_hint":"Car","transported":{"name":"Fred","age":25}} ``` Ok, now let's make it interesting.. +**File1.scala** ```scala -package com.me - object VehicleClass extends Enumeration { type VehicleClass = Value val Land, Air, Sea = Value } -import VehicleClass._ +import VehicleClass.* -trait Vehicle[K <: VehicleClass] { val kind: K } -case class Car(passengers: Int) extends Vehicle[Land.type] { val kind: Land.type = Land } +sealed trait Vehicle { val kind: VehicleClass } +case class Car(passengers: Int) extends Vehicle { val kind: Land.type = Land } -trait Hobby[X, Y] { val thing1: X; val thing2: Y } -trait Artist[W, Z] { val instrument: W; val effort: Z } -trait Person[X, Y] { val who: X; val org: Y } +sealed trait Hobby[X, Y] { val thing1: X; val thing2: Y } +sealed trait Artist[W, Z] { val instrument: W; val effort: Z } +sealed trait Person[X, Y] { val who: X; val org: Y } case class Sports[A, B](thing1: A, thing2: B) extends Hobby[A, B] case class Painter[A, B](instrument: A, effort: B) extends Artist[A, B] case class Employee[A, B, C, D](who: Artist[C, Hobby[D, A]], org: B) extends Person[Artist[C, Hobby[D, A]], B] +type ComplexPerson = Person[Artist[Int, Hobby[Double, Char]], Vehicle] +``` + +**File2.scala** +```scala -val inst: Person[Artist[Int, Hobby[Double, Char]], Vehicle[_ <: VehicleClass]] = Employee(Painter(5, Sports(1.2, 'Z')), "wow") -val js = sj.render(inst) -// {"_hint":"com.me.Employee","who":{"_hint":"com.me.Painter","instrument":5,"effort":{"_hint":"com.me.Sports","thing1":1.2,"thing2":"Z"}},"org":{"_hint":"com.me.Car","passengers":4}} +given sjPerson: ScalaJack[ComplexPerson] = sjCodecOf[ComplexPerson] +val inst: ComplexPerson = Employee(Painter(5, Sports(1.2, 'Z')), Car(4)) +val js = sjPerson.toJson(inst) -val orig = sj.read[Person[Artist[Int, Hobby[Double, Char]], Vehicle[_ <: VehicleClass]]](js) +// {"_hint":"Employee","who":{"_hint":"Painter","instrument":5,"effort":{"_hint":"Sports","thing1":1.2,"thing2":"Z"}},"org":{"_hint":"Car","passengers":4}} + +sjPerson.fromJson(js) // re-constitutes inst ``` -Clearly this is a contrived (and forced) example, but it does show that complex nesting relationships between parameterized types work just fine, even when we threw in some traits and an Enumeration for good measure. Remember that you always have control of ScalaJack's reading and rendering. You can force a more general trait (will generate type hints) or use a specific concrete class (won't need type hints). + +Clearly this is a contrived example, but it does show that complex nesting relationships between parameterized types work just fine, even when we threw in some traits and an Enumeration for good measure. Remember that you always have control of ScalaJack's reading and rendering. You can force a more general trait (will generate type hints) or use a specific concrete class (won't need type hints). \ No newline at end of file diff --git a/doc/parseOrElse.md b/doc/parseOrElse.md deleted file mode 100644 index d53ae003..00000000 --- a/doc/parseOrElse.md +++ /dev/null @@ -1,63 +0,0 @@ - -## ParseOrElse and Cascading Fallback Parsing - -Sometimes you're not 100% sure if you can actually parse a given object or not. For example, let's imagine code you wrote is part of a large, distributed message-passing system. Your program listens for messages (each corresponding to a case class). Let's further imagine that all messages in this ecosystem derive from trait ActionMessage and that your code is designed to listen for SortAction messages. The problem is... you may receive other kinds of messages--messages you don't care about or know what to do with. You may not even have the jar file containing code to deserialize these other messages. - -How will you even deserialize an incoming message to determine if it's one of yours? That's a problem. - -This is the scenario parseOrElse() was designed for: attempt to parse JSON (presumably into a trait). If successful very well, but if you can't parse (perhaps because you don't have a class file for the given type) then return a given default object. - -```scala -package com.mycompany - -trait ActionMessage -case class Wrapped(id:Long, src:String, msg:ActionMessage) -case class SortAction(isDescending: Boolean) extends ActionMessage -case class DefaultAction() extends ActionMessage // some do-nothing ActionMessage we own - -val sj = ScalaJack().parseOrElse(typeOf[ActionMessage] -> typeOf[DefaultAction]) - -val js = """{"id":123,"src":"main","msg":{"_hint":"com.mycompany.SpecialAction","contact":"fred"}}""" -val myAction = sj.read[Wrapped](js) -``` - -Presuming we have no code for class SpecialAction, when this Wrapped object is read ScalaJack will consult the ParseOrElse lookup table and see that for an unknown ActionMessage it should return a DefaultAction object. - -parseOrElse takes a mapping Type->Type, where the first argument is the type of the trait to be parsed and the second argument is the type of the default object if the trait's type hint is unknown. - -### Cascading Fallback - -parseOrElse() can be cascaded as in this example: - -```scala -package com.mycompany - -trait ActionMessage { val priority: Int } -case class Wrapped(id: Long, src: String, msg: ActionMessage) - -case class SortAction(priority: Int, isDescending: Boolean) extends ActionMessage -case class DefaultAction(priority: Int) extends ActionMessage with SJCapture // some do-nothing ActionMessage we own -case class UnknownMessage() extends ActionMessage with SJCapture { - val priority = -1 -} - -val sj = ScalaJack() - .parseOrElse( - typeOf[ActionMessage] -> typeOf[DefaultAction], - typeOf[DefaultAction] -> typeOf[UnknownMessage] - ) - -val js = - """{"id":123,"src":"main","msg":{"_hint":"com.mycompany.SpecialAction","priority":2,"contact":"fred"}}""" -val myAction = sj.read[Wrapped](js) // Produces DefaultAction - -val js2 = // (missing required priority field for ActionMessage) - """{"id":123,"src":"main","msg":{"_hint":"com.mycompany.SpecialAction","contact":"fred"}}""" -val myAction2 = sj.read[Wrapped](js2) // Produces UnknownMessage -``` - -In this case, as before, we attempt to deserialize a class, SpecialAction, for which we have no jar file. As before we attempt to fall back to deserializing a DefaultAction, but in this case that fails too, because the required 'priority' field is missing from the second input (js2). In this case we fall back again and create an UnknownMessage. - -As an added flourish we've mixed in SJCapture so that all the data sent in the JSON is at least captured, even if we don't known what to do with it. That's purely optional, of course, if you need that data. - -The intention of parseOrElse() along with the cascading fallback pattern is that you shouldn't need a nest of Exception handlers. You should be able to always have your parse produce something rational and useful. \ No newline at end of file diff --git a/doc/tryAndCapture.md b/doc/tryAndCapture.md deleted file mode 100644 index 44fe4701..00000000 --- a/doc/tryAndCapture.md +++ /dev/null @@ -1,74 +0,0 @@ -## Try and Capture - -There are times when you just don't care. Try and Capture support is designed to let you control how much you care about the input you parse. - -### Case 1: I care about some fields and not at all about the rest! -In this case, simply don't represent JSON fields you never care about in your class. Parsing will harmlessly ignore them. This also has the benefit of being very flexible if the source JSON changes--as long as they don't change the fields you care about nothing in your system breaks. -```scala -val js = """{"one":"Thing","two":"Another thing","three":"Last thing"}""" -case class ThingsICareAbout(one:String, three:String) // field two is ignored -``` -Note that fields ignored on read, because they're not present in your class, will not be output upon render. (If you don't care about certain fields, but don't want to lose them on render ScalaJack has a mechanism to preserve this knowledge we'll see shortly.) - -### Case 2: I have an optional field that I care about if it exists. -In this case, represent optionally-present fields as Option type in your class. If they're present in the JSON, they'll be materialized as Some of something, and None if not. -```scala -val js = """{"one":"Thing","three":"Last thing"}""" -case class ThingsICareAbout(one:String, two:Option[String], three:String) -// materializes: ThingsICareAbout("Thing",None,"LastThing") -``` -In this case Option fields who's value is None will not be rendered, but if their value is Some(something) then output will be rendered. - -### Case 3: There's a field that may or may not parse but I want to keep it -This case is for when you get unreliable JSON, likely from a 3rd party. In that JSON is a field you expect and require to be present (i.e. it's not optional) but you're not 100% sure of its content or proper formatting. If it can parse you want the value, but if not... you want to keep the original JSON intact as-given, because you have a requirement to re-generate the original message format. - -An example of this case is if your code is some kind of proxy that receives JSON, makes certain changes and re-emits a modified version of the original JSON. - -In this instance we support a Try: - -```scala -val js = """{"one":"Thing","two":"another thing","three":"Last thing"}""" - -case class ThingsICareAbout(one:String, two:Try[String], three:String) // <-- Note the Try! - -// materializes: ThingsICareAbout("Thing",Success("another thing"),"LastThing") - -val js2 = """{"one":"Thing","two":true,"three":"Last thing"}""" -val myObj = sj.read[ThingsICareAbout](js2) - -/* -Oops! Materializes: -ThingsICareAbout(Thing,Failure(co.blocke.scalajack.typeadapter.ValueBackedException: [$.two]: Expected String here but found Boolean -{"one":"Thing","two":true,"three":"Last thing"} -------------------------^),Last thing) -*/ - -// -// but... -val rerendered = sj.render(myObj) -// emits: {"one":"Thing","two":true,"three":"Last thing"} -``` - -Wow! Did you catch that? Parsing a Try captured, but didn't throw, the ValueBackedException (we fed a boolean value into a String). Behind the scenes, ScalaJack also captured the original JSON, so that even though we couldn't parse it into our class, when the object was rendered, the original JSON was put back into place! - -The assumption here is that we're getting 3rd party JSON. Maybe types change unexpectedly and we can't read them but we don't want to screw things up for others in the ecosystem that perhaps do expect the given type. - -### Case 4: I don't care about any "extra" fields in the JSON, but please don't lose them! -This case is along the lines of Case 3 except that here we don't care about any fields we're not going to use. For the same reasons (maybe our code is a proxy or pass-through) we may need to re-render all those "don't-care" fields in their original format. - -We accomplish this behavior like this: - -```scala -case class ThingsICareAbout(one:String) extends SJCapture // <-- Note the extends SJCapture - -val js = """{"one":"thing","id":1234,"isOK":true}""" -val myObj = sj.read[ThingsICareAbout](js) -// myObj = ThingsICareAbout("thing") - -println(sj.render(myObj)) -// prints: {"one":"thing","id":1234,"isOK":true} -``` - -Because your class extends SJCapture, all the "extra" incoming JSON fields are quietly captured and stored until the object is rendered. - -This is ideal for pass-through applications where the JSON formats are unstable or you are only concerned about a subset of fields as you pass through the object. Your code won't break if changes happen to any of the extra, captured fields. diff --git a/doc/typeHint.md b/doc/typeHint.md index 71c6a7c3..31d0cfce 100644 --- a/doc/typeHint.md +++ b/doc/typeHint.md @@ -1,91 +1,58 @@ -## Trait Hint Type Customization -By default, ScalaJack uses a simple type hint strategy to record concrete types for traits: it inserts a type hint into the rendered JSON object with key "_hint" and value equal to the fully-qualified class name. +## Trait Hint Type Customization -This is a pretty good and straightforward way to handle type hints, but there are occasions when you may want something else. For example when you're dealing with 3rd party JSON. In those cases you may not control, or wish to share, the full class name of your objects. +By default, ScalaJack uses a simple type hint strategy to record concrete types for sealed traits: it inserts a type hint into the rendered JSON object with key "_hint" and value equal to the class simple class name. -Here are 3 ways you can customize trait type hint handling in ScalaJack. +This is a pretty good and straightforward way to handle type hints, but there are occasions when you may want something else. Here are several ways you can customize trait type hint handling in ScalaJack. ### Change the Default Hint Label + You can change the global default hint label from "_hint" to whatever else you want using a ScalaJack configuration. +**File1.scala** ```scala -package com.me - -trait Foo{ val bar:Int } -case class Blather(bar:Int) extends Foo - -val sj = ScalaJack().withDefaultHint("kind") //<<-- Note the config here - -println(sj.render[Foo](Blather(2))) -// {"kind":"com.me.Blather","bar":2} //<<-- Type hint is now "kind" +sealed trait Foo{ val bar:Int } +case class Blather(bar:Int) extends Foo ``` -You'll see the normal _hint is replaced by "kind". - -### Class-Specific Hint Label Customization -This is a more granular control of the first case, which allows you to change the type hint on a class-by-class basis (you can still override the default case for classes not mapped using this method): +**File2.scala** ```scala -package com.me -import scala.reflect.runtime.universe.typeOf - -trait Foo{ val a:Int } -trait Bar{ val b:Int; val c:Foo } -case class One(a:Int) extends Foo -case class Two(b:Int, c:Foo) extends Bar - -val sj = ScalaJack().withHints((typeOf[Foo] -> "mark")).withDefaultHint("kind") - -val inst:Bar = Two(3, One(2)) -println(sj.render(inst)) -// {"kind":"com.me.Two","b":3,"c":{"mark":"com.me.One","a":2}} +given sjFoo: ScalaJack[Foo] = sjCodecOf[Foo](SJConfig().withTypeHintLabel("kind")) +println(sjFoo.toJson(Blather(2))) +// {"kind":"Blather","bar":2} //<<-- Type hint is now "kind" and not "_hint" ``` -Here we've shown a type-specific change to the type hint and a general override of the default type hint. - -(Hint: typeOf[SomeClass] is an easy way to get the type of something. This works for parameterized types too, e.g. typeOf[Vehicle[Person]].) ### Type Hint Value Customization -We've seen how to change the type hint label, but what if you want to modify the behavior of using the fully-qualified class name to use another value instead? This is ideal when you don't want to expose the class name in JSON or 3rd party consumers of your output expect something else. ScalaJack has the concept of a HintValueModifier for this purpose. -ScalaJack suppies two pre-built HintValueModifiers out of-the-box: ClassNameHintModifier and StringMatchHintModifier. (You are certainly not limited to just these two--you can write your own HintValueModifiers to do whatever you want, but these cover some common use cases.) - -The ClassNameHintModifier re-writes the class name itself. You pass in 2 functions: the first accepts a simple hint string and your function produces a fully-qualified class name, and the second accepts the fully-qualified class name and produces the simple hint string. +We've seen how to change the type hint label, but what if you want something different than the class' simple name as the value? There are two possible use cases for wanting to change the hint value. First, having the class name in the JSON message might be more information than you'd like to share. ScalaJack provides a way to encode the class name into a value that's not so obvious. This isn't security, by any means, but it is obfuscation and should at least deter curious idiots. (If what you need is true security, you should be encrypting the entire JSON message anyway.). +**File1.scala** ```scala -val prependHintMod = model.ClassNameHintModifier((hint: String) => "com.me." + hint, (cname: String) => cname.split('.').last) -val sj = ScalaJack().withHintModifiers((typeOf[Foo], prependHintMod)) -val inst:Bar = Two(3, One(2)) -println(sj.render(inst)) -// {"_hint":"com.me.Two","b":3,"c":{"_hint":"One","a":2}} +sealed trait Foo{ val bar:Int } +case class Blather(bar:Int) extends Foo ``` -We've first used ClassNameHintModifier to specify 2 functions: value->classname, classname->value. In our case it adds/strips the packages from the classname leaving just the last part of the class path, or "One" in this case. Reading re-inserts the path: "One" -> "com.me.One". Then we've associated that modifier to a particular type, Foo in our case. - -The other provided HintValueModifier is StringMatchHintModifier: - +**File2.scala** ```scala -val strMatchHintMod = model.StringMatchHintModifier(Map("Yes" -> typeOf[One])) -val sj = ScalaJack().withHintModifiers((typeOf[Foo], strMatchHintMod)) -val inst:Bar = Two(3, One(2)) -println(sj.render(inst)) -// {"_hint":"com.me.Two","b":3,"c":{"_hint":"Yes","a":2}} +given sjFoo: ScalaJack[Foo] = sjCodecOf[Foo]( + SJConfig().withTypeHintLabel("ref").withTypeHintPolicy(TypeHintPolicy.SCRAMBLE_CLASSNAME)) +println(sjFoo.toJson(Blather(2))) +// {"ref":"86999-847-46A","bar":2} //<<-- Type hint is now obfuscated--looks like some kind of id ``` -In this example we mapped the type of the concrete class to some string value (vs modifying the text of the class name itself). Note a key point here--The StringMatchHindModifier is a map of String->*concrete class type*. - -### Pulling it Together -Using these hint modification methods together is where the power becomes apparent. A great case for this is when you're parsing 3rd party JSON you don't control. If there's a "variant" object that could take one of several concrete forms there's bound to be some field that behaves as a discriminator. That field will likely not be labeled "_hint", and the value will not be a class name. +The other use case for modifying type hint values is to support 3rd party JSON that has its own discriminator values that may be different from your class names. To support that, ScalaJack uses annotations on the class. -In that case a StringMatchHintModifier is a great choice, along with a .withHints mapping so you can wrangle what you're given into what you need to read the correct concrete class in your code. +**File1.scala** +```scala +sealed trait Foo{ val bar:Int } +@TypeHint(hintValue = "yammer") +case class Blather(bar:Int) extends Foo +``` +**File2.scala** ```scala -val cardHints = StringMatchHintModifier(Map("ace" -> typeOf[PlayerAce], "king" -> typeOf[PlayerKing])) -val sizeHints = StringMatchHintModifier(Map("small" -> typeOf[Ant], "big" -> typeOf[Elephant])) -val sj = ScalaJack() - .withHintModifiers((typeOf[Card] -> cardHints), (typeOf[Animal] -> sizeHints)) - .withHints((typeOf[Card] -> "card"),(typeOf[Animal] -> "size")) -val inst:Animal = Elephant(3, PlayerAce(2)) -println(sj.render(inst)) -// {"size":"big","b":3,"c":{"card":"ace","a":2}} +given sjFoo: ScalaJack[Foo] = sjCodecOf[Foo](SJConfig().withTypeHintPolicy(TypeHintPolicy.USE_ANNOTATION)) +println(sjFoo.toJson(Blather(2))) +// {"_hint":"yammer","bar":2} //<<-- Type hint for Blather is now "yammer", by annotation ``` -Wow! Now there's nothing left in the rendered (and read) JSON that remotely looks like a normal ScalaJack type hint. Both the labels and the values have been modified, which is entirely suitable for 3rd party JSON. +When using TypeHintPolicy.USE_ANNOTATION, any classes that do not have a @TypeHint annotation will simply revert to the simple class name scheme, so you can mix and match as needed. \ No newline at end of file diff --git a/doc/union.md b/doc/union.md index 57b4a6e9..ea54f519 100644 --- a/doc/union.md +++ b/doc/union.md @@ -1,25 +1,25 @@ -## Union Types -JSON can be messy. Very messy. Take Avro schema file definitions, for example. An Avro "type" field can have 1 of 4 possible values: -1) An ennumeration value of a simple type -2) A symbol (string) of a named complex type -3) An embedded complex type (JSON object) -4) A union of a list of any of the above -Great, eh? Definitely not something easy to deserialize. Ideally, Scala would have built-in support for union types, or a type that can have 1 of several types of values. Several 3rd party libraries, e.g. Cats, glom this feature into Scala with varying degrees of success. The problem is that because these are not language-native ScalaJack's reflection engine doesn't know how to unpack them. +## Union Types -The solution is the ScalaJack Union type, which is a caveman-primitive implementation consisting of several typed fields, all of which are optional, and only one specified. Yuck, right? +JSON can be messy. Very messy. Although it is terrible practice, 3rd party JSON of questionable discipline can specify fields that can hold more than one data type. For example let's assume we get JSON like this from a 3rd party: -Looks like this: +```json +{"id":"12345", "category":"linear", "count": 3} +{"id":"12345", "category":["linear", "concentric"], "count": 3} +``` +In this example, the category field can either be a String or a List[String]. We can use ScalaJack's support of Scala 3's Union type to solve this problem. +**File 1.scala** ```scala -import co.blocke.scalajack.Union3 -case class Multi3(one: Union3[List[String], List[Int], Boolean]) -val m3 = Multi3(Union3(None,Some(List(1,2,3)),None)) +case class VendorRecord(id: String, category: String | List[String], count: Int) ``` -In this case we have a Union3, which can be of type List[String], List[Int], or Boolean. - -ScalaJack defines 3 Union types: Union2, Union3, and Union4. +**File 2.scala** +```scala +given sjVendor: ScalaJack[VendorRecord] = sjCodecOf[VendorRecord] -Not an elegant solution at all, but it does work if you have 3rd party JSON that may be well-formed, but that doesn't follow the strict rules of typed object serialization. +sjVendor.fromJson("""{"id":"12345", "category":"linear", "count": 3}""") +// materializes VendorRecord("12345","linear",3) -> Call For Proposal: If anyone can suggest or contribute a more elegant way of accomplishing this goal, please submit a PR! +sjVendor.fromJson("""{"id":"12345", "category":["linear", "concentric"], "count": 3}""" ) +// materializes VendorRecord("12345",List("linear","concentric"),3) +``` \ No newline at end of file diff --git a/doc/valueClass.md b/doc/valueClass.md index 8f0217c6..2c0d9d97 100644 --- a/doc/valueClass.md +++ b/doc/valueClass.md @@ -1,19 +1,19 @@ + ## Value Class Support ScalaJack supports value classes seamlessly. + **File1.scala** ```scala -case class UUID_VC(underlying: UUID) extends AnyVal - -val u = UUID_VC(UUID.randomUUID) -val js = sj.render(u) -println(js) -// prints: "85c80eb0-5973-4938-abb7-b29b962531ca" - -println(sj.read[UUID_VC](js)) -// prints: UUID_VC("85c80eb0-5973-4938-abb7-b29b962531ca") +case class UUID_VC(underlying: UUID) extends AnyVal ``` +**File2.scala** +```scala +given sjUUID: ScalaJack[UUID_VC] = sjCodecOf[UUID_VC] +val u = UUID_VC(UUID.randomUUID) +val js = sjUUID.toJson(u) +println(js) // prints: "85c80eb0-5973-4938-abb7-b29b962531ca" +println(sjUUID.fromJson(js)) // prints: UUID_VC("85c80eb0-5973-4938-abb7-b29b962531ca") +``` You can see here that the wrapping/unwrapping of the value class is handled by ScalaJack and from a JSON perspective the value is treated as though it was never wrapped at all. - -Note that if you want custom read/render handling for your underlying type, the process for customization is the same as for the naked type. In other words the fact that the type is wrapped in a value class is immaterial to the customized handling. diff --git a/doc/viewSplice.md b/doc/viewSplice.md deleted file mode 100644 index a14adcdb..00000000 --- a/doc/viewSplice.md +++ /dev/null @@ -1,38 +0,0 @@ -## View / SpliceInto - -The use case for view/spliceInto is where you have a master class, which may contain some system-private information. We may also need a 'lite' version of the master class for transport to a UI, but the lite version must not contain any of the private information. We want the lite version to be a "view" (projection) of the master class. If the UI modifies the lite version, we want to splice its changes back into the master. - -First the classes: -```scala -case class Master( - name: String, - id: Long, - underwearSize: Char, - favorites: List[String] - ) - -case class Subset( - name: String, - id: Long, - favorites: List[String] - ) -``` - -Here we have a Subset class that doesn't include the person's underwearSize. Now let's see how to project that view: - -```scala -val master = Master("Fred",123L, 'M', List("music","football")) -val subset = sj.view[Subset](master) -// subset = Subset("Fred", 123L, List("music","football")) -``` - -We now have a Subset object that's "safe" to transport. - -Now let's assume that something has modified Subset and we want to recombine it back with Master. We can do it this way: - -```scala -val newMaster = sj.spliceInto(modifiedSubset, master) -``` - -Easy, right? - diff --git a/doc/yaml.md b/doc/yaml.md deleted file mode 100644 index 7f3e6f8b..00000000 --- a/doc/yaml.md +++ /dev/null @@ -1,21 +0,0 @@ -## YAML Support - -YAML is now supported as a flavor for ScalaJack, so you can serialize your Scala artifacts directly to/from YAML documents. - -All the normal trait-handling, type hints, modifiers, etc. that apply in JSON apply in YAML too, so you lose no flexibility. - -With YAML you gain the ability to natively support non-String (i.e. complex) Map keys. ScalaJack's JSON flavor allowed this too, but required serializing the non-String key into a String, which frankly is a bit messy. No need for that anymore with YAML! - -One thing that is different for YAML is there are no permissive primitives settings (.withPermissivePrimitives), for example "true" = true, "123" = 123. In YAML, permissive primitive behavior is default unless you quote your values. - -Usage is as you'd expect: -```scala -import co.blocke.scalajack.yaml._ - -case class Person(...) - -val sj = ScalaJack(YamlFlavor()) -sj.render(Person("Fred",34)) -val myPerson = sj.read[Person](someYaml) -``` -One limitation: ScalaJack's YAML parser does not handle multi-document YAML input, so basically one top-level class is parsed. If that is a huge need, open an issue and we'll see what we can do. \ No newline at end of file diff --git a/dynamodb/src/main/scala/co.block.scalajack/dynamodb/DynamoFlavor.scala b/dynamodb/src/main/scala/co.block.scalajack/dynamodb/DynamoFlavor.scala deleted file mode 100644 index 206bc9b1..00000000 --- a/dynamodb/src/main/scala/co.block.scalajack/dynamodb/DynamoFlavor.scala +++ /dev/null @@ -1,139 +0,0 @@ -package co.blocke.scalajack -package dynamodb - -import model._ -import typeadapter._ -import co.blocke.scala_reflection.RType - -import scala.jdk.CollectionConverters._ -import com.amazonaws.services.dynamodbv2.document.Item -import com.amazonaws.services.dynamodbv2.model.{ - AttributeDefinition, - CreateTableRequest, - KeySchemaElement, - KeyType, - ProvisionedThroughput, - ScalarAttributeType -} - -case class DynamoFlavor( - override val defaultHint: String = "_hint", - override val permissivesOk: Boolean = false, - override val customAdapters: List[TypeAdapterFactory] = List.empty[TypeAdapterFactory], - override val hintMap: Map[String, String] = Map.empty[String, String], - override val hintValueModifiers: Map[String, HintValueModifier] = Map.empty[String, HintValueModifier], - override val typeValueModifier: HintValueModifier = DefaultHintModifier, - override val parseOrElseMap: Map[Class[_], RType] = Map.empty[Class[_], RType], - override val enumsAsInt: Boolean = false -) extends JackFlavor[Item] { - - // $COVERAGE-OFF$Not testing any of this stuff. Either unused for Dynamo or an exact copy of thoroughly-tested JSON flavor - override val stringifyMapKeys: Boolean = true - - def stringWrapTypeAdapterFactory[T]( - wrappedTypeAdapter: TypeAdapter[T], - emptyStringOk: Boolean = true - ): TypeAdapter[T] = ??? - - def maybeStringWrapTypeAdapterFactory[T]( - wrappedTypeAdapter: TypeAdapter[T], - emptyStringOk: Boolean = true - ): TypeAdapter[T] = ??? - - def allowPermissivePrimitives(): JackFlavor[Item] = - this.copy(permissivesOk = true) - def enumsAsInts(): JackFlavor[Item] = this.copy(enumsAsInt = true) - def parseOrElse(poe: (RType, RType)*): JackFlavor[Item] = - this.copy(parseOrElseMap = this.parseOrElseMap ++ poe.map{(p,oe) => p.infoClass->oe}) - def withAdapters(ta: TypeAdapterFactory*): JackFlavor[Item] = - this.copy(customAdapters = this.customAdapters ++ ta.toList) - def withDefaultHint(hint: String): JackFlavor[Item] = - this.copy(defaultHint = hint) - def withHints(h: (RType, String)*): JackFlavor[Item] = - this.copy(hintMap = this.hintMap ++ h.map{(rt,hint) => rt.name->hint}) - def withHintModifiers(hm: (RType, HintValueModifier)*): JackFlavor[Item] = - this.copy(hintValueModifiers = this.hintValueModifiers ++ hm.map{(rt,hintM) => rt.name->hintM}) - def withTypeValueModifier(tm: HintValueModifier): JackFlavor[Item] = - this.copy(typeValueModifier = tm) - - def parse(input: Item): Parser = ??? - // $COVERAGE-ON$ - - // Embedded JSON-flavored ScalaJack, as Item can read/write JSON, so this is actually the most straightforward - // path to serialization. - lazy val sj: JackFlavor[json.JSON] = { - val baseSj = ScalaJack() - .withAdapters(customAdapters: _*) - .withHints(hintMap.map{ case (k,v) => (RType.of(Class.forName(k)),v) }.toList: _*) - .withHintModifiers(hintValueModifiers.map{ case (k,v) => (RType.of(Class.forName(k)),v)}.toList: _*) - .withDefaultHint(defaultHint) - .parseOrElse(parseOrElseMap.map{ case(k,v) => (RType.of(k),v)}.toList: _*) - baseSj.withTypeValueModifier(typeValueModifier) - } - - private val jsonWriter = json.JsonWriter() - - def _read[T](input: Item, typeAdapter: TypeAdapter[T]): T = - // sj.read[T](input.toJSON.asInstanceOf[json.JSON]) - val parser = json.JsonParser(input.toJSON.asInstanceOf[json.JSON], sj) - typeAdapter.read(parser).asInstanceOf[T] - - def _render[T](t: T, typeAdapter: TypeAdapter[T]): Item = - val sb = StringBuilder[json.JSON]() - typeAdapter.write(t, jsonWriter, sb) - Item.fromJSON(sb.result().asInstanceOf[String]) - // Item.fromJSON(sj.render[T](t).asInstanceOf[String]) - /* - def write[WIRE]( - t: T, - writer: Writer[WIRE], - out: mutable.Builder[WIRE, WIRE]): Unit - */ - - // This is Dynamo-Only. User will have to case ScalaJack to DynamoFlavor to call this. - // Yeah, this is a little large-ish for an inlined call, but it *MUST* be inlined for [T] - // to be known/visible, otherwise it will be (incorrectly) interpreted as Any! - inline def createTableRequest[T](provisionedThroughput: ProvisionedThroughput): CreateTableRequest = { - val (optionalTableName, keys, className) = taCache.typeAdapterOf[T] match { - case ta: classes.ClassTypeAdapterBase[_] => (ta.dbCollectionName, ta.dbKeys, ta.info.name) - } - val tableName = optionalTableName.getOrElse( - throw new java.lang.IllegalStateException( - s"Class ${className} must be annotated with @Collection to specify a table name." - ) - ) - - val cleanKeys = keys.map(_.asInstanceOf[ClassFieldMember[_, _]]) - - if (cleanKeys.isEmpty) - throw new java.lang.IllegalStateException( - s"Class ${className} must define at least a primary key with @DBKey." - ) - - val attrDetail = cleanKeys.zipWithIndex.collect { - case (key, idx) if idx == 0 => - ( - new AttributeDefinition(key.name, getAttrType(key)), - new KeySchemaElement(key.name, KeyType.HASH) - ) - case (key, idx) if idx == 1 => - ( - new AttributeDefinition(key.name, getAttrType(key)), - new KeySchemaElement(key.name, KeyType.RANGE) - ) - } - - new CreateTableRequest( - attrDetail.map(_._1).asJava, - tableName, - attrDetail.map(_._2).asJava, - provisionedThroughput - ) - } - - private def getAttrType(key: ClassFieldMember[_, _]) = - if (key.valueTypeAdapter.isStringish) - ScalarAttributeType.S - else - ScalarAttributeType.N -} \ No newline at end of file diff --git a/dynamodb/src/test/scala/co.blocke.scalajack/TestUtil.scala b/dynamodb/src/test/scala/co.blocke.scalajack/TestUtil.scala deleted file mode 100644 index c8365bdf..00000000 --- a/dynamodb/src/test/scala/co.blocke.scalajack/TestUtil.scala +++ /dev/null @@ -1,42 +0,0 @@ -package co.blocke.scalajack - -import munit.internal.console - -object TestUtil { - - inline def describe(message: String, color: String = Console.MAGENTA): Unit = println(s"$color$message${Console.RESET}") - inline def pending = describe(" << Test Pending (below) >>", Console.YELLOW) - - def hexStringToByteArray(s: String): Array[Byte] = { - val len = s.length - val data = new Array[Byte](len / 2) - var i = 0 - while ({ - i < len - }) { - data(i / 2) = ((Character.digit(s.charAt(i), 16) << 4) + Character.digit( - s.charAt(i + 1), - 16 - )).toByte - - i += 2 - } - data - } - - // Utility to generate test code quickly - def showException(label: String, fnStr: String, fn: () => Any) = - try { - fn() - } catch { - case x: IndexOutOfBoundsException => throw x - case t: Throwable => - if (!t.getMessage.contains("\n")) - throw t - val msg = "\"\"\"" + t.getMessage().replace("\n", "\n |") + "\"\"\"" - println( - label + " >> " + t.getClass.getName + "\n-----------------------\n" + - s"val msg = $msg.stripMargin\nthe[${t.getClass.getName}] thrownBy $fnStr should have message msg\n" - ) - } -} \ No newline at end of file diff --git a/dynamodb/src/test/scala/co.blocke.scalajack/dynamodb/Basics.scala b/dynamodb/src/test/scala/co.blocke.scalajack/dynamodb/Basics.scala deleted file mode 100644 index 0572de83..00000000 --- a/dynamodb/src/test/scala/co.blocke.scalajack/dynamodb/Basics.scala +++ /dev/null @@ -1,145 +0,0 @@ -package co.blocke.scalajack -package dynamodb - -import model._ -import co.blocke.scala_reflection.RType - -import TestUtil._ -import munit._ -import munit.internal.console - -import com.amazonaws.services.dynamodbv2.document.Item - -class Basics extends FunSuite: - - val sj = ScalaJack(DynamoFlavor()) - - test("Basic class with embedded class") { - describe( - "--------------------------------\n: DynamoDB Basic Value Tests :\n--------------------------------", Console.BLUE - ) - describe("Standard Serialization:") - val inst: Person = Person( - "Greg", - 50, - List("Woodworking", "Diet Coke"), - Misc(1.23, "boom"), - Some(true) - ) - val item = sj.render(inst) - assertEquals( - """{ Item: {name=Greg, age=50, likes=[Woodworking, Diet Coke], stuff={wow=1.23, bing=boom}, foo=true} }""", - item.toString - ) - assertEquals(inst, sj.read[Person](item)) - } - - test("Optional default = None") { - val inst: Person = Person( - "Greg", - 50, - List("Woodworking", "Diet Coke"), - Misc(1.23, "boom") - ) - val item = sj.render(inst) - assertEquals( - """{ Item: {name=Greg, age=50, likes=[Woodworking, Diet Coke], stuff={wow=1.23, bing=boom}} }""", - item.toString - ) - assertEquals(inst, sj.read[Person](item)) - } - - test("Trait support") { - val inst: Human = Person( - "Greg", - 50, - List("Woodworking", "Diet Coke"), - Misc(1.23, "boom"), - Some(false) - ) - val item = sj.render(inst) - assertEquals( - """{ Item: {_hint=co.blocke.scalajack.dynamodb.Person, name=Greg, age=50, likes=[Woodworking, Diet Coke], stuff={wow=1.23, bing=boom}, foo=false} }""", - item.toString - ) - assertEquals(inst, sj.read[Human](item)) - } - - test("Custom type adapter") { - describe("Extended Serialization:") - val sj = ScalaJack(DynamoFlavor()).withAdapters(PhoneAdapter) - val inst = PersonWithPhone("Bartholomew", "5555555555".asInstanceOf[Phone]) - val item = sj.render(inst) - assertEquals("""{ Item: {name=Bartholomew, phone=555-555-5555} }""", item.toString) - assertEquals(inst, sj.read[PersonWithPhone](item)) - } - - test("With Hints") { - val sj = - ScalaJack(DynamoFlavor()).withHints((RType.of[Address] -> "addr_kind")) - val inst: Address = USAddress("123 Main", "New York", "NY", "39822") - val item = sj.render(inst) - assertEquals( - """{ Item: {addr_kind=co.blocke.scalajack.dynamodb.USAddress, street=123 Main, city=New York, state=NY, postalCode=39822} }""", - item.toString - ) - assertEquals(inst, sj.read[Address](item)) - } - - test("With Hint Modifiers") { - val prependHintMod = ClassNameHintModifier( - (hint: String) => "co.blocke.scalajack.dynamodb." + hint, - (cname: String) => cname.split('.').last - ) - val sj = ScalaJack(DynamoFlavor()) - .withHintModifiers((RType.of[Address], prependHintMod)) - val inst: Address = USAddress("123 Main", "New York", "NY", "39822") - val item = sj.render(inst) - assertEquals( - """{ Item: {_hint=USAddress, street=123 Main, city=New York, state=NY, postalCode=39822} }""", - item.toString - ) - assertEquals(inst, sj.read[Address](item)) - } - - test("Externalized type modifier") { - val sj = ScalaJack(DynamoFlavor()).withTypeValueModifier( - ClassNameHintModifier( - (hint: String) => "co.blocke.scalajack.dynamodb." + hint, - (cname: String) => cname.split('.').last - ) - ) - val value = Envelope("DEF", FancyBody("BOO")) - val item = sj.render(value) - assertEquals( - "{ Item: {Giraffe=FancyBody, id=DEF, body={message=BOO}} }", - item.toString - ) - assert(value == sj.read[Envelope[Body]](item)) - } - - test("Default Hint") { - val sj = ScalaJack(DynamoFlavor()).withDefaultHint("kind") - val inst: Human = Person( - "Greg", - 50, - List("Woodworking", "Diet Coke"), - Misc(1.23, "boom"), - Some(false) - ) - val item = sj.render(inst) - assertEquals( - """{ Item: {kind=co.blocke.scalajack.dynamodb.Person, name=Greg, age=50, likes=[Woodworking, Diet Coke], stuff={wow=1.23, bing=boom}, foo=false} }""", - item.toString - ) - assertEquals(inst, sj.read[Human](item)) - } - - test("ParseOrElse") { - val sj = ScalaJack(DynamoFlavor()) - .parseOrElse((RType.of[Address] -> RType.of[DefaultAddress])) - val item = Item.fromJSON( - """{"_hint":"co.blocke.scalajack.custom.UnknownAddress","street":"123 Main","city":"New York","state":"NY","postalCode":"39822"}""" - ) - assert(DefaultAddress("39822") == sj.read[Address](item)) - } diff --git a/dynamodb/src/test/scala/co.blocke.scalajack/dynamodb/CreateTableRequest.scala b/dynamodb/src/test/scala/co.blocke.scalajack/dynamodb/CreateTableRequest.scala deleted file mode 100644 index 76bbb3e5..00000000 --- a/dynamodb/src/test/scala/co.blocke.scalajack/dynamodb/CreateTableRequest.scala +++ /dev/null @@ -1,41 +0,0 @@ -package co.blocke.scalajack -package dynamodb - -import TestUtil._ -import munit._ -import munit.internal.console -import com.amazonaws.services.dynamodbv2.model.ProvisionedThroughput - -class CreateTableRequest extends FunSuite: - - val sj = ScalaJack(DynamoFlavor()).asInstanceOf[DynamoFlavor] - - test("Single primary key") { - describe( - "-----------------------------------------\n: DynamoDB Create Table Request Tests :\n-----------------------------------------", Console.BLUE - ) - val req = sj.createTableRequest[PersonOneKey](new ProvisionedThroughput(12L, 5L)) - assertEquals( - """{AttributeDefinitions: [{AttributeName: name,AttributeType: S}],TableName: people2,KeySchema: [{AttributeName: name,KeyType: HASH}],ProvisionedThroughput: {ReadCapacityUnits: 12,WriteCapacityUnits: 5},}""", - req.toString) - } - - test("Primary key with sorting key") { - val req = - sj.createTableRequest[Person](new ProvisionedThroughput(12L, 5L)) - assertEquals( - """{AttributeDefinitions: [{AttributeName: age,AttributeType: N}, {AttributeName: name,AttributeType: S}],TableName: people,KeySchema: [{AttributeName: age,KeyType: HASH}, {AttributeName: name,KeyType: RANGE}],ProvisionedThroughput: {ReadCapacityUnits: 12,WriteCapacityUnits: 5},}""", - req.toString) - } - - test("Error - no key specified") { - interceptMessage[java.lang.IllegalStateException]("Class co.blocke.scalajack.dynamodb.ErrorNoKey must define at least a primary key with @DBKey."){ - sj.createTableRequest[ErrorNoKey](new ProvisionedThroughput(12L, 5L)) - } - } - - test("Error - no table specified") { - interceptMessage[java.lang.IllegalStateException]("Class co.blocke.scalajack.dynamodb.ErrorNoTable must be annotated with @Collection to specify a table name."){ - sj.createTableRequest[ErrorNoTable](new ProvisionedThroughput(12L, 5L)) - } - } diff --git a/dynamodb/src/test/scala/co.blocke.scalajack/dynamodb/JsonDiff.scala b/dynamodb/src/test/scala/co.blocke.scalajack/dynamodb/JsonDiff.scala deleted file mode 100644 index 7a49c311..00000000 --- a/dynamodb/src/test/scala/co.blocke.scalajack/dynamodb/JsonDiff.scala +++ /dev/null @@ -1,58 +0,0 @@ -package co.blocke.scalajack -package dynamodb - -import org.json4s.JsonAST.{ JNothing, JObject, JValue } - -object JsonDiff { - - def compare( - left: JValue, - right: JValue, - leftLabel: String = "left", - rightLabel: String = "right"): Seq[JsonDiff] = { - (left, right) match { - case (JObject(leftFields), JObject(rightFields)) => - val allFieldNames = - (leftFields.map(_._1) ++ rightFields.map(_._1)).distinct - allFieldNames.sorted flatMap { fieldName => - val leftFieldValue = leftFields - .collectFirst({ case (`fieldName`, fieldValue) => fieldValue }) - .getOrElse(JNothing) - val rightFieldValue = rightFields - .collectFirst({ case (`fieldName`, fieldValue) => fieldValue }) - .getOrElse(JNothing) - compare(leftFieldValue, rightFieldValue, leftLabel, rightLabel) - } - - // ---- Not used/needed at present, and I have questions about the correct behavior here. Exactly how do you - // "diff" two arrays (not necessarily homogeneous typed)? - // - // case (JArray(leftElements), JArray(rightElements)) => - // (0 until (leftElements.size max rightElements.size)) flatMap { elementIndex => - // val leftElement = leftElements.applyOrElse(elementIndex, (_: Int) => JNothing) - // val rightElement = rightElements.applyOrElse(elementIndex, (_: Int) => JNothing) - // compare(path \ elementIndex, leftElement, rightElement, leftLabel, rightLabel) - // } - - case _ => - if (left == right) { - Seq.empty - } else { - val outerLeft = left - val outerRight = right - Seq(new JsonDiff { - override val left: JValue = outerLeft - override val right: JValue = outerRight - override def toString: String = - s"JsonDiff($leftLabel: $left, $rightLabel: $right)" - }) - } - } - } - -} - -trait JsonDiff { - val left: JValue - val right: JValue -} diff --git a/dynamodb/src/test/scala/co.blocke.scalajack/dynamodb/JsonMatcher.scala b/dynamodb/src/test/scala/co.blocke.scalajack/dynamodb/JsonMatcher.scala deleted file mode 100644 index 1cdd381a..00000000 --- a/dynamodb/src/test/scala/co.blocke.scalajack/dynamodb/JsonMatcher.scala +++ /dev/null @@ -1,21 +0,0 @@ -package co.blocke.scalajack -package dynamodb - -import co.blocke.scalajack.json.JSON -import org.json4s.JsonAST.JValue -import org.json4s.native.JsonMethods._ -import org.json4s.string2JsonInput - -object JsonMatcher { - - def jsonMatches( expected: JSON, actual: JSON ): Boolean = - val diffs = JsonDiff.compare( - parseJValue(expected.asInstanceOf[String]), - parseJValue(actual.asInstanceOf[String]), - "expected", - "actual" - ) - diffs.isEmpty - - implicit def parseJValue(string: String): JValue = parse(string) -} diff --git a/dynamodb/src/test/scala/co.blocke.scalajack/dynamodb/Model.scala b/dynamodb/src/test/scala/co.blocke.scalajack/dynamodb/Model.scala deleted file mode 100644 index 36f725e6..00000000 --- a/dynamodb/src/test/scala/co.blocke.scalajack/dynamodb/Model.scala +++ /dev/null @@ -1,110 +0,0 @@ -package co.blocke.scalajack -package dynamodb - -import model._ -import scala.collection.mutable -import co.blocke.scala_reflection.RType -import co.blocke.scala_reflection.info.AliasInfo - -opaque type Phone >: Null = String - -// Override just Phone -object PhoneAdapter extends TypeAdapterFactory with TypeAdapter[Phone]: - def matches(concrete: RType): Boolean = - concrete match { - case a: AliasInfo if a.name == "Phone" => true - case _ => false - } - def makeTypeAdapter(concrete: RType)(implicit taCache: TypeAdapterCache): TypeAdapter[Phone] = this - val info = RType.of[Phone] - override def isStringish: Boolean = true - - def read(parser: Parser): Phone = - parser.expectString() match { - case null => null.asInstanceOf[Phone] - case s: String => s.replaceAll("-", "").asInstanceOf[Phone] - } - - def write[WIRE]( - t: Phone, - writer: Writer[WIRE], - out: mutable.Builder[WIRE, WIRE]): Unit = t match { - case null => writer.writeNull(out) - case _ => - writer.writeString( - "%s-%s-%s".format(t.toString.substring(0, 3), t.toString.substring(3, 6), t.toString.substring(6)), - out - ) - } - -trait Human { val name: String; val age: Int } -case class Misc(wow: Double, bing: String) - -@Collection(name = "people") -case class Person( - @DBKey(index = 1) name:String, - @DBKey(index = 0) age:Int, - likes: List[String], - stuff: Misc, - foo: Option[Boolean] = None) - extends Human - -@Collection(name = "people2") -case class PersonOneKey( - @DBKey(index = 0) name:String, - age: Int, - likes: List[String], - stuff: Misc, - foo: Option[Boolean] = None) - extends Human - -@Collection(name = "bogus") -case class ErrorNoKey( - name: String, - age: Int, - likes: List[String], - stuff: Misc, - foo: Option[Boolean] = None) - extends Human - -case class ErrorNoTable( - @DBKey(index = 0) name:String, - age: Int, - likes: List[String], - stuff: Misc, - foo: Option[Boolean] = None) - extends Human - -case class PersonWithPhone(name: String, phone: Phone) -trait Address { val postalCode: String } -case class DefaultAddress(postalCode: String) extends Address -case class USAddress( - street: String, - city: String, - state: String, - postalCode: String) - extends Address - -@Collection(name = "people") -class PersonPlain1( - @DBKey(index = 1) val name:String, - @DBKey(index = 0) val age:Int, - val likes: List[String], - val stuff: Misc, - val foo: Option[Boolean] = None) - -@Collection(name = "people") -class PersonPlain2() { - @DBKey(index = 1) var name: String = "" - @DBKey(index = 0) var age: Int = 0 - var likes: List[String] = List.empty[String] - var stuff: Misc = _ - var foo: Option[Boolean] = None -} - -trait Body -case class FancyBody(message: String) extends Body - -case class Envelope[T <: Body](id: String, body: T) { - type Giraffe = T -} \ No newline at end of file diff --git a/dynamodb/src/test/scala/co.blocke.scalajack/dynamodb/Plain.scala b/dynamodb/src/test/scala/co.blocke.scalajack/dynamodb/Plain.scala deleted file mode 100644 index 5f177097..00000000 --- a/dynamodb/src/test/scala/co.blocke.scalajack/dynamodb/Plain.scala +++ /dev/null @@ -1,72 +0,0 @@ -package co.blocke.scalajack -package dynamodb - -import TestUtil._ -import munit._ -import munit.internal.console -import JsonMatcher._ - -import com.amazonaws.services.dynamodbv2.model.ProvisionedThroughput - -class Plain extends FunSuite: - - val sj = ScalaJack(DynamoFlavor()) - - test("All val constructor") { - describe( - "--------------------------------\n: DynamoDB Plain Class Tests :\n--------------------------------", Console.BLUE - ) - describe("Items:") - val inst = new PersonPlain1( - "Greg", - 50, - List("Woodworking", "Diet Coke"), - Misc(1.23, "boom"), - Some(true) - ) - val item = sj.render(inst) - assertEquals(true, jsonMatches( - """{"name":"Greg","age":50,"likes":["Woodworking","Diet Coke"],"stuff":{"wow":1.23,"bing":"boom"},"foo":true}""".asInstanceOf[json.JSON], - item.toJSON.asInstanceOf[json.JSON] - )) - assertEquals(true, { - val inst2 = sj.read[PersonPlain1](item) - inst.name == inst2.name && inst.age == inst2.age && inst.stuff.wow == inst2.stuff.wow - }) - } - - test("Zero-arg constructor with var members") { - val inst = new PersonPlain2() - inst.name = "Greg" - inst.age = 50 - inst.likes = List("Woodworking", "Diet Coke") - inst.stuff = Misc(1.23, "boom") - val item = sj.render(inst) - assertEquals(true, jsonMatches( - """{"name":"Greg","age":50,"likes":["Woodworking","Diet Coke"],"stuff":{"wow":1.23,"bing":"boom"}}""".asInstanceOf[json.JSON], - item.toJSON.asInstanceOf[json.JSON] - )) - assertEquals(true, { - val inst2 = sj.read[PersonPlain2](item) - inst.name == inst2.name && inst.age == inst2.age && inst.stuff.wow == inst2.stuff.wow - }) - } - - test("All val constructor") { - describe("Creation:") - val req = sj - .asInstanceOf[DynamoFlavor] - .createTableRequest[PersonPlain1](new ProvisionedThroughput(12L, 5L)) - assertEquals( - """{AttributeDefinitions: [{AttributeName: age,AttributeType: N}, {AttributeName: name,AttributeType: S}],TableName: people,KeySchema: [{AttributeName: age,KeyType: HASH}, {AttributeName: name,KeyType: RANGE}],ProvisionedThroughput: {ReadCapacityUnits: 12,WriteCapacityUnits: 5},}""", - req.toString) - } - - test("Zero-arg constructor with var members") { - val req = sj - .asInstanceOf[DynamoFlavor] - .createTableRequest[PersonPlain2](new ProvisionedThroughput(12L, 5L)) - assertEquals( - """{AttributeDefinitions: [{AttributeName: age,AttributeType: N}, {AttributeName: name,AttributeType: S}],TableName: people,KeySchema: [{AttributeName: age,KeyType: HASH}, {AttributeName: name,KeyType: RANGE}],ProvisionedThroughput: {ReadCapacityUnits: 12,WriteCapacityUnits: 5},}""", - req.toString) - } diff --git a/dynamodb/src/test/scala/co.blocke.scalajack/dynamodb/README.md b/dynamodb/src/test/scala/co.blocke.scalajack/dynamodb/README.md deleted file mode 100644 index 5992d855..00000000 --- a/dynamodb/src/test/scala/co.blocke.scalajack/dynamodb/README.md +++ /dev/null @@ -1,10 +0,0 @@ -This test suite is very minimal. - -Since the core of the DynamoDB implementation for ScalaJack involves merely using Item's own fromJSON method, -we just wrapped ScalaJack's native JSON capabilities, which have been extensively tested. There's no particular -need to re-test these here. - -There are limitations on what DynamoDB can handle, for example you can't serialize a naked List[Something] -like you can in the JSON flavor. - -If DynamoDB can handle it, ScalaJack's DynamoDB flavor should do it. \ No newline at end of file diff --git a/mongo/src/main/scala/co.blocke.scalajack/mongo/BsonBuilder.scala b/mongo/src/main/scala/co.blocke.scalajack/mongo/BsonBuilder.scala deleted file mode 100644 index 51ffa89a..00000000 --- a/mongo/src/main/scala/co.blocke.scalajack/mongo/BsonBuilder.scala +++ /dev/null @@ -1,22 +0,0 @@ -package co.blocke.scalajack -package mongo - -import org.bson.BsonValue - -import scala.collection.mutable - -case class BsonBuilder() extends mutable.Builder[BsonValue, BsonValue] { - private var internalValue: Option[BsonValue] = None - - def addOne(elem: BsonValue): this.type = { - internalValue = Some(elem) - this - } - - def clear(): Unit = internalValue = None - - def result(): BsonValue = - internalValue.getOrElse( - throw new ScalaJackError("No value set for internal mongo builder") - ) -} \ No newline at end of file diff --git a/mongo/src/main/scala/co.blocke.scalajack/mongo/BsonParser.scala b/mongo/src/main/scala/co.blocke.scalajack/mongo/BsonParser.scala deleted file mode 100644 index 23d6d50e..00000000 --- a/mongo/src/main/scala/co.blocke.scalajack/mongo/BsonParser.scala +++ /dev/null @@ -1,216 +0,0 @@ -package co.blocke.scalajack -package mongo - -import model._ -import org.bson._ -import org.bson.types.ObjectId -import co.blocke.scalajack.typeadapter.classes.ClassTypeAdapterBase -import co.blocke.scala_reflection.RType -import co.blocke.scala_reflection.info.TypeMemberInfo - -import scala.collection.mutable -import scala.jdk.CollectionConverters._ - -case class BsonParser(input: BsonValue, jackFlavor: JackFlavor[BsonValue]) extends Parser { - type WIRE = BsonValue - - def expectString(nullOK: Boolean = true): String = - if (input == null || input.isNull) - null - else if (input.isString) - input.asString.getValue - else - throw new ScalaJackError(s"Expected string here, not '$input'") - - def expectList[K, TO](KtypeAdapter: TypeAdapter[K], builder: mutable.Builder[K, TO]): TO = - if (input == null || input.isNull) - // $COVERAGE-OFF$Null caught by TypeAdapter but this left here as a safety - null.asInstanceOf[TO] - // $COVERAGE-ON$ - else if (input.isArray) { - input.asArray.getValues.asScala - .foreach(v => builder += KtypeAdapter.read(BsonParser(v, jackFlavor))) - builder.result() - } else - throw new ScalaJackError(s"Expected list here, not '$input'") - - def expectTuple( - tupleFieldTypeAdapters: List[TypeAdapter[_]] - ): List[Object] = - input match { - // $COVERAGE-OFF$Null caught by TypeAdapter but this left here as a safety - case inp if inp == null || inp.isNull => - null - // $COVERAGE-ON$ - case inp if inp.isArray => - tupleFieldTypeAdapters.zip(input.asArray.getValues.asScala).map { (fieldTypeAdapter, v) => - fieldTypeAdapter.read(BsonParser(v, jackFlavor)).asInstanceOf[Object] - } - case x => - throw new ScalaJackError(s"Expected tuple (list) here, not '$input'") - } - - def expectMap[K, V, TO](keyTypeAdapter: TypeAdapter[K], valueTypeAdapter: TypeAdapter[V], builder: mutable.Builder[(K, V), TO]): TO = - if (input == null || input.isNull) - // $COVERAGE-OFF$Null caught by TypeAdapter but this left here as a safety - null.asInstanceOf[TO] - // $COVERAGE-ON$ - else if (input.isDocument) { - input.asDocument.entrySet.asScala.foreach { entry => - val mapKey = keyTypeAdapter.read( - BsonParser(new BsonString(entry.getKey), jackFlavor) - ) - val mapValue = - valueTypeAdapter.read(BsonParser(entry.getValue, jackFlavor)) - val newElem: (K, V) = (mapKey, mapValue) - builder += newElem - } - builder.result - } else - throw new ScalaJackError(s"Expected document (map) here, not '$input'") - - def expectObject( - classBase: ClassTypeAdapterBase[_], - hintLabel: String - ): (mutable.BitSet, List[Object], java.util.HashMap[String, _]) = - if (input == null || input.isNull) - // $COVERAGE-OFF$Null caught by TypeAdapter but this left here as a safety - null - // $COVERAGE-ON$ - else if (input.isDocument) { - val args = classBase.argsTemplate.clone() - val fieldBits = mutable.BitSet() - val captured = - if (classBase.isSJCapture) new java.util.HashMap[String, BsonValue]() - else null - input.asDocument.entrySet.asScala.foreach { - case entry if entry.getKey == ID_FIELD && classBase.dbKeys.size == 1 => - val dbKey = classBase.dbKeys.head - fieldBits += dbKey.info.index - args(dbKey.info.index) = dbKey.valueTypeAdapter.read(BsonParser(entry.getValue, jackFlavor)).asInstanceOf[Object] - case entry if entry.getKey == ID_FIELD => // compound key - entry.getValue.asDocument.entrySet.asScala.foreach { dbKeyEntry => - classBase.fieldMembersByName - .get(dbKeyEntry.getKey) - .map { field => - fieldBits += field.info.index - args(field.info.index) = field.valueTypeAdapter.read( - BsonParser(dbKeyEntry.getValue, jackFlavor) - ).asInstanceOf[Object] - } - } - case entry => - classBase.fieldMembersByName.get(entry.getKey) match { - case Some(field) => - fieldBits += field.info.index - args(field.info.index) = field.valueTypeAdapter.read( - BsonParser(entry.getValue, jackFlavor) - ).asInstanceOf[Object] - case None => // found some input field not present in class - if (captured != null) - captured.put(entry.getKey, entry.getValue) - } - } - val missing = fieldBits.intersect(classBase.dbKeys.map(_.info.index).toSet) - if (classBase.dbKeys.size > 0 && missing.isEmpty) { - if (classBase.dbKeys.size == 1) - throw new ScalaJackError( - "Missing key (_id) field, or a component of a compound key field" - ) - else - throw new ScalaJackError( - "Missing key (_id) field, or a component of a compound key field: " + - classBase.dbKeys - .collect { case f if missing.contains(f.info.index) => f.name } - .mkString(",") - ) - } - (fieldBits, args.toList, captured) - } else - throw new ScalaJackError(s"Expected document (object) here, not '$input'") - - def expectBoolean(): Boolean = - if (input.isBoolean) - input.asBoolean.getValue - else - throw new ScalaJackError(s"Expected boolean here, not '$input'") - - def expectNumber(nullOK: Boolean = false): String = - input match { - case i if i.isNull && nullOK => null - case i if i.isNull => - throw new ScalaJackError(s"Expected number here, not '$input'") - case i if i.isDecimal128 => i.asDecimal128.getValue.toString - case i if i.isDouble => i.asDouble.getValue.toString - case i if i.isInt32 => i.asInt32.getValue.toString - case i if i.isInt64 => i.asInt64.getValue.toString - case i if i.isDateTime => i.asDateTime.getValue.toString - case _ => throw new ScalaJackError(s"Expected number here, not '$input'") - } - - def peekForNull: Boolean = input == null || input.isNull - - def scanForHint(hint: String, converterFn: HintBijective): Class[_] = - if (input.isDocument) { - val doc = input.asDocument - Option(doc.get(hint)) match { - case Some(hintValue) if hintValue.isString => - val hintType = try { - Class.forName(converterFn.apply(hintValue.asString.getValue)) - } catch { - case t: Throwable => - throw new ScalaJackError( - s"Couldn't marshal class for ${hintValue.asString.getValue}" - ) - } - hintType - case Some(hintValue) => - throw new ScalaJackError(s"Hint value $hint must be a string value") - case None => throw new ScalaJackError(s"Type hint '$hint' not found") - } - } else - throw new ScalaJackError(s"Expected document here, not '$input'") - - // For embedded type members. Convert the type member into runtime "actual" type, e.g. T --> Foo - def resolveTypeMembers( - typeMembersByName: Map[String, TypeMemberInfo], - converterFn: HintBijective - ): Map[String, TypeMemberInfo] = // Returns Map[Type Signature Type (e.g. 'T'), Type] - if (input.isDocument) { - val doc = input.asDocument - val collected = doc.keySet.asScala.collect { - case key if typeMembersByName.contains(key) => - ( - key, - TypeMemberInfo(key, typeMembersByName(key).typeSymbol, RType.of(Class.forName(converterFn.apply(doc.get(key).asString.getValue)))) - ) - } - collected.toMap - } else - throw new ScalaJackError(s"Expected document (object) here, not '$input'") - - def showError(msg: String): String = msg - def backspace(): Unit = {} - def mark(): Int = -1 - def revertToMark(mark: Int): Unit = {} - def nextIsString: Boolean = input.isString - def nextIsNumber: Boolean = input.isNumber - def nextIsObject: Boolean = input.isDocument - def nextIsArray: Boolean = input.isArray - def nextIsBoolean: Boolean = input.isBoolean - def subParser(input: BsonValue): Parser = this - def sourceAsString: String = - throw new ScalaJackError( - s"""BSON type ${input.getClass.getName} is not currently supported in ScalaJack.""" - ) - - //--- Mongo Specific --- - def expectObjectId(): ObjectId = - if (input == null || input.isNull) - null - else if (input.isObjectId) - input.asObjectId.getValue - else - throw new ScalaJackError(s"Expected ObjectId here, not '$input'") - -} \ No newline at end of file diff --git a/mongo/src/main/scala/co.blocke.scalajack/mongo/Converters.scala b/mongo/src/main/scala/co.blocke.scalajack/mongo/Converters.scala deleted file mode 100644 index 01092c7f..00000000 --- a/mongo/src/main/scala/co.blocke.scalajack/mongo/Converters.scala +++ /dev/null @@ -1,37 +0,0 @@ -package co.blocke.scalajack -package mongo - -import org.bson._ -import model.JackFlavor -import co.blocke.scalajack.Converters._ -import co.blocke.scalajack.json._ -import json4s._ -import yaml._ -import delimited._ -import org.json4s.JValue - -object Converters: - - extension (b: BsonValue) - inline def mapMongoTo[T, S](toFlavor: JackFlavor[S])(fn: T => T)(implicit sjB: JackFlavor[BsonValue]): S = toFlavor.render[T](fn(sjB.read[T](b))) - - // montoTo... flavors need T to be able to handle _id (DBKey) fields. - extension (b: BsonValue) - inline def mongoToJson[T](implicit sjJ: JackFlavor[JSON], sjB: JackFlavor[BsonValue]): JSON = sjJ.render( sjB.read[T](b) ) - inline def mongoToYaml[T](implicit sjY: JackFlavor[YAML], sjB: JackFlavor[BsonValue]): YAML = sjY.render( sjB.read[T](b) ) - inline def mongoToJson4s[T](implicit sjV: JackFlavor[JValue], sjB: JackFlavor[BsonValue]): JValue = sjV.render( sjB.read[T](b) ) - inline def fromMongo[T](implicit sjB: JackFlavor[BsonValue]): T = sjB.read[T](b) - inline def mapMongo[T](fn: T => T)(implicit sjB: JackFlavor[BsonValue]): BsonValue = sjB.render[T](fn(sjB.read[T](b))) - - // Tie in other converters... - extension (j: JSON) - inline def jsonToMongo[T](implicit sjB: JackFlavor[BsonValue], sjJ: JackFlavor[JSON]): BsonValue = sjB.render( sjJ.read[T](j) ) - - extension (y: YAML) - inline def yamlToMongo[T](implicit sjB: JackFlavor[BsonValue], sjY: JackFlavor[YAML]): BsonValue = sjB.render( sjY.read[T](y) ) - - extension (j: JValue) - inline def json4sToMongo[T](implicit sjB: JackFlavor[BsonValue], sjV: JackFlavor[JValue]): BsonValue = sjB.render( sjV.read[T](j) ) - - extension[T] (a: T) - inline def toMongo(implicit sjB: JackFlavor[BsonValue]): BsonValue = sjB.render(a) \ No newline at end of file diff --git a/mongo/src/main/scala/co.blocke.scalajack/mongo/MongoFlavor.scala b/mongo/src/main/scala/co.blocke.scalajack/mongo/MongoFlavor.scala deleted file mode 100644 index 2957d2b5..00000000 --- a/mongo/src/main/scala/co.blocke.scalajack/mongo/MongoFlavor.scala +++ /dev/null @@ -1,79 +0,0 @@ -package co.blocke.scalajack -package mongo - -import model._ -import typeadapter._ -import org.bson._ -import co.blocke.scala_reflection.RType - -import scala.collection.mutable - -final val ID_FIELD = "_id" - -case class MongoFlavor( - override val defaultHint: String = "_hint", - override val permissivesOk: Boolean = false, - override val customAdapters: List[TypeAdapterFactory] = List.empty[TypeAdapterFactory], - override val hintMap: Map[String, String] = Map.empty[String, String], - override val hintValueModifiers: Map[String, HintValueModifier] = Map.empty[String, HintValueModifier], - override val typeValueModifier: HintValueModifier = DefaultHintModifier, - override val parseOrElseMap: Map[Class[_], RType] = Map.empty[Class[_], RType], - override val enumsAsInt: Boolean = false -) extends JackFlavor[BsonValue] { - - override val stringifyMapKeys: Boolean = true - - def allowPermissivePrimitives(): JackFlavor[BsonValue] = - this.copy(permissivesOk = true) - def enumsAsInts(): JackFlavor[BsonValue] = this.copy(enumsAsInt = true) - def parseOrElse(poe: (RType, RType)*): JackFlavor[BsonValue] = - this.copy(parseOrElseMap = this.parseOrElseMap ++ poe.map{(p,oe) => p.infoClass->oe}) - def withAdapters(ta: TypeAdapterFactory*): JackFlavor[BsonValue] = - this.copy(customAdapters = this.customAdapters ++ ta.toList) - def withDefaultHint(hint: String): JackFlavor[BsonValue] = - this.copy(defaultHint = hint) - def withHints(h: (RType, String)*): JackFlavor[BsonValue] = - this.copy(hintMap = this.hintMap ++ h.map{(rt,hint) => rt.name->hint}) - def withHintModifiers(hm: (RType, HintValueModifier)*): JackFlavor[BsonValue] = - this.copy(hintValueModifiers = this.hintValueModifiers ++ hm.map{(rt,hintM) => rt.name->hintM}) - def withTypeValueModifier(tm: HintValueModifier): JackFlavor[BsonValue] = - this.copy(typeValueModifier = tm) - - def stringWrapTypeAdapterFactory[T]( - wrappedTypeAdapter: TypeAdapter[T], - emptyStringOk: Boolean = true - ): TypeAdapter[T] = - new StringWrapTypeAdapter(wrappedTypeAdapter) - - def maybeStringWrapTypeAdapterFactory[T]( - wrappedTypeAdapter: TypeAdapter[T], - emptyStringOk: Boolean = true - ): TypeAdapter[T] = - co.blocke.scalajack.typeadapter.MaybeStringWrapTypeAdapter(this, wrappedTypeAdapter, emptyStringOk) - - override def bakeCache(): TypeAdapterCache = { - val dads = super.bakeCache() - dads.copy( - factories = List( - ObjectIdTypeAdapter, - OffsetDateTimeTypeAdapter, - ZonedDateTimeTypeAdapter - ) ++ dads.factories.toList - ) - } - - private val writer = MongoWriter(anyTypeAdapter) - - def parse(input: BsonValue): Parser = BsonParser(input, this) - - def _read[T](input: BsonValue, typeAdapter: TypeAdapter[T]): T = - val parser = BsonParser(input, this) - typeAdapter.read(parser).asInstanceOf[T] - - def _render[T](t: T, typeAdapter: TypeAdapter[T]): BsonValue = - val sb = mongo.BsonBuilder() - typeAdapter.write(t, writer, sb) - sb.result() - - override def getBuilder: mutable.Builder[BsonValue, BsonValue] = mongo.BsonBuilder() -} \ No newline at end of file diff --git a/mongo/src/main/scala/co.blocke.scalajack/mongo/MongoWriter.scala b/mongo/src/main/scala/co.blocke.scalajack/mongo/MongoWriter.scala deleted file mode 100644 index 82a9266d..00000000 --- a/mongo/src/main/scala/co.blocke.scalajack/mongo/MongoWriter.scala +++ /dev/null @@ -1,169 +0,0 @@ -package co.blocke.scalajack -package mongo - -import model._ - -import scala.collection.Map -import scala.collection.mutable -import org.bson._ -import org.bson.types.Decimal128 - -case class MongoWriter(anyTypeAdapter: TypeAdapter[Any]) extends Writer[BsonValue] { - - def writeBigInt(t: BigInt, out: mutable.Builder[BsonValue, BsonValue]): Unit = - throw new ScalaJackError( - "BigInt is currently an unsupported datatype for MongoDB serialization" - ) - def writeBoolean(t: Boolean, out: mutable.Builder[BsonValue, BsonValue]): Unit = - out += new BsonBoolean(t) - def writeDecimal(t: BigDecimal, out: mutable.Builder[BsonValue, BsonValue]): Unit = - out += new BsonDecimal128(new Decimal128(t.bigDecimal)) - def writeDouble(t: Double, out: mutable.Builder[BsonValue, BsonValue]): Unit = - out += new BsonDouble(t) - def writeInt(t: Int, out: mutable.Builder[BsonValue, BsonValue]): Unit = - out += new BsonInt32(t) - def writeLong(t: Long, out: mutable.Builder[BsonValue, BsonValue]): Unit = - out += new BsonInt64(t) - def writeNull(out: mutable.Builder[BsonValue, BsonValue]): Unit = - out += new BsonNull() - def writeString(t: String, out: mutable.Builder[BsonValue, BsonValue]): Unit = - out += new BsonString(t) - def writeRaw(t: BsonValue, out: mutable.Builder[BsonValue, BsonValue]): Unit = - out += t - - def writeArray[Elem](t: Iterable[Elem], elemTypeAdapter: TypeAdapter[Elem], out: mutable.Builder[BsonValue, BsonValue]): Unit = - t match { - case null => out += new BsonNull() - case a => - val array = new BsonArray() - val builder = BsonBuilder() - val iter = a.iterator - while (iter.hasNext) { - elemTypeAdapter.write[BsonValue](iter.next, this, builder) - array.add(builder.result()) - builder.clear() - } - out += array - } - - def writeMap[Key, Value, To]( - t: Map[Key, Value], - keyTypeAdapter: TypeAdapter[Key], - valueTypeAdapter: TypeAdapter[Value], - out: mutable.Builder[BsonValue, BsonValue] - ): Unit = t match { - case null => out += new BsonNull() - case daMap => - val doc = new BsonDocument() - val keyBuilder = BsonBuilder() - val valueBuilder = BsonBuilder() - daMap.foreach { - case (key, value) => - if (key == null) - throw new ScalaJackError("Map keys cannot be null.") - keyTypeAdapter.write(key, this, keyBuilder) - valueTypeAdapter.write(value, this, valueBuilder) - doc.append( - keyBuilder.result().asString.getValue, - valueBuilder.result() - ) - keyBuilder.clear() - valueBuilder.clear() - } - out += doc - } - - def writeTuple[T]( - t: T, - writeFn: (Product) => List[(TypeAdapter[_], Any)], - out: mutable.Builder[BsonValue, BsonValue] - ): Unit = { - - var arr = new BsonArray() - val outBuf = BsonBuilder() - writeFn(t.asInstanceOf[Product]).foreach { (fieldTA, fieldValue) => - outBuf.clear() - fieldTA.castAndWrite(fieldValue, this, outBuf) - arr.add(outBuf.result()) - } - out += arr - } - - @inline private def writeFields(fields: List[(String, Object, TypeAdapter[_])], doc: BsonDocument): Unit = { - for ((label, value, valueTypeAdapter) <- fields) - if (value != None) { - val builder = BsonBuilder() - valueTypeAdapter.castAndWrite(value, this, builder) - doc.append(label, builder.result()) - } - } - - def writeObject[T]( - t: T, - orderedFieldNames: List[String], - fieldMembersByName: Map[String, ClassFieldMember[_,_]], - out: mutable.Builder[BsonValue, BsonValue], - extras: List[(String, ExtraFieldValue[_])] = List.empty[(String, ExtraFieldValue[_])] - ): Unit = - if (t == null) - out += new BsonNull() - else { - val doc = new BsonDocument() - writeFields( - extras.map( - e => - ( - e._1, - e._2.value.asInstanceOf[Object], - e._2.valueTypeAdapter.asInstanceOf[TypeAdapter[Any]] - ) - ), - doc - ) - - // Logic to handle _id (@DBKey) - val (dbkeys, theRest) = - fieldMembersByName.partition(_._2.dbKeyIndex.isDefined) - if (dbkeys.isEmpty) // no @DBKey fields - writeFields( - fieldMembersByName - .map(f => (f._1, f._2.info.valueOf(t), f._2.valueTypeAdapter)) - .toList, - doc - ) - else { - if (dbkeys.size == 1) { - val toWrite = List( - ( - ID_FIELD, - fieldMembersByName(dbkeys.head._1).info.valueOf(t), - fieldMembersByName(dbkeys.head._1).valueTypeAdapter - ) - ) - writeFields(toWrite, doc) - } else { - val idDoc = new BsonDocument() - val toWrite = dbkeys - .map(e => (e._1, e._2.info.valueOf(t), e._2.valueTypeAdapter)) - .toList - writeFields(toWrite, idDoc) - doc.append(ID_FIELD, idDoc) - } - val toWrite = theRest - .map(e => (e._1, e._2.info.valueOf(t), e._2.valueTypeAdapter)) - .toList - writeFields(toWrite, doc) - } - - t match { - case sjc: SJCapture => - import scala.jdk.CollectionConverters._ - sjc.captured.asScala.foreach { - case (label, capturedValue) => - doc.append(label, capturedValue.asInstanceOf[BsonValue]) - } - case _ => - } - out += doc - } -} \ No newline at end of file diff --git a/mongo/src/main/scala/co.blocke.scalajack/mongo/typeadapter/ObjectIdTypeAdapter.scala b/mongo/src/main/scala/co.blocke.scalajack/mongo/typeadapter/ObjectIdTypeAdapter.scala deleted file mode 100644 index 64d6b108..00000000 --- a/mongo/src/main/scala/co.blocke.scalajack/mongo/typeadapter/ObjectIdTypeAdapter.scala +++ /dev/null @@ -1,28 +0,0 @@ -package co.blocke.scalajack -package mongo -package typeadapter - -import model._ - -import scala.collection.mutable -import org.bson.types.ObjectId -import org.bson._ -import co.blocke.scala_reflection.RType - -object ObjectIdTypeAdapter extends TypeAdapterFactory with TypeAdapter[ObjectId]: - - def matches(concrete: RType): Boolean = concrete == info - - def makeTypeAdapter(concrete: RType)(implicit taCache: TypeAdapterCache): TypeAdapter[_] = this - - val info = RType.of[ObjectId] - - def read(parser: Parser): ObjectId = - parser.asInstanceOf[BsonParser].expectObjectId() - - def write[WIRE](t: ObjectId, writer: Writer[WIRE], out: mutable.Builder[WIRE, WIRE]): Unit = - t match { - case null => out += new BsonNull().asInstanceOf[WIRE] - case _ => - out += (new BsonObjectId(t)).asInstanceOf[WIRE] - } \ No newline at end of file diff --git a/mongo/src/main/scala/co.blocke.scalajack/mongo/typeadapter/OffsetDateTimeTypeAdapter.scala b/mongo/src/main/scala/co.blocke.scalajack/mongo/typeadapter/OffsetDateTimeTypeAdapter.scala deleted file mode 100644 index 465bcd96..00000000 --- a/mongo/src/main/scala/co.blocke.scalajack/mongo/typeadapter/OffsetDateTimeTypeAdapter.scala +++ /dev/null @@ -1,35 +0,0 @@ -package co.blocke.scalajack -package mongo -package typeadapter - -import model._ - -import scala.collection.mutable -import org.bson._ -import java.time._ -import co.blocke.scala_reflection.RType - -object OffsetDateTimeTypeAdapter extends TypeAdapterFactory with TypeAdapter[OffsetDateTime]: - - def matches(concrete: RType): Boolean = concrete == info - - def makeTypeAdapter(concrete: RType)(implicit taCache: TypeAdapterCache): TypeAdapter[_] = this - - val info = RType.of[OffsetDateTime] - - def read(parser: Parser): OffsetDateTime = - parser.expectNumber(true) match { - case null => null - case dateTimeLong => - OffsetDateTime.ofInstant( - Instant.ofEpochMilli(dateTimeLong.toLong), - ZoneOffset.UTC - ) - } - - def write[WIRE](t: OffsetDateTime, writer: Writer[WIRE], out: mutable.Builder[WIRE, WIRE]): Unit = - t match { - case null => out += new BsonNull().asInstanceOf[WIRE] - case _ => - out += new BsonDateTime(t.toInstant.toEpochMilli).asInstanceOf[WIRE] - } diff --git a/mongo/src/main/scala/co.blocke.scalajack/mongo/typeadapter/StringWrapTypeAdapter.scala b/mongo/src/main/scala/co.blocke.scalajack/mongo/typeadapter/StringWrapTypeAdapter.scala deleted file mode 100644 index 801ffe66..00000000 --- a/mongo/src/main/scala/co.blocke.scalajack/mongo/typeadapter/StringWrapTypeAdapter.scala +++ /dev/null @@ -1,69 +0,0 @@ -package co.blocke.scalajack -package mongo -package typeadapter - -import model._ -import org.bson._ -import co.blocke.scalajack.typeadapter.AnyTypeAdapter -import co.blocke.scala_reflection.RType -import co.blocke.scala_reflection.impl.PrimitiveType -import co.blocke.scalajack.typeadapter._ - -import scala.collection.mutable - -// A TypeAdapter for a type T, which is wrapped in a String, a.k.a. "stringified". -// This is used for JSON Map keys, which must be strings. -case class StringWrapTypeAdapter[T](wrappedTypeAdapter: TypeAdapter[T]) extends TypeAdapter[T]: - - override def isStringish: Boolean = true - val info: RType = wrappedTypeAdapter.info - - def read(parser: Parser): T = - val wrappedValueString = parser.expectString() - wrappedTypeAdapter match { - case value: ScalarTypeAdapter[_] => - value.info match { - case PrimitiveType.Scala_Byte => wrappedValueString.toByte.asInstanceOf[T] - case PrimitiveType.Scala_Int => wrappedValueString.toInt.asInstanceOf[T] - case PrimitiveType.Scala_Long => wrappedValueString.toLong.asInstanceOf[T] - case PrimitiveType.Scala_Double => wrappedValueString.toDouble.asInstanceOf[T] - case PrimitiveType.Scala_Float => wrappedValueString.toFloat.asInstanceOf[T] - case PrimitiveType.Scala_Short => wrappedValueString.toShort.asInstanceOf[T] - case PrimitiveType.Scala_Boolean => wrappedValueString.toBoolean.asInstanceOf[T] - case PrimitiveType.Java_Number => wrappedValueString.toDouble.asInstanceOf[T] - case r: RType if r.name == "scala.math.BigInt" => BigInt(wrappedValueString).asInstanceOf[T] - case r: RType if r.name == "scala.math.BigDecimal" => BigDecimal(wrappedValueString).asInstanceOf[T] - // $COVERAGE-OFF$Currently all scalars in ScalaJack are supported. Here just in case... - case _ => - throw new ScalaJackError( - "Only Scala scalar values are supported as BSON Map keys" - ) - // $COVERAGE-ON$ - } - case value: AnyTypeAdapter => - value.read(parser).asInstanceOf[T] - case _ => - throw new ScalaJackError( - "Only scalar values are supported as BSON Map keys" - ) - } - - def write[WIRE]( - t: T, - writer: Writer[WIRE], - out: mutable.Builder[WIRE, WIRE]): Unit = - val keyValBuilder = BsonBuilder().asInstanceOf[mutable.Builder[Any, WIRE]] - wrappedTypeAdapter.write(t, writer, keyValBuilder) - val result = keyValBuilder.result() match { - case r: BsonBoolean => r.asBoolean().getValue.toString - case r: BsonInt32 => r.asInt32().getValue.toString - case r: BsonInt64 => r.asInt64().getValue.toString - case r: BsonDecimal128 => r.asDecimal128().getValue.toString - case r: BsonDouble => r.asDouble().getValue.toString - case r: BsonNumber => r.asDecimal128().getValue.toString - case r => - throw new ScalaJackError( - "BSON type " + r.getClass.getName + " is not supported as a Map key" - ) - } - writer.writeString(result, out) diff --git a/mongo/src/main/scala/co.blocke.scalajack/mongo/typeadapter/ZonedDateTimeTypeAdapter.scala b/mongo/src/main/scala/co.blocke.scalajack/mongo/typeadapter/ZonedDateTimeTypeAdapter.scala deleted file mode 100644 index c8cfad5d..00000000 --- a/mongo/src/main/scala/co.blocke.scalajack/mongo/typeadapter/ZonedDateTimeTypeAdapter.scala +++ /dev/null @@ -1,38 +0,0 @@ -package co.blocke.scalajack -package mongo -package typeadapter - -import scala.collection.mutable -import org.bson._ -import java.time._ -import co.blocke.scala_reflection.RType - -import model._ - -object ZonedDateTimeTypeAdapter extends TypeAdapterFactory with TypeAdapter[ZonedDateTime]: - - def matches(concrete: RType): Boolean = concrete == info - - def makeTypeAdapter(concrete: RType)(implicit taCache: TypeAdapterCache): TypeAdapter[_] = this - - val info = RType.of[ZonedDateTime] - - def read(parser: Parser): ZonedDateTime = - parser.expectNumber(true) match { - case null => null - case dateTimeLong => - ZonedDateTime.ofInstant( - Instant.ofEpochMilli(dateTimeLong.toLong), - ZoneId.of("UTC") - ) - } - - def write[WIRE](t: ZonedDateTime, writer: Writer[WIRE], out: mutable.Builder[WIRE, WIRE]): Unit = - t match { - case null => - out += new BsonNull().asInstanceOf[WIRE] - case _ => - out += new BsonDateTime( - t.withZoneSameInstant(ZoneId.of("UTC")).toInstant.toEpochMilli - ).asInstanceOf[WIRE] - } diff --git a/mongo/src/test/scala/co.blocke.scalajack/JsonDiff.scala b/mongo/src/test/scala/co.blocke.scalajack/JsonDiff.scala deleted file mode 100644 index 6ab1886a..00000000 --- a/mongo/src/test/scala/co.blocke.scalajack/JsonDiff.scala +++ /dev/null @@ -1,58 +0,0 @@ -package co.blocke.scalajack -package mongo - -import org.json4s.JsonAST.{ JNothing, JObject, JValue } - -object JsonDiff { - - def compare( - left: JValue, - right: JValue, - leftLabel: String = "left", - rightLabel: String = "right"): Seq[JsonDiff] = { - (left, right) match { - case (JObject(leftFields), JObject(rightFields)) => - val allFieldNames = - (leftFields.map(_._1) ++ rightFields.map(_._1)).distinct - allFieldNames.sorted flatMap { fieldName => - val leftFieldValue = leftFields - .collectFirst({ case (`fieldName`, fieldValue) => fieldValue }) - .getOrElse(JNothing) - val rightFieldValue = rightFields - .collectFirst({ case (`fieldName`, fieldValue) => fieldValue }) - .getOrElse(JNothing) - compare(leftFieldValue, rightFieldValue, leftLabel, rightLabel) - } - - // ---- Not used/needed at present, and I have questions about the correct behavior here. Exactly how do you - // "diff" two arrays (not necessarily homogeneous typed)? - // - // case (JArray(leftElements), JArray(rightElements)) => - // (0 until (leftElements.size max rightElements.size)) flatMap { elementIndex => - // val leftElement = leftElements.applyOrElse(elementIndex, (_: Int) => JNothing) - // val rightElement = rightElements.applyOrElse(elementIndex, (_: Int) => JNothing) - // compare(path \ elementIndex, leftElement, rightElement, leftLabel, rightLabel) - // } - - case _ => - if (left == right) { - Seq.empty - } else { - val outerLeft = left - val outerRight = right - Seq(new JsonDiff { - override val left: JValue = outerLeft - override val right: JValue = outerRight - override def toString: String = - s"JsonDiff($leftLabel: $left, $rightLabel: $right)" - }) - } - } - } - -} - -trait JsonDiff { - val left: JValue - val right: JValue -} diff --git a/mongo/src/test/scala/co.blocke.scalajack/JsonMatcher.scala b/mongo/src/test/scala/co.blocke.scalajack/JsonMatcher.scala deleted file mode 100644 index 4a483d64..00000000 --- a/mongo/src/test/scala/co.blocke.scalajack/JsonMatcher.scala +++ /dev/null @@ -1,21 +0,0 @@ -package co.blocke.scalajack -package mongo - -import co.blocke.scalajack.json.JSON -import org.json4s.JsonAST.JValue -import org.json4s.native.JsonMethods._ -import org.json4s.string2JsonInput - -object JsonMatcher { - - def jsonMatches( expected: JSON, actual: JSON ): Boolean = - val diffs = JsonDiff.compare( - parseJValue(expected.asInstanceOf[String]), - parseJValue(actual.asInstanceOf[String]), - "expected", - "actual" - ) - diffs.isEmpty - - implicit def parseJValue(string: String): JValue = parse(string) -} diff --git a/mongo/src/test/scala/co.blocke.scalajack/TestUtil.scala b/mongo/src/test/scala/co.blocke.scalajack/TestUtil.scala deleted file mode 100644 index c8365bdf..00000000 --- a/mongo/src/test/scala/co.blocke.scalajack/TestUtil.scala +++ /dev/null @@ -1,42 +0,0 @@ -package co.blocke.scalajack - -import munit.internal.console - -object TestUtil { - - inline def describe(message: String, color: String = Console.MAGENTA): Unit = println(s"$color$message${Console.RESET}") - inline def pending = describe(" << Test Pending (below) >>", Console.YELLOW) - - def hexStringToByteArray(s: String): Array[Byte] = { - val len = s.length - val data = new Array[Byte](len / 2) - var i = 0 - while ({ - i < len - }) { - data(i / 2) = ((Character.digit(s.charAt(i), 16) << 4) + Character.digit( - s.charAt(i + 1), - 16 - )).toByte - - i += 2 - } - data - } - - // Utility to generate test code quickly - def showException(label: String, fnStr: String, fn: () => Any) = - try { - fn() - } catch { - case x: IndexOutOfBoundsException => throw x - case t: Throwable => - if (!t.getMessage.contains("\n")) - throw t - val msg = "\"\"\"" + t.getMessage().replace("\n", "\n |") + "\"\"\"" - println( - label + " >> " + t.getClass.getName + "\n-----------------------\n" + - s"val msg = $msg.stripMargin\nthe[${t.getClass.getName}] thrownBy $fnStr should have message msg\n" - ) - } -} \ No newline at end of file diff --git a/mongo/src/test/scala/co.blocke.scalajack/mongo/AnySpec.scala b/mongo/src/test/scala/co.blocke.scalajack/mongo/AnySpec.scala deleted file mode 100644 index 6a422652..00000000 --- a/mongo/src/test/scala/co.blocke.scalajack/mongo/AnySpec.scala +++ /dev/null @@ -1,209 +0,0 @@ -package co.blocke.scalajack -package mongo - -import TestUtil._ -import munit._ -import munit.internal.console -import org.bson._ -import scala.jdk.CollectionConverters._ - -case class Something(name: String, stuff: Map[String, Any]) - -class AnySpec extends FunSuite: - - val sjM = ScalaJack(MongoFlavor()) - - object MongoMaster { - val a = new BsonDocument( - List( - new BsonElement("name", new BsonString("Fred")), - new BsonElement( - "stuff", - new BsonDocument( - List( - new BsonElement("a", new BsonInt32(1)), - new BsonElement("b", new BsonBoolean(true)) - ).asJava - ) - ) - ).asJava - ) - - val b = new BsonDocument( - List( - new BsonElement("name", new BsonString("Fred")), - new BsonElement( - "stuff", - new BsonDocument( - List( - new BsonElement("a", new BsonInt32(1)), - new BsonElement( - "b", - new BsonArray( - List(new BsonInt32(4), new BsonInt32(5), new BsonInt32(6)).asJava - ) - ) - ).asJava - ) - ) - ).asJava - ) - - val c = new BsonDocument( - List( - new BsonElement("name", new BsonString("Fred")), - new BsonElement( - "stuff", - new BsonDocument( - List( - new BsonElement("a", new BsonInt32(1)), - new BsonElement( - "b", - new BsonArray( - List( - new BsonDocument( - List( - new BsonElement("x", new BsonString("Fido")), - new BsonElement("y", new BsonBoolean(false)) - ).asJava - ), - new BsonDocument( - List( - new BsonElement("x", new BsonString("Cat")), - new BsonElement("y", new BsonBoolean(true)) - ).asJava - ) - ).asJava - ) - ) - ).asJava - ) - ) - ).asJava - ) - - val e = new BsonDocument( - List( - new BsonElement("name", new BsonString("Fred")), - new BsonElement( - "stuff", - new BsonDocument( - List( - new BsonElement("a", new BsonInt32(1)), - new BsonElement( - "b", - new BsonArray( - List( - new BsonString("foo"), - new BsonNull(), - new BsonString("bar") - ).asJava - ) - ) - ).asJava - ) - ) - ).asJava - ) - - val f = new BsonDocument( - List( - new BsonElement("name", new BsonString("Fred")), - new BsonElement( - "stuff", - new BsonDocument( - List( - new BsonElement("a", new BsonInt32(1)), - new BsonElement("b", new BsonDouble(1.23)) - ).asJava - ) - ) - ).asJava - ) - - val g = new BsonDocument( - List( - new BsonElement("name", new BsonString("Fred")), - new BsonElement( - "stuff", - new BsonDocument( - List( - new BsonElement("a", new BsonInt32(1)), - new BsonElement("b", new BsonInt64(25L)) - ).asJava - ) - ) - ).asJava - ) - } - - object ScalaMaster { - val a = Something("Fred", Map("a" -> 1, "b" -> true)) - val b = Something("Fred", Map("a" -> 1, "b" -> List(4, 5, 6))) - val c = Something( - "Fred", - Map( - "a" -> 1, - "b" -> List( - Map("x" -> "Fido", "y" -> false), - Map("x" -> "Cat", "y" -> true) - ) - ) - ) - val e = Something("Fred", Map("a" -> 1, "b" -> List("foo", null, "bar"))) - val f = Something("Fred", Map("a" -> 1, "b" -> 1.23)) - val g = Something("Fred", Map("a" -> 1, "b" -> 25L)) - } - - test("Any 1") { - describe( - "-------------------------\n: Any Tests (MongoDB) :\n-------------------------", Console.BLUE - ) - describe("Render Tests") - assertEquals(sjM.render(ScalaMaster.a), MongoMaster.a) - } - - test("Any 2") { - assertEquals(sjM.render(ScalaMaster.b), MongoMaster.b) - } - - test("Any 3") { - assertEquals(sjM.render(ScalaMaster.c), MongoMaster.c) - } - - test("Any 4") { - assertEquals(sjM.render(ScalaMaster.e), MongoMaster.e) - } - - test("Any 5") { - assertEquals(sjM.render(ScalaMaster.f), MongoMaster.f) - } - - test("Any 6") { - assertEquals(sjM.render(ScalaMaster.g), MongoMaster.g) - } - - test("Any 1") { - describe("Read Tests") - assertEquals(sjM.read[Something](MongoMaster.a), ScalaMaster.a) - } - - test("Any 2") { - assertEquals(sjM.read[Something](MongoMaster.b), ScalaMaster.b) - } - - test("Any 3") { - assertEquals(sjM.read[Something](MongoMaster.c), ScalaMaster.c) - } - - test("Any 4") { - assertEquals(sjM.read[Something](MongoMaster.e), ScalaMaster.e) - } - - test("Any 5") { - assertEquals(sjM.read[Something](MongoMaster.f), ScalaMaster.f) - } - - test("Any 6") { - assertEquals(sjM.read[Something](MongoMaster.g), ScalaMaster.g) - } diff --git a/mongo/src/test/scala/co.blocke.scalajack/mongo/ConverterSpec.scala b/mongo/src/test/scala/co.blocke.scalajack/mongo/ConverterSpec.scala deleted file mode 100644 index a74d5897..00000000 --- a/mongo/src/test/scala/co.blocke.scalajack/mongo/ConverterSpec.scala +++ /dev/null @@ -1,134 +0,0 @@ -package co.blocke.scalajack -package mongo - -import co.blocke.scalajack.json.JsonFlavor -import co.blocke.scalajack.model.JackFlavor -import TestUtil._ -import munit._ -import munit.internal.console - -import yaml._ -import json._ -import json4s._ -import delimited._ -import Converters._ -import mongo.Converters._ -import org.json4s._ -import org.bson._ -import scala.jdk.CollectionConverters._ - -trait HumanM -case class PersonM(@DBKey name: String, age: Int) extends HumanM -case class TypeyM[T](thing: T) { - type foom = T -} - -class ConverterSpec extends FunSuite: - - implicit val sj: JsonFlavor = ScalaJack() - implicit val sjY: JackFlavor[YAML] = ScalaJack(YamlFlavor()) - implicit val sjV: JackFlavor[JValue] = ScalaJack(Json4sFlavor()) - implicit val sjD: JackFlavor[DELIMITED] = ScalaJack(DelimitedFlavor()) - implicit val sjB: JackFlavor[BsonValue] = ScalaJack(MongoFlavor()) - - val simple: PersonM = PersonM("Fred", 34) - val complex: TypeyM[HumanM] = TypeyM[HumanM](PersonM("Fred", 34)) - val simpleJson = """{"name":"Fred","age":34}""".asInstanceOf[JSON] - val complexJson = """{"foom":"co.blocke.scalajack.mongo.PersonM","thing":{"name":"Fred","age":34}}""".asInstanceOf[JSON] - val simpleJson4s = JObject(List(("name", JString("Fred")), ("age", JInt(34)))) - val complexJson4s = JObject( - List( - ("foom", JString("co.blocke.scalajack.mongo.PersonM")), - ("thing", JObject(List(("name", JString("Fred")), ("age", JInt(34))))) - )) - val simpleYaml = """name: Fred - |age: 34 - |""".stripMargin.asInstanceOf[YAML] - val complexYaml = """foom: co.blocke.scalajack.mongo.PersonM - |thing: - | name: Fred - | age: 34 - |""".stripMargin.asInstanceOf[YAML] - val simpleDelimited = "Fred,34".asInstanceOf[DELIMITED] - val simpleMongo = new BsonDocument( - List( - new BsonElement("_id", new BsonString("Fred")), - new BsonElement("age", new BsonInt32(34)) - ).asJava - ) - val complexMongo = new BsonDocument( - List( - new BsonElement("foom", new BsonString("co.blocke.scalajack.mongo.PersonM")), - new BsonElement("thing", - new BsonDocument( - List( - new BsonElement("_id", new BsonString("Fred")), - new BsonElement("age", new BsonInt32(34)) - ).asJava - )) - ).asJava - ) - - test("mapMongo") { - describe( - "-------------------------------------\n: Converter Mapping Tests (Mongo) :\n-------------------------------------", Console.BLUE - ) - assertEquals(simpleMongo.mapMongo[PersonM](_.copy(age = 45)), new BsonDocument( - List( - new BsonElement("_id", new BsonString("Fred")), - new BsonElement("age", new BsonInt32(45)) - ).asJava - )) - } - - test("mapMongoTo") { - assertEquals(simpleMongo.mapMongoTo[PersonM, YAML](sjY)(_.copy(age = 45)), """name: Fred - |age: 45 - |""".stripMargin.asInstanceOf[YAML]) - } - - test("toMongo") { - describe( - "-----------------------------------------\n: Convenience \"to/from\" Tests (Mongo) :\n-----------------------------------------", Console.BLUE - ) - assertEquals(toMongo[PersonM](simple), simpleMongo) - assertEquals(toMongo[TypeyM[HumanM]](complex), complexMongo) - } - - test("fromMongo") { - assertEquals(simpleMongo.fromMongo[PersonM], simple) - assertEquals(complexMongo.fromMongo[TypeyM[HumanM]], complex) - } - - test("mongoToJson") { - describe( - "------------------------------\n: Converters Tests (Mongo) :\n------------------------------", Console.BLUE - ) - assertEquals(simpleMongo.mongoToJson[PersonM], simpleJson) - assertEquals(complexMongo.mongoToJson[TypeyM[HumanM]], complexJson) - } - - test("mongoToJson4s") { - assertEquals(simpleMongo.mongoToJson4s[PersonM], simpleJson4s) - assertEquals(complexMongo.mongoToJson4s[TypeyM[HumanM]], complexJson4s) - } - - test("mongoToYaml") { - assertEquals(simpleMongo.mongoToYaml[PersonM], simpleYaml) - assertEquals(complexMongo.mongoToYaml[TypeyM[HumanM]], complexYaml) - } - - test("jsonToMongo") { - assertEquals(simpleJson.jsonToMongo[PersonM], simpleMongo) - assertEquals(complexJson.jsonToMongo[TypeyM[HumanM]], complexMongo) - } - - test("json4sToMongo") { - assertEquals(simpleJson4s.json4sToMongo[PersonM], simpleMongo) - assertEquals(complexJson4s.json4sToMongo[TypeyM[HumanM]], complexMongo) - } - - test("yamlToMongo") { - assertEquals(simpleJson4s.json4sToMongo[PersonM], simpleMongo) - assertEquals(complexJson4s.json4sToMongo[TypeyM[HumanM]], complexMongo) - } \ No newline at end of file diff --git a/mongo/src/test/scala/co.blocke.scalajack/mongo/Custom.scala b/mongo/src/test/scala/co.blocke.scalajack/mongo/Custom.scala deleted file mode 100644 index 21411e83..00000000 --- a/mongo/src/test/scala/co.blocke.scalajack/mongo/Custom.scala +++ /dev/null @@ -1,159 +0,0 @@ -package co.blocke.scalajack -package mongo - -import TestUtil._ -import munit._ -import munit.internal.console -import org.bson._ -import scala.jdk.CollectionConverters._ -import co.blocke.scala_reflection.RType - -class Custom extends FunSuite: - - test("Supports withAdapters") { - describe( - "----------------------------\n: Custom Tests (MongoDB) :\n----------------------------", Console.BLUE - ) - val sj = ScalaJack(MongoFlavor()).withAdapters(PhoneAdapter) - val dbo = new BsonDocument( - List( - new BsonElement("_id", new BsonString("Fred")), - new BsonElement("phone", new BsonString("123-456-7890")) - ).asJava - ) - assertEquals(dbo.toJson, """{"_id": "Fred", "phone": "123-456-7890"}""") - val read = sj.read[Person](dbo) - assertEquals(read, Person("Fred", "1234567890".asInstanceOf[Phone])) - assertEquals(sj.render(read), dbo) - } - - test("Supports withHints") { - val sj = ScalaJack(MongoFlavor()).withHints( - RType.of[Address] -> "addr_kind", - RType.of[Demographic] -> "demo" - ) - val dbo = new BsonDocument( - List( - new BsonElement( - "demo", - new BsonString("co.blocke.scalajack.mongo.USDemographic") - ), - new BsonElement("_id", new BsonString("34")), - new BsonElement( - "address", - new BsonDocument( - List( - new BsonElement( - "addr_kind", - new BsonString("co.blocke.scalajack.mongo.USAddress") - ), - new BsonElement("street", new BsonString("123 Main")), - new BsonElement("city", new BsonString("New York")), - new BsonElement("state", new BsonString("NY")), - new BsonElement("postalCode", new BsonString("39822")) - ).asJava - ) - ) - ).asJava - ) - assertEquals(dbo.toJson, - """{"demo": "co.blocke.scalajack.mongo.USDemographic", "_id": "34", "address": {"addr_kind": "co.blocke.scalajack.mongo.USAddress", "street": "123 Main", "city": "New York", "state": "NY", "postalCode": "39822"}}""" - ) - val read = sj.read[Demographic](dbo) - assertEquals(read, USDemographic("34", USAddress("123 Main", "New York", "NY", "39822"))) - assertEquals(sj.render(read), dbo) - } - - // withHintModifiers tested in another case - - test("Supports withDefaultHint") { - val sj = ScalaJack(MongoFlavor()).withDefaultHint("kind") - val dbo = new BsonDocument( - List( - new BsonElement( - "kind", - new BsonString("co.blocke.scalajack.mongo.USDemographic") - ), - new BsonElement("_id", new BsonString("34")), - new BsonElement( - "address", - new BsonDocument( - List( - new BsonElement( - "kind", - new BsonString("co.blocke.scalajack.mongo.USAddress") - ), - new BsonElement("street", new BsonString("123 Main")), - new BsonElement("city", new BsonString("New York")), - new BsonElement("state", new BsonString("NY")), - new BsonElement("postalCode", new BsonString("39822")) - ).asJava - ) - ) - ).asJava - ) - assertEquals(dbo.toJson, - """{"kind": "co.blocke.scalajack.mongo.USDemographic", "_id": "34", "address": {"kind": "co.blocke.scalajack.mongo.USAddress", "street": "123 Main", "city": "New York", "state": "NY", "postalCode": "39822"}}""" - ) - val read = sj.read[Demographic](dbo) - assertEquals(read, USDemographic("34", USAddress("123 Main", "New York", "NY", "39822")) ) - assertEquals(sj.render(read), dbo) - } - - test( - "Provide a default object if the object specified in the type hint is unknown (parseOrElse)" - ) { - val sj = ScalaJack(MongoFlavor()) - .parseOrElse((RType.of[Address] -> RType.of[DefaultAddress])) - val dbo = new BsonDocument( - List( - new BsonElement( - "_hint", - new BsonString("co.blocke.scalajack.mongo.USDemographic") - ), - new BsonElement("_id", new BsonString("34")), - new BsonElement( - "address", - new BsonDocument( - List( - new BsonElement( - "_hint", - new BsonString("co.blocke.scalajack.mongo.UnknownAddress") - ), - new BsonElement("street", new BsonString("123 Main")), - new BsonElement("city", new BsonString("New York")), - new BsonElement("state", new BsonString("NY")), - new BsonElement("postalCode", new BsonString("39822")) - ).asJava - ) - ) - ).asJava - ) - val dboDefault = new BsonDocument( - List( - new BsonElement( - "_hint", - new BsonString("co.blocke.scalajack.mongo.USDemographic") - ), - new BsonElement("_id", new BsonString("34")), - new BsonElement( - "address", - new BsonDocument( - List( - new BsonElement( - "_hint", - new BsonString("co.blocke.scalajack.mongo.DefaultAddress") - ), - new BsonElement("postalCode", new BsonString("39822")) - ).asJava - ) - ) - ).asJava - ) - assertEquals(dbo.toJson, - """{"_hint": "co.blocke.scalajack.mongo.USDemographic", "_id": "34", "address": {"_hint": "co.blocke.scalajack.mongo.UnknownAddress", "street": "123 Main", "city": "New York", "state": "NY", "postalCode": "39822"}}""" - ) - val read = sj.read[Demographic](dbo) - assertEquals(read, USDemographic("34", DefaultAddress("39822"))) - assertEquals(sj.render(read), dboDefault) - } diff --git a/mongo/src/test/scala/co.blocke.scalajack/mongo/LooseChange.scala b/mongo/src/test/scala/co.blocke.scalajack/mongo/LooseChange.scala deleted file mode 100644 index 9d4b8493..00000000 --- a/mongo/src/test/scala/co.blocke.scalajack/mongo/LooseChange.scala +++ /dev/null @@ -1,376 +0,0 @@ -package co.blocke.scalajack -package mongo - -import co.blocke.scalajack.model.JackFlavor -import TestUtil._ -import munit._ -import munit.internal.console -import org.bson._ -import JsonMatcher._ -import java.time.{ OffsetDateTime, ZonedDateTime } - -import scala.jdk.CollectionConverters._ - -case class MyNumbers(d: BigDecimal, n: Number) -case class NumberBoom(x: BigInt) - -class LooseChange extends FunSuite: - val sjM: JackFlavor[BsonValue] = ScalaJack(MongoFlavor()) - - object MongoMaster { - val a = new BsonDocument( - List( - new BsonElement("name", new BsonString("Fred")), - new BsonElement( - "stuff", - new BsonDocument( - List( - new BsonElement("a", new BsonInt32(1)), - new BsonElement("b", new BsonBoolean(true)) - ).asJava - ) - ) - ).asJava - ) - - val b = new BsonDocument( - List( - new BsonElement("name", new BsonString("Fred")), - new BsonElement( - "stuff", - new BsonDocument( - List( - new BsonElement("a", new BsonInt32(1)), - new BsonElement( - "b", - new BsonArray( - List(new BsonInt32(4), new BsonInt32(5), new BsonInt32(6)).asJava - ) - ) - ).asJava - ) - ) - ).asJava - ) - - val c = new BsonDocument( - List( - new BsonElement("name", new BsonString("Fred")), - new BsonElement( - "stuff", - new BsonDocument( - List( - new BsonElement("a", new BsonInt32(1)), - new BsonElement( - "b", - new BsonArray( - List( - new BsonDocument( - List( - new BsonElement("x", new BsonString("Fido")), - new BsonElement("y", new BsonBoolean(false)) - ).asJava - ), - new BsonDocument( - List( - new BsonElement("x", new BsonString("Cat")), - new BsonElement("y", new BsonBoolean(true)) - ).asJava - ) - ).asJava - ) - ) - ).asJava - ) - ) - ).asJava - ) - - val e = new BsonDocument( - List( - new BsonElement("name", new BsonString("Fred")), - new BsonElement( - "stuff", - new BsonDocument( - List( - new BsonElement("a", new BsonInt32(1)), - new BsonElement( - "b", - new BsonArray( - List( - new BsonString("foo"), - new BsonNull(), - new BsonString("bar") - ).asJava - ) - ) - ).asJava - ) - ) - ).asJava - ) - - val f = new BsonDocument( - List( - new BsonElement("name", new BsonString("Fred")), - new BsonElement( - "stuff", - new BsonDocument( - List( - new BsonElement("a", new BsonInt32(1)), - new BsonElement("b", new BsonDouble(1.23)) - ).asJava - ) - ) - ).asJava - ) - - val g = new BsonDocument( - List( - new BsonElement("name", new BsonString("Fred")), - new BsonElement( - "stuff", - new BsonDocument( - List( - new BsonElement("a", new BsonInt32(1)), - new BsonElement("b", new BsonInt64(25L)) - ).asJava - ) - ) - ).asJava - ) - } - - object ScalaMaster { - val a: Something = Something("Fred", Map("a" -> 1, "b" -> true)) - val b: Something = Something("Fred", Map("a" -> 1, "b" -> List(4, 5, 6))) - val c: Something = Something( - "Fred", - Map( - "a" -> 1, - "b" -> List( - Map("x" -> "Fido", "y" -> false), - Map("x" -> "Cat", "y" -> true) - ) - ) - ) - val e: Something = - Something("Fred", Map("a" -> 1, "b" -> List("foo", null, "bar"))) - val f: Something = Something("Fred", Map("a" -> 1, "b" -> 1.23)) - val g: Something = Something("Fred", Map("a" -> 1, "b" -> 25L)) - } - - test("Handles null value") { - describe( - "----------------------------\n: Loose Change (MongoDB) :\n----------------------------" - ) - val dbo = new BsonDocument( - List( - new BsonElement("name", new BsonString("Fred")), - new BsonElement( - "stuff", - new BsonDocument( - List( - new BsonElement("a", new BsonInt32(1)), - new BsonElement("b", new BsonInt32(15)) - ).asJava - ) - ) - ).asJava - ) - assertEquals(sjM.read[Something](dbo), - Something("Fred", Map("a" -> 1, "b" -> 15)) - ) - } - - test("Should blow up for unsupported BSON type") { - val dbo = new BsonDocument( - List( - new BsonElement("name", new BsonString("Fred")), - new BsonElement( - "stuff", - new BsonDocument( - List(new BsonElement("a", new BsonJavaScript("code here"))).asJava - ) - ) - ).asJava - ) - interceptMessage[ScalaJackError]("""BSON type org.bson.BsonJavaScript is not currently supported in ScalaJack."""){ - sjM.read[Something](dbo) - } - } - - test("Field name remapping must work") { - val mfp = MapFactor("wonder", 25L, 3, "hungry") - val dbo = sjM.render(mfp) - assertEquals(dbo.asDocument.toJson, - """{"foo_bar": "wonder", "a_b": {"$numberLong": "25"}, "count": 3, "big_mac": "hungry"}""" - ) - assertEquals(sjM.read[MapFactor](dbo), mfp) - } - - test("Field name remapping on dbkey must work") { - // val mfp = MapFactorId2("wonder", 25L, 1, 3) - val mfp = MapFactorId("wonder", 25L, 3, "hungry") - val dbo = sjM.render(mfp) - assertEquals(dbo.asDocument.toJson, - """{"_id": "wonder", "a_b": {"$numberLong": "25"}, "count": 3, "big_mac": "hungry"}""" - ) - assertEquals(sjM.read[MapFactorId](dbo), mfp) - } - - test("Field name remapping on dbkey with multi-part keys must work") { - val mfp = MapFactorId2("wonder", 25L, 1, 3, "hungry") - val dbo = sjM.render(mfp) - assert(jsonMatches( - dbo.asDocument.toJson.asInstanceOf[co.blocke.scalajack.json.JSON], - """{"_id": {"foo_bar": "wonder", "a_b": {"$numberLong": "25"}, "hey": 1}, "count": 3, "big_mac": "hungry"}""".asInstanceOf[co.blocke.scalajack.json.JSON] - )) - assertEquals(sjM.read[MapFactorId2](dbo), mfp) - } - - test("Number, Decimal, and BigInt handling") { - val my = MyNumbers(BigDecimal(123.45), 15) - val d = sjM.render(my) - assertEquals(d.asDocument.toJson, - """{"d": {"$numberDecimal": "123.45"}, "n": 15}""" - ) - assertEquals(sjM.read[MyNumbers](d), my) - - val boom = NumberBoom(BigInt(5)) - interceptMessage[ScalaJackError]("BigInt is currently an unsupported datatype for MongoDB serialization"){ - sjM.render(boom) - } - } - - test("Nulls") { - val prim = PrimitiveLists(null, null, null, null, null) - assertEquals(sjM.read[PrimitiveLists](sjM.render(prim)), prim) - - val os = OneSub2("foo", flipflop = false, null) - interceptMessage[ScalaJackError]("Class co.blocke.scalajack.mongo.PrimitiveLists missing required fields: bools, chars, doubles, ints, longs"){ - sjM.read[PrimitiveLists](sjM.render(os)) - } - - val os2 = - OneSub2("foo", flipflop = false, Map(null.asInstanceOf[String] -> 5)) - interceptMessage[ScalaJackError]("Map keys cannot be null."){ - sjM.render(os2) - } - - val out = null - assertEquals(sjM.render[OneSub2](out).isNull, true) - - val mapDoc = new BsonDocument( - List( - new BsonElement("i", new BsonInt32(5)), - new BsonElement("items", new BsonNull()) - ).asJava - ) - assertEquals(sjM.read[BagMap[Int]](mapDoc), BagMap(5, null)) - - assertEquals(sjM.read[OneSub2](null), null) - - val times = Times(null, null) - val d = sjM.render(times) - assertEquals(d.asDocument().toJson, """{"offset": null, "zoned": null}""") - assertEquals(sjM.read[Times](d), times) - val d2 = new BsonDocument( - List( - new BsonElement("offset", new BsonNull()), - new BsonElement("zoned", new BsonNull()) - ).asJava - ) - assertEquals(sjM.read[Times](d2), times) - } - - test("Good time values") { - val t = Times( - OffsetDateTime.parse("2007-12-03T10:15:30+01:00"), - ZonedDateTime.parse("2007-12-03T10:15:30Z[UTC]") - ) - val d = sjM.render(t) - val t2 = sjM.read[Times](d) - // Extra gymnastics because the read/rendered OffsetDateTime, while the same actual instant, isn't the same value so comparison fails - assert(t2.offset.atZoneSameInstant(java.time.ZoneId.of("Europe/Paris")) == t.offset.atZoneSameInstant(java.time.ZoneId.of("Europe/Paris"))) - assertEquals(t2.zoned == t.zoned, true) - } - - test("Bad expected values") { - val d = new BsonDocument( - List( - new BsonElement("name", new BsonString("Fred")), - new BsonElement("big", new BsonDouble(123.45)) - ).asJava - ) - interceptMessage[ScalaJackError]("Cannot parse an Long from value"){ - sjM.read[OneSub1](d) - } - } - - test("Non-hint hint-labeled field") { - val d = new BsonDocument( - List( - new BsonElement("num", new BsonInt32(3)), - new BsonElement( - "s", - new BsonDocument( - List( - new BsonElement("_hint", new BsonInt32(45)), - new BsonElement("size", new BsonInt32(34)) - ).asJava - ) - ) - ).asJava - ) - interceptMessage[ScalaJackError]("Hint value _hint must be a string value"){ - sjM.read[StrangeWrapper](d) - } - } - - test("Can't find trait hint") { - val d = new BsonDocument( - List( - new BsonElement("num", new BsonInt32(3)), - new BsonElement( - "s", - new BsonDocument( - List(new BsonElement("size", new BsonInt32(34))).asJava - ) - ) - ).asJava - ) - interceptMessage[ScalaJackError]("Type hint '_hint' not found"){ - sjM.read[StrangeWrapper](d) - } - } - - test("Enums as ints work") { - val sj = ScalaJack(MongoFlavor()).enumsAsInts() - val n = Numy(5, Num.B) - val d = sj.render(n) - assertEquals(d.asDocument.toJson, """{"age": 5, "num": 1}""") - assertEquals(sj.read[Numy](d), n) - } - - test("Failure due to empty BsonBuilder") { - interceptMessage[ScalaJackError]("No value set for internal mongo builder"){ - BsonBuilder().result() - } - } - - test("Parse only") { - val d = new BsonDocument( - List( - new BsonElement("num", new BsonInt32(3)), - new BsonElement( - "s", - new BsonDocument( - List(new BsonElement("size", new BsonInt32(34))).asJava - ) - ) - ).asJava - ) - val p = sjM.parse(d) - assertEquals(p.isInstanceOf[BsonParser], true) - } \ No newline at end of file diff --git a/mongo/src/test/scala/co.blocke.scalajack/mongo/MapKeys.scala b/mongo/src/test/scala/co.blocke.scalajack/mongo/MapKeys.scala deleted file mode 100644 index 827bd4b6..00000000 --- a/mongo/src/test/scala/co.blocke.scalajack/mongo/MapKeys.scala +++ /dev/null @@ -1,96 +0,0 @@ -package co.blocke.scalajack -package mongo - -import TestUtil._ -import munit._ -import munit.internal.console -import java.util.UUID -import org.bson.BsonDocument - -case class IntKey(m: Map[Int, Int]) -case class LongKey(m: Map[Long, Int]) -case class DoubleKey(m: Map[Double, Int]) -case class FloatKey(m: Map[Float, Int]) -case class ShortKey(m: Map[Short, Int]) -case class BigDecimalKey(m: Map[BigDecimal, Int]) -case class ByteKey(m: Map[Byte, Int]) -case class BooleanKey(m: Map[Boolean, Int]) -case class CharKey(m: Map[Char, Int]) -case class UUIDKey(m: Map[UUID, Int]) -case class NumberKey(m: Map[java.lang.Number, Int]) - -case class WithMap(m: Map[Map[Int, Int], Int]) - -class MapKeys extends FunSuite: - - val sj = ScalaJack(MongoFlavor()) - - test("Renders wrapped non-String scalar Map keys") { - describe( - "------------------------\n: Map Keys (MongoDB) :\n------------------------", Console.BLUE - ) - val short: Short = 5 - val bte: Byte = 5 - val a = IntKey(Map(5 -> 2)) - val b = LongKey(Map(5L -> 2)) - val c = DoubleKey(Map(5.3 -> 2)) - val d = FloatKey(Map(5F -> 2)) - val e = ShortKey(Map(short -> 2)) - val f = BigDecimalKey(Map(BigDecimal(5.1) -> 2)) - val g = ByteKey(Map(bte -> 2)) - val h = BooleanKey(Map(true -> 2)) - val i = CharKey(Map('c' -> 2)) - val j = NumberKey(Map(5.asInstanceOf[Number] -> 2)) - - val docs = List( - sj.render(a), - sj.render(b), - sj.render(c), - sj.render(d), - sj.render(e), - sj.render(f), - sj.render(g), - sj.render(h), - sj.render(i), - sj.render(j), - ) - assertEquals(docs.map(_.asDocument.toJson), - List( - """{"m": {"5": 2}}""", - """{"m": {"5": 2}}""", - """{"m": {"5.3": 2}}""", - """{"m": {"5.0": 2}}""", - """{"m": {"5": 2}}""", - """{"m": {"5.1": 2}}""", - """{"m": {"5": 2}}""", - """{"m": {"true": 2}}""", - """{"m": {"c": 2}}""", - """{"m": {"5": 2}}""" - ) - ) - - assertEquals(sj.read[IntKey](docs(0)), a) - assertEquals(sj.read[LongKey](docs(1)), b) - assertEquals(sj.read[DoubleKey](docs(2)), c) - assertEquals(sj.read[FloatKey](docs(3)), d) - assertEquals(sj.read[ShortKey](docs(4)), e) - assertEquals(sj.read[BigDecimalKey](docs(5)), f) - assertEquals(sj.read[ByteKey](docs(6)), g) - assertEquals(sj.read[BooleanKey](docs(7)), h) - assertEquals(sj.read[CharKey](docs(8)), i) - assertEquals(sj.read[NumberKey](docs(9)), j) - } - - test("Traps attempt to read non-scalar Map key") { - val d = BsonDocument.parse("""{"m":{"bogus":5}}""") - interceptMessage[ScalaJackError]("Only scalar values are supported as BSON Map keys"){ - sj.read[WithMap](d) - } - } - - test("Traps attempt to write non-scalar Map key") { - val m = WithMap(Map(Map(1 -> 2) -> 3)) - interceptMessage[ScalaJackError]("BSON type org.bson.BsonDocument is not supported as a Map key"){ - sj.render(m) - } - } diff --git a/mongo/src/test/scala/co.blocke.scalajack/mongo/Model.scala b/mongo/src/test/scala/co.blocke.scalajack/mongo/Model.scala deleted file mode 100644 index 522a9248..00000000 --- a/mongo/src/test/scala/co.blocke.scalajack/mongo/Model.scala +++ /dev/null @@ -1,272 +0,0 @@ -package co.blocke.scalajack -package mongo - -import java.time._ - -import model._ -import org.bson.types.ObjectId - -import scala.collection.mutable -import scala.util.Try -import co.blocke.scala_reflection.RType -import co.blocke.scala_reflection.info.AliasInfo - -object Num extends Enumeration { - val A, B, C = Value -} - -case class Bar[A, B](a: A, b: B) -case class Zoo[U](name: String, z: U) //stuff:Bar[U,String]) -case class Hey(age: Int) - -case class Wrap[T, U](name: String, data: T, stuff: U) -case class Carry[V](s: String, w: Wrap[V, String]) -case class CarryList[V](li: List[String], w: Wrap[V, String]) -case class CarryOpt[V](li: List[String], w: Wrap[V, String]) -case class BagList[Y](s: String, many: List[Y]) -case class BagMap[Y](i: Int, items: Map[String, Y]) -case class BagOpt[Y](i: Int, maybe: Option[Y]) -case class Truck[Z](s: Z, t: Two) - -case class AllOpt( - one: Option[String], - two: Option[String], - three: Option[String]) - -case class PrimitiveLists( - ints: List[Int], - longs: List[Long], - bools: List[Boolean], - chars: List[Char], - doubles: List[Double]) - -case class One( - name: String, - stuff: List[String], - more: List[Two], - nest: Two, - maybe: Option[String], - mymap: Map[String, Int], - flipflop: Boolean, - big: Long, - num: Num.Value, - age: Int) { - val foo: String = "yikes!" -} - -case class OneSub1(name: String, big: Long, maybe: Option[String]) - -case class OneSub2(name: String, flipflop: Boolean, mymap: Map[String, Int]) - -case class Two(foo: String, bar: Boolean) - -case class Three(name: String, two: Num.Value, pp: Pop) - -case class Four(stuff: List[String], things: Map[String, Int]) - -case class Five(@DBKey name: String, two: Two) - -case class Six(@DBKey name: String, @DBKey num: Int, two: Two) - -case class Seven(@DBKey _id: ObjectId, two: Two) - -case class Numy(age: Int, num: Num.Value) - -case class UuidThing( - name: String, - uuid: java.util.UUID, - many: List[java.util.UUID], - maybe: Option[java.util.UUID]) - -case class JodaThing( - name: String, - dt: OffsetDateTime, - many: List[OffsetDateTime], - maybe: Option[OffsetDateTime]) - -trait Pop { - def go(): Unit -} -trait Tart[T] { - val yum: T -} -trait Soup[A] { - val sweet: A -} - -case class Wow1(a: String, b: Int) extends Pop { - def go(): Unit = println("--1--") -} -case class Wow2(x: String, y: Int) extends Pop { - def go(): Unit = println("--2--") -} -case class Cruton[U](i: Int, sweet: U) extends Soup[U] -case class Toast[D](g: Int, yum: D) extends Tart[D] -case class Bun[R](g: Int, yum: R) extends Tart[R] -case class Breakfast[K](y: Boolean, bread: Tart[K]) - -case class Animal(name: String, legs: Int) - -// Value class support w/custom rendering -class Wrapper(val underlying: Int) extends AnyVal -case class ValSupport(name: String, wrap: Wrapper, more: Boolean) - -case class ListValSupport(name: String, wrap: List[Wrapper], more: Boolean) -case class OptValSupport(name: String, wrap: Option[Wrapper]) -case class MapValSupport(name: String, wrap: Map[String, Wrapper]) - -// Test Lists -case class ListList(name: String, stuff: List[List[Animal]]) -case class ListListList(name: String, stuff: List[List[List[Animal]]]) -case class ListOpt(name: String, stuff: List[Option[Animal]]) -case class ListMap(name: String, stuff: List[Map[String, Animal]]) - -// Test nested Options+Variants w/other collections -case class OpOp(name: String, opts: Option[Option[Animal]]) -case class OpList(name: String, opList: Option[List[Animal]]) -case class OpListList(name: String, opListList: Option[List[List[Animal]]]) -case class OpMap(name: String, opMap: Option[Map[String, Animal]]) - -// Test nested Maps+Variants w/other collections -case class MapList(name: String, mapList: Map[String, List[Animal]]) -case class MapListList(name: String, mapList: Map[String, List[List[Animal]]]) -case class MapOpt(name: String, mapOpt: Map[String, Option[Animal]]) -case class MapMap(name: String, mapmap: Map[String, Map[String, Animal]]) - -case class Foo(name: String, stuff: List[String]) - -trait PetAnimal { - val name: String -} -case class Dog(name: String) extends PetAnimal -case class Cat(name: String) extends PetAnimal -trait Pet { - val kind: PetAnimal - val food: String -} -case class NicePet(kind: PetAnimal, food: String) extends Pet -case class GrumpyPet(kind: PetAnimal, food: String) extends Pet -case class WithDefaults( - name: String, - age: Int = 50, - num: Option[Int], - hasStuff: Option[Boolean] = Some(true), - pet: Pet = NicePet(Dog("Fido"), "bones")) - -object CustomVC { - - // val typeAdapter = StringTypeAdapter andThen - - // def stringToDateTime - // - // def read:PartialFunction[(KindMarker,_), Any] = { - // case (jk:JsonKind,js:String) => DateTimeFormat.forPattern("MMMM, yyyy").parseDateTime(js) - // case (mk:MongoKind,bdt:BsonDateTime) => new DateTime(bdt.getValue) - // } - // def render:PartialFunction[(KindMarker,_), Any] = { - // case (jk:JsonKind,dt:DateTime) => '"'+DateTimeFormat.forPattern("MMMM, yyyy").print(dt)+'"' - // case (mk:MongoKind,dt:DateTime) => BsonDateTime(dt.toDate) - // } -} - -class CustomVC(val underlying: YearMonth) extends AnyVal { - override def toString = s"CustomVC($underlying)" -} -case class SomethingSpecial(what: String, when: CustomVC) - -case class SampleZonedDateTime(o1: ZonedDateTime, o2: ZonedDateTime) - -trait Address { val postalCode: String } -case class USAddress( - street: String, - city: String, - state: String, - postalCode: String) - extends Address -case class CanadaAddress( - street: String, - city: String, - province: String, - postalCode: String) - extends Address -case class DefaultAddress(postalCode: String) extends Address -trait Demographic { val address: Address } -case class USDemographic(@DBKey age: String, address: Address) - extends Demographic - -opaque type Phone >: Null = String - -// Override just Phone -object PhoneAdapter extends TypeAdapterFactory with TypeAdapter[Phone]: - def matches(concrete: RType): Boolean = - concrete match { - case a: AliasInfo if a.name == "Phone" => true - case _ => false - } - def makeTypeAdapter(concrete: RType)(implicit taCache: TypeAdapterCache): TypeAdapter[Phone] = this - val info = RType.of[Phone] - override def isStringish: Boolean = true - - def read(parser: Parser): Phone = - parser.expectString() match { - case null => null.asInstanceOf[Phone] - case s: String => s.replaceAll("-", "").asInstanceOf[Phone] - } - - def write[WIRE]( - t: Phone, - writer: Writer[WIRE], - out: mutable.Builder[WIRE, WIRE]): Unit = t match { - case null => writer.writeNull(out) - case _ => - writer.writeString( - "%s-%s-%s".format(t.toString.substring(0, 3), t.toString.substring(3, 6), t.toString.substring(6)), - out - ) - } - -case class Person(@DBKey name: String, phone: Phone) - -case class Loose(a: Char, b: Float, c: Short, d: Byte) - -case class MapFactor( - @Change(name = "foo_bar") fooBar:String, - @Change(name = "a_b") thingy: Long, - count: Int, - @Change(name = "big_mac") bigMac:String) -case class MapFactorId( - @DBKey @Change(name = "foo_bar") fooBar:String, - @Change(name = "a_b") thingy: Long, - count: Int, - @Change(name = "big_mac") bigMac: String) -case class MapFactorId2( - @DBKey @Change(name = "foo_bar") fooBar:String, - @DBKey @Change(name = "a_b") thingy: Long, - @DBKey hey: Int, - count: Int, - @Change(name = "big_mac") bigMac: String) - -case class PersonCapture( - id: ObjectId, - name: String, - age: Int, - stuff: Map[Int, Int]) - extends SJCapture -case class Tuple(t: (String, Int)) - -trait Strange -case class StrangeWrapper(num: Int, s: Strange) -case class StrangeHint(_hint: Int, size: Int) extends Strange - -trait Body -case class FancyBody(message: String) extends Body -case class Envelope[T <: Body](id: String, body: T) { - type Giraffe = T -} - -case class Times(offset: OffsetDateTime, zoned: ZonedDateTime) - -case class Embed(stuff: List[String], num: Int) -case class Boom(name: String, other: Try[Embed]) - -case class Flexible(name: String, dunno: Any) \ No newline at end of file diff --git a/mongo/src/test/scala/co.blocke.scalajack/mongo/MongoSpec.scala b/mongo/src/test/scala/co.blocke.scalajack/mongo/MongoSpec.scala deleted file mode 100644 index d741b5e2..00000000 --- a/mongo/src/test/scala/co.blocke.scalajack/mongo/MongoSpec.scala +++ /dev/null @@ -1,891 +0,0 @@ -package co.blocke.scalajack -package mongo - -import model._ - -import java.time._ -import java.util.UUID - -import org.bson._ -import org.bson.types.ObjectId -import TestUtil._ -import munit._ -import munit.internal.console -import scala.jdk.CollectionConverters._ -import co.blocke.scala_reflection.RType - -class WrappedOffsetDateTime(val offsetDateTime: OffsetDateTime) extends AnyVal - -class MongoSpec extends FunSuite: - - val TRUE = true - val FALSE = false - - val data = One( - "Greg", - List("a", "b"), - List(Two("x", FALSE), Two("y", TRUE)), - Two("Nest!", TRUE), - Some("wow"), - Map("hey" -> 17, "you" -> 21), - TRUE, - 99123986123L, - Num.C, - 46 - ) - - def mongoScalaJack: JackFlavor[BsonValue] = ScalaJack(MongoFlavor()) - - test("Naked Map support") { - describe( - "---------------------------\n: Mongo Tests (MongoDB) :\n---------------------------", Console.BLUE - ) - describe("Prinitives") - val li = Map("a" -> 1, "b" -> 2, "c" -> 3) - val dbo: BsonValue = mongoScalaJack.render(li) - assertEquals(dbo.asDocument.toJson, """{"a": 1, "b": 2, "c": 3}""") - assertEquals(mongoScalaJack.read[Map[String, Int]](dbo), li) - } - - test("UUID support") { - val thing = UuidThing( - "Foo", - UUID.fromString("1e6c2b31-4dfe-4bf6-a0a0-882caaff0e9c"), - List( - UUID.fromString("1e6c2b31-4dfe-4bf6-a0a0-882caaff0e9c"), - UUID.fromString("1e6c2b31-4dfe-4bf6-a0a0-882caaff0e9c") - ), - Some(UUID.fromString("1e6c2b31-4dfe-4bf6-a0a0-882caaff0e9c")) - ) - val dbo = mongoScalaJack.render(thing) - assertEquals(dbo.asDocument.toJson, - """{"name": "Foo", "uuid": "1e6c2b31-4dfe-4bf6-a0a0-882caaff0e9c", "many": ["1e6c2b31-4dfe-4bf6-a0a0-882caaff0e9c", "1e6c2b31-4dfe-4bf6-a0a0-882caaff0e9c"], "maybe": "1e6c2b31-4dfe-4bf6-a0a0-882caaff0e9c"}""" - ) - val b = mongoScalaJack.read[UuidThing](dbo) - assertEquals(b, thing) - } - - test("Misc number primitives support") { - val inst = Loose('A', 1.23F, 15.toShort, 3.toByte) - val dbo = mongoScalaJack.render(inst) - assertEquals(dbo.asDocument.toJson, - """{"a": "A", "b": 1.23, "c": 15, "d": 3}""" - ) - assertEquals(mongoScalaJack.read[Loose](dbo), inst) - } - - test("OffsetDateTime support") { - val t = LocalDate - .parse("1986-07-01") - .atTime(OffsetTime.of(LocalTime.MIDNIGHT, ZoneOffset.UTC)) - val thing = JodaThing("Foo", t, List(t, t), Some(t)) - val dbo = mongoScalaJack.render(thing) - assertEquals(dbo.asDocument.toJson, - """{"name": "Foo", "dt": {"$date": 520560000000}, "many": [{"$date": 520560000000}, {"$date": 520560000000}], "maybe": {"$date": 520560000000}}""" - ) - val b = mongoScalaJack.read[JodaThing](dbo) - assertEquals(b, thing) - } - - test("ZonedDateTime must work") { - val inst = SampleZonedDateTime( - ZonedDateTime.parse("2007-12-03T10:15:30Z[UTC]"), - ZonedDateTime.parse("2007-12-03T10:15:30Z[UTC]") - ) - val dbo = mongoScalaJack.render(inst) - assertEquals(dbo.asDocument.toJson, - """{"o1": {"$date": 1196676930000}, "o2": {"$date": 1196676930000}}""" - ) - val b = mongoScalaJack.read[SampleZonedDateTime](dbo) - assertEquals(b, inst) - } - - test("Permissives work") { - val bd = new BsonDocument() - bd.append("name", new BsonString("Fido")) - bd.append("legs", new BsonString("3")) - val wPerm = mongoScalaJack.allowPermissivePrimitives() - assertEquals(wPerm.read[Animal](bd), Animal("Fido", 3)) - } - - test( - "Case class having List parameter - Foo[A](x:A) where A -> List of simple type" - ) { - describe("Basic Collection Support") - val w = Carry("Trey", Wrap("Hobbies", List(true, true, false), "all")) - val db = mongoScalaJack.render(w) - assertEquals(db.asDocument.toJson, - """{"s": "Trey", "w": {"name": "Hobbies", "data": [true, true, false], "stuff": "all"}}""" - ) - assertEquals(mongoScalaJack.read[Carry[List[Boolean]]](db), w) - } - - test( - "Case class having Map parameter - Foo[A](x:A) where A -> Map of simple type" - ) { - val w = Carry("Troy", Wrap("Articles", Map("OK" -> 59), "all")) - val db = mongoScalaJack.render(w) - assertEquals(db.asDocument.toJson, - """{"s": "Troy", "w": {"name": "Articles", "data": {"OK": 59}, "stuff": "all"}}""" - ) - assertEquals(mongoScalaJack.read[Carry[Map[String, Int]]](db), w) - } - - test( - "Case class having Option parameter - Foo[A](x:A) where A -> Option of simple type" - ) { - val w = Carry( - "Terri", - Wrap("Hobbies", Some(17).asInstanceOf[Option[Int]], "all") - ) - val x = Carry[Option[Int]]("Terry", Wrap("Hobbies", None, "all")) - val db = mongoScalaJack.render(w) - val db2 = mongoScalaJack.render(x) - assertEquals(db.asDocument.toJson, - """{"s": "Terri", "w": {"name": "Hobbies", "data": 17, "stuff": "all"}}""" - ) - assertEquals(db2.asDocument.toJson, - """{"s": "Terry", "w": {"name": "Hobbies", "stuff": "all"}}""" - ) - assertEquals(mongoScalaJack.read[Carry[Option[Int]]](db), w) - assertEquals(mongoScalaJack.read[Carry[Option[Int]]](db2), x) - } - - test( - "Case class having List of parameterized value - Foo[A](x:List[A]) - where A is a simple type" - ) { - val w = BagList("list", List(1, 2, 3)) - val db = mongoScalaJack.render(w) - assertEquals(db.asDocument.toJson, - """{"s": "list", "many": [1, 2, 3]}""" - ) - assertEquals(mongoScalaJack.read[BagList[Int]](db), w) - } - - test( - "Case class having Map of parameterized value - Foo[A,B](x:Map[A,B]) - where A,B are simple types" - ) { - val w = BagMap(5, Map("one" -> true, "two" -> false)) - val db = mongoScalaJack.render(w) - assertEquals(db.asDocument.toJson, - """{"i": 5, "items": {"one": true, "two": false}}""" - ) - assertEquals(mongoScalaJack.read[BagMap[Boolean]](db), w) - } - - test( - "Case class having Option of parameterized value - Foo[A](x:Option[A]) - where A is a simple type" - ) { - val w = BagOpt(1, Some("ok")) - val x = BagOpt[String](1, None) - val db = mongoScalaJack.render(w) - val db2 = mongoScalaJack.render(x) - assertEquals(db.asDocument.toJson, """{"i": 1, "maybe": "ok"}""") - assertEquals(db2.asDocument.toJson, """{"i": 1}""") - assertEquals(mongoScalaJack.read[BagOpt[String]](db), w) - assertEquals(mongoScalaJack.read[BagOpt[String]](db2), x) - } - - test( - "Case class having List parameter - Foo[A](x:A) where A -> List of Bar[Int]" - ) { - describe( - "Advanced Collection Support - collections of parameterized case class" - ) - val w = Carry( - "Trey", - Wrap("Hobbies", List(Zoo("one", 1), Zoo("two", 2)), "all") - ) - val db = mongoScalaJack.render(w) - assertEquals(db.asDocument.toJson, - """{"s": "Trey", "w": {"name": "Hobbies", "data": [{"name": "one", "z": 1}, {"name": "two", "z": 2}], "stuff": "all"}}""" - ) - assertEquals(mongoScalaJack.read[Carry[List[Zoo[Int]]]](db), w) - } - - test( - "Case class having Map parameter - Foo[A](x:A) where A -> Map of Bar[Int,String]" - ) { - val w = - Carry("Troy", Wrap("Articles", Map("OK" -> Zoo("q", false)), "all")) - val db = mongoScalaJack.render(w) - assertEquals(db.asDocument.toJson, - """{"s": "Troy", "w": {"name": "Articles", "data": {"OK": {"name": "q", "z": false}}, "stuff": "all"}}""" - ) - assertEquals(mongoScalaJack.read[Carry[Map[String, Zoo[Boolean]]]](db), - w - ) - } - - test( - "Case class having Option parameter - Foo[A](x:A) where A -> Option of Bar[Int]" - ) { - val w = Carry( - "Terri", - Wrap( - "Hobbies", - Some(Zoo("a", "b")).asInstanceOf[Option[Zoo[String]]], - "all" - ) - ) - val x = Carry[Option[Int]]("Terry", Wrap("Hobbies", None, "all")) - val db = mongoScalaJack.render(w) - val db2 = mongoScalaJack.render(x) - assertEquals(db.asDocument.toJson, - """{"s": "Terri", "w": {"name": "Hobbies", "data": {"name": "a", "z": "b"}, "stuff": "all"}}""" - ) - assertEquals(db2.asDocument.toJson, - """{"s": "Terry", "w": {"name": "Hobbies", "stuff": "all"}}""" - ) - assertEquals(mongoScalaJack.read[Carry[Option[Zoo[String]]]](db), w) - assert(mongoScalaJack.read[Carry[Option[Zoo[String]]]](db2) == x) - } - - test( - "Case class having List parameter - Foo[A](x:A) where A -> List of value class" - ) { - val w = Carry( - "Trey", - Wrap("Hobbies", List(new Wrapper(99), new Wrapper(100)), "all") - ) - val db = mongoScalaJack.render(w) - assertEquals(db.asDocument.toJson, - """{"s": "Trey", "w": {"name": "Hobbies", "data": [99, 100], "stuff": "all"}}""" - ) - assertEquals(mongoScalaJack.read[Carry[List[Wrapper]]](db), w) - } - - test( - "Case class having Map parameter - Foo[A](x:A) where A -> Map of Bar[String,value class]" - ) { - val w = - Carry("Troy", Wrap("Articles", Map("OK" -> new Wrapper(2)), "all")) - val db = mongoScalaJack.render(w) - assertEquals(db.asDocument.toJson, - """{"s": "Troy", "w": {"name": "Articles", "data": {"OK": 2}, "stuff": "all"}}""" - ) - assertEquals(mongoScalaJack.read[Carry[Map[String, Wrapper]]](db), w) - } - - test( - "Case class having Option parameter - Foo[A](x:A) where A -> Option of value class" - ) { - val w = Carry( - "Terri", - Wrap( - "Hobbies", - Some(new Wrapper(-2)).asInstanceOf[Option[Wrapper]], - "all" - ) - ) - val x = Carry[Option[Wrapper]]("Terry", Wrap("Hobbies", None, "all")) - val db = mongoScalaJack.render(w) - val db2 = mongoScalaJack.render(x) - assertEquals(db.asDocument.toJson, - """{"s": "Terri", "w": {"name": "Hobbies", "data": -2, "stuff": "all"}}""" - ) - assertEquals(db2.asDocument.toJson, - """{"s": "Terry", "w": {"name": "Hobbies", "stuff": "all"}}""" - ) - assertEquals(mongoScalaJack.read[Carry[Option[Wrapper]]](db), w) - assertEquals(mongoScalaJack.read[Carry[Option[Wrapper]]](db2), x) - } - - test( - "Case class having List of parameterized value - Foo[A](x:List[A]) - where A -> Bar[Int]" - ) { - val w = BagList("list", List(Zoo("a", 1), Zoo("b", 2))) - val db = mongoScalaJack.render(w) - assertEquals(db.asDocument.toJson, - """{"s": "list", "many": [{"name": "a", "z": 1}, {"name": "b", "z": 2}]}""" - ) - assertEquals(mongoScalaJack.read[BagList[Zoo[Int]]](db), w) - } - - test( - "Case class having Map of parameterized value - Foo[A,B](x:Map[A,B]) - where A,B -> String,Bar[Int]" - ) { - val w = BagMap(5, Map("one" -> Zoo("a", 1), "two" -> Zoo("b", 2))) - val db = mongoScalaJack.render(w) - assertEquals(db.asDocument.toJson, - """{"i": 5, "items": {"one": {"name": "a", "z": 1}, "two": {"name": "b", "z": 2}}}""" - ) - assertEquals(mongoScalaJack.read[BagMap[Zoo[Int]]](db), w) - } - - test( - "Case class having Option of parameterized value - Foo[A](x:Option[A]) - where A -> Bar[Int]" - ) { - val w = Carry( - "Terri", - Wrap( - "Hobbies", - Some(Truck(false, Two("aaa", true))) - .asInstanceOf[Option[Truck[Boolean]]], - "all" - ) - ) - val x = - Carry[Option[Truck[Boolean]]]("Terry", Wrap("Hobbies", None, "all")) - val db = mongoScalaJack.render(w) - val db2 = mongoScalaJack.render(x) - assertEquals(db.asDocument.toJson, - """{"s": "Terri", "w": {"name": "Hobbies", "data": {"s": false, "t": {"foo": "aaa", "bar": true}}, "stuff": "all"}}""" - ) - assertEquals(db2.asDocument.toJson, - """{"s": "Terry", "w": {"name": "Hobbies", "stuff": "all"}}""" - ) - assertEquals(mongoScalaJack.read[Carry[Option[Truck[Boolean]]]](db), w) - assertEquals(mongoScalaJack.read[Carry[Option[Truck[Boolean]]]](db2), x) - } - - test( - "Case class having List of parameterized value - Foo[A](x:List[A]) - where A -> value class" - ) { - val w = BagList( - "list", - List(Zoo("a", new Wrapper(1)), Zoo("b", new Wrapper(2))) - ) - val db = mongoScalaJack.render(w) - assertEquals(db.asDocument.toJson, - """{"s": "list", "many": [{"name": "a", "z": 1}, {"name": "b", "z": 2}]}""" - ) - assertEquals(mongoScalaJack.read[BagList[Zoo[Wrapper]]](db), w) - } - - test( - "Case class having Map of parameterized value - Foo[A,B](x:Map[A,B]) - where A,B -> String,value class" - ) { - val w = BagMap( - 5, - Map( - "one" -> Zoo("a", new Wrapper(1)), - "two" -> Zoo("b", new Wrapper(2)) - ) - ) - val db = mongoScalaJack.render(w) - assertEquals(db.asDocument.toJson, - """{"i": 5, "items": {"one": {"name": "a", "z": 1}, "two": {"name": "b", "z": 2}}}""" - ) - assertEquals(mongoScalaJack.read[BagMap[Zoo[Wrapper]]](db), w) - } - - test( - "Case class having Option of parameterized value - Foo[A](x:Option[A]) - where A -> value class" - ) { - val w = Carry( - "Terri", - Wrap( - "Hobbies", - Some(Zoo("a", new Wrapper(12))).asInstanceOf[Option[Zoo[Wrapper]]], - "all" - ) - ) - val x = - Carry[Option[Truck[Boolean]]]("Terry", Wrap("Hobbies", None, "all")) - val db = mongoScalaJack.render(w) - val db2 = mongoScalaJack.render(x) - assertEquals(db.asDocument.toJson, - """{"s": "Terri", "w": {"name": "Hobbies", "data": {"name": "a", "z": 12}, "stuff": "all"}}""" - ) - assertEquals(db2.asDocument.toJson, - """{"s": "Terry", "w": {"name": "Hobbies", "stuff": "all"}}""" - ) - assertEquals(mongoScalaJack.read[Carry[Option[Zoo[Wrapper]]]](db), w) - assert(mongoScalaJack.read[Carry[Option[Zoo[Wrapper]]]](db2) == x) - } - - test("Parameter is a simple trait") { - describe("Basic trait support") - val w = Carry[Pop]("Surprise", Wrap("Yellow", Wow2("three", 3), "Done")) - val db = mongoScalaJack.render(w) - assertEquals(db.asDocument.toJson, - """{"s": "Surprise", "w": {"name": "Yellow", "data": {"_hint": "co.blocke.scalajack.mongo.Wow2", "x": "three", "y": 3}, "stuff": "Done"}}""" - ) - assertEquals(mongoScalaJack.read[Carry[Pop]](db), w) - } - - test("Parameter is a simple trait with hint function value mappings") { - val w = Carry[Pop]("Surprise", Wrap("Yellow", Wow2("three", 4), "Done")) - val scalaJack = mongoScalaJack.withHintModifiers( - RType.of[Pop] -> ClassNameHintModifier( - hint => s"co.blocke.scalajack.mongo.$hint", - fullName => fullName.split('.').last - ) - ) - val db = scalaJack.render(w) - assertEquals(db.asDocument.toJson, - """{"s": "Surprise", "w": {"name": "Yellow", "data": {"_hint": "Wow2", "x": "three", "y": 4}, "stuff": "Done"}}""" - ) - assertEquals(scalaJack.read[Carry[Pop]](db), w) - } - - test("Hint modifier fails") { - val w = Carry[Pop]("Surprise", Wrap("Yellow", Wow2("three", 4), "Done")) - val scalaJack = mongoScalaJack.withHintModifiers( - RType.of[Pop] -> ClassNameHintModifier( - hint => throw new Exception("Boom"), // intentional hint mod failure - fullName => fullName.split('.').last - ) - ) - val db = scalaJack.render(w) - interceptMessage[ScalaJackError]("Couldn't marshal class for Wow2"){ - scalaJack.read[Carry[Pop]](db) - } - } - - test("Type modifier works") { - val scalaJack = ScalaJack(MongoFlavor()).withTypeValueModifier( - ClassNameHintModifier( - (hint: String) => "co.blocke.scalajack.mongo." + hint, - (cname: String) => cname.split('.').last - ) - ) - val value: Envelope[Body] = Envelope("DEF", FancyBody("BOO")) - val d = scalaJack.render[Envelope[Body]](value) - assertEquals(d.asDocument.toJson, - """{"Giraffe": "FancyBody", "id": "DEF", "body": {"message": "BOO"}}""" - ) - assertEquals(scalaJack.read[Envelope[Body]](d), value) - } - - test("Parameter is List of trait") { - val w = Carry[List[Pop]]( - "Surprise", - Wrap("Yellow", List(Wow1("four", 4), Wow2("three", 3)), "Done") - ) - val db = mongoScalaJack.render(w) - assertEquals(db.asDocument.toJson, - """{"s": "Surprise", "w": {"name": "Yellow", "data": [{"_hint": "co.blocke.scalajack.mongo.Wow1", "a": "four", "b": 4}, {"_hint": "co.blocke.scalajack.mongo.Wow2", "x": "three", "y": 3}], "stuff": "Done"}}""" - ) - assertEquals(mongoScalaJack.read[Carry[List[Pop]]](db), w) - } - - test("Parameter is Map of String->trait") { - val w = Carry[Map[String, Pop]]( - "Surprise", - Wrap( - "Yellow", - Map("a" -> Wow1("four", 4), "b" -> Wow2("three", 3)), - "Done" - ) - ) - val db = mongoScalaJack.render(w) - assertEquals(db.asDocument.toJson, - """{"s": "Surprise", "w": {"name": "Yellow", "data": {"a": {"_hint": "co.blocke.scalajack.mongo.Wow1", "a": "four", "b": 4}, "b": {"_hint": "co.blocke.scalajack.mongo.Wow2", "x": "three", "y": 3}}, "stuff": "Done"}}""" - ) - assertEquals(mongoScalaJack.read[Carry[Map[String, Pop]]](db), w) - } - - test("Parameter is an Option of trait") { - val w = Carry[Option[Pop]]( - "Terri", - Wrap("Hobbies", Some(Wow1("ok", -99)), "all") - ) - val x = Carry[Option[Pop]]("Terry", Wrap("Hobbies", None, "all")) - val db = mongoScalaJack.render(w) - val db2 = mongoScalaJack.render(x) - assertEquals(db.asDocument.toJson, - """{"s": "Terri", "w": {"name": "Hobbies", "data": {"_hint": "co.blocke.scalajack.mongo.Wow1", "a": "ok", "b": -99}, "stuff": "all"}}""" - ) - assertEquals(db2.asDocument.toJson, - """{"s": "Terry", "w": {"name": "Hobbies", "stuff": "all"}}""" - ) - assertEquals(mongoScalaJack.read[Carry[Option[Pop]]](db), w) - assertEquals(mongoScalaJack.read[Carry[Option[Pop]]](db2), x) - } - - test("List of parameter, where parameter is a trait") { - val w = BagList[Pop]("list", List(Wow1("A", 1), Wow1("B", 2))) - val db = mongoScalaJack.render(w) - assertEquals(db.asDocument.toJson, - """{"s": "list", "many": [{"_hint": "co.blocke.scalajack.mongo.Wow1", "a": "A", "b": 1}, {"_hint": "co.blocke.scalajack.mongo.Wow1", "a": "B", "b": 2}]}""" - ) - assertEquals(mongoScalaJack.read[BagList[Pop]](db), w) - } - - test("Map of String->parameter, where parameter is a trait") { - val w = - BagMap[Pop](5, Map("one" -> Wow2("q", 7), "two" -> Wow1("r", 3))) - val db = mongoScalaJack.render(w) - assertEquals(db.asDocument.toJson, - """{"i": 5, "items": {"one": {"_hint": "co.blocke.scalajack.mongo.Wow2", "x": "q", "y": 7}, "two": {"_hint": "co.blocke.scalajack.mongo.Wow1", "a": "r", "b": 3}}}""" - ) - assertEquals(mongoScalaJack.read[BagMap[Pop]](db), w) - } - - test("Option of parameter, where parameter is a trait") { - val w = Carry[Option[Pop]]( - "Terri", - Wrap("Hobbies", Some(Wow2("finite", 1000)), "all") - ) - val x = Carry[Option[Pop]]("Terry", Wrap("Hobbies", None, "all")) - val db = mongoScalaJack.render(w) - val db2 = mongoScalaJack.render(x) - assertEquals(db.asDocument.toJson, - """{"s": "Terri", "w": {"name": "Hobbies", "data": {"_hint": "co.blocke.scalajack.mongo.Wow2", "x": "finite", "y": 1000}, "stuff": "all"}}""" - ) - assertEquals(db2.asDocument.toJson, - """{"s": "Terry", "w": {"name": "Hobbies", "stuff": "all"}}""" - ) - assertEquals(mongoScalaJack.read[Carry[Option[Pop]]](db), w) - assertEquals(mongoScalaJack.read[Carry[Option[Pop]]](db2), x) - } - - test("Case class having an embedded parameterized trait") { - describe( - "Advanced trait support -- parameters are traits, themselves having parameters" - ) - val w = Breakfast(true, Toast(7, "Burnt")) - val db = mongoScalaJack.render(w) - assertEquals(db.asDocument.toJson, - """{"y": true, "bread": {"_hint": "co.blocke.scalajack.mongo.Toast", "g": 7, "yum": "Burnt"}}""" - ) - assertEquals(mongoScalaJack.read[Breakfast[String]](db), w) - } - - test( - "Case class having an embedded parameterized trait, with the trait's parameter another case class" - ) { - val w = Breakfast(true, Toast(7, Two("two", true))) - val db = mongoScalaJack.render(w) - assertEquals(db.asDocument.toJson, - """{"y": true, "bread": {"_hint": "co.blocke.scalajack.mongo.Toast", "g": 7, "yum": {"foo": "two", "bar": true}}}""" - ) - assertEquals(mongoScalaJack.read[Breakfast[Two]](db), w) - } - - test( - "Case class having an embedded parameterized trait, with the trait's parameter a value class" - ) { - val w = Breakfast(true, Toast(7, new Wrapper(-100))) - val db = mongoScalaJack.render(w) - assertEquals(db.asDocument.toJson, - """{"y": true, "bread": {"_hint": "co.blocke.scalajack.mongo.Toast", "g": 7, "yum": -100}}""" - ) - assertEquals(mongoScalaJack.read[Breakfast[Wrapper]](db), w) - } - - test("Parameter is a parameterized trait") { // I can't believe this one worked! - val w = Carry[Tart[Soup[String]]]( - "Bill", - Wrap("Betty", Bun(3, Cruton(8, "eight")), "ok") - ) - val db = mongoScalaJack.render(w) - assertEquals(db.asDocument.toJson, - """{"s": "Bill", "w": {"name": "Betty", "data": {"_hint": "co.blocke.scalajack.mongo.Bun", "g": 3, "yum": {"_hint": "co.blocke.scalajack.mongo.Cruton", "i": 8, "sweet": "eight"}}, "stuff": "ok"}}""" - ) - assertEquals(mongoScalaJack.read[Carry[Tart[Soup[String]]]](db), w) - } - - test("Parameter is List of parameterized trait") { - val w = Carry[List[Tart[Boolean]]]( - "Trey", - Wrap("Hobbies", List(Bun(1, false), Toast(2, true)), "all") - ) - val db = mongoScalaJack.render(w) - assertEquals(db.asDocument.toJson, - """{"s": "Trey", "w": {"name": "Hobbies", "data": [{"_hint": "co.blocke.scalajack.mongo.Bun", "g": 1, "yum": false}, {"_hint": "co.blocke.scalajack.mongo.Toast", "g": 2, "yum": true}], "stuff": "all"}}""" - ) - assertEquals(mongoScalaJack.read[Carry[List[Tart[Boolean]]]](db), w) - } - - test("Parameter is Map of String->parameterized trait") { - val w = Carry[Map[String, Tart[String]]]( - "Troy", - Wrap("Articles", Map("OK" -> Bun(27, "Hot")), "all") - ) - val db = mongoScalaJack.render(w) - assertEquals(db.asDocument.toJson, - """{"s": "Troy", "w": {"name": "Articles", "data": {"OK": {"_hint": "co.blocke.scalajack.mongo.Bun", "g": 27, "yum": "Hot"}}, "stuff": "all"}}""" - ) - assertEquals(mongoScalaJack.read[Carry[Map[String, Tart[String]]]](db), - w - ) - } - - test("Parameter is an Option of parameterized trait") { - val w = Carry[Option[Tart[Int]]]( - "Terri", - Wrap("Hobbies", Some(Toast(11, 12)), "all") - ) - val x = Carry[Option[Tart[Int]]]("Terry", Wrap("Hobbies", None, "all")) - val db = mongoScalaJack.render(w) - val db2 = mongoScalaJack.render(x) - assertEquals(db.asDocument.toJson, - """{"s": "Terri", "w": {"name": "Hobbies", "data": {"_hint": "co.blocke.scalajack.mongo.Toast", "g": 11, "yum": 12}, "stuff": "all"}}""" - ) - assertEquals(db2.asDocument.toJson, - """{"s": "Terry", "w": {"name": "Hobbies", "stuff": "all"}}""" - ) - assertEquals(mongoScalaJack.read[Carry[Option[Tart[Int]]]](db), w) - assertEquals(mongoScalaJack.read[Carry[Option[Tart[Int]]]](db2), x) - } - - test("List of parameter, where parameter is a parameterized trait") { - val w = - BagList[Tart[Boolean]]("list", List(Toast(1, true), Bun(2, false))) - val db = mongoScalaJack.render(w) - assertEquals(db.asDocument.toJson, - """{"s": "list", "many": [{"_hint": "co.blocke.scalajack.mongo.Toast", "g": 1, "yum": true}, {"_hint": "co.blocke.scalajack.mongo.Bun", "g": 2, "yum": false}]}""" - ) - assertEquals(mongoScalaJack.read[BagList[Tart[Boolean]]](db), w) - } - - test("Map of String->parameter, where parameter is a parameterized trait") { - val w = BagMap[Tart[Boolean]]( - 5, - Map("one" -> Bun(1, true), "two" -> Toast(2, false)) - ) - val db = mongoScalaJack.render(w) - assertEquals(db.asDocument.toJson, - """{"i": 5, "items": {"one": {"_hint": "co.blocke.scalajack.mongo.Bun", "g": 1, "yum": true}, "two": {"_hint": "co.blocke.scalajack.mongo.Toast", "g": 2, "yum": false}}}""" - ) - assertEquals(mongoScalaJack.read[BagMap[Tart[Boolean]]](db), w) - } - - test("Option of parameter, where parameter is a parameterized trait") { - val w = BagOpt[Tart[String]](1, Some(Bun(6, "ok"))) - val x = BagOpt[Tart[String]](1, None) - val db = mongoScalaJack.render(w) - val db2 = mongoScalaJack.render(x) - assertEquals(db.asDocument.toJson, - """{"i": 1, "maybe": {"_hint": "co.blocke.scalajack.mongo.Bun", "g": 6, "yum": "ok"}}""" - ) - assertEquals(db2.asDocument.toJson, """{"i": 1}""") - assertEquals(mongoScalaJack.read[BagOpt[Tart[String]]](db), w) - assertEquals(mongoScalaJack.read[BagOpt[Tart[String]]](db2), x) - } - - test("DBKey Annotation (_id field generation) - single key") { - describe("Annotations (e.g. DBKey)") - val five = Five("Fred", Two("blah", true)) - val dbo = mongoScalaJack.render(five) - assertEquals(dbo.asDocument.toJson, - """{"_id": "Fred", "two": {"foo": "blah", "bar": true}}""" - ) - assertEquals(mongoScalaJack.read[Five](dbo), five) - } - - test( - "DBKey Annotation (_id field generation) - single key -- Missing Non-Key Field" - ) { - val dbo = new BsonDocument( - List( - new BsonElement("_id", new BsonString("Fred")), - new BsonElement( - "two", - new BsonDocument( - List(new BsonElement("bar", new BsonBoolean(true))).asJava - ) - ) - ).asJava - ) - assertEquals(dbo.toJson, """{"_id": "Fred", "two": {"bar": true}}""") - val msg = """Class co.blocke.scalajack.mongo.Two missing required fields: foo""".stripMargin - interceptMessage[ScalaJackError](msg){ - mongoScalaJack.read[Five](dbo) - } - } - - test( - "DBKey Annotation (_id field generation) - single key -- Missing Key Field" - ) { - val dbo = new BsonDocument( - List( - new BsonElement( - "two", - new BsonDocument( - List( - new BsonElement("foo", new BsonString("blah")), - new BsonElement("bar", new BsonBoolean(true)) - ).asJava - ) - ) - ).asJava - ) - assertEquals(dbo.toJson, """{"two": {"foo": "blah", "bar": true}}""") - val msg = - """Missing key (_id) field, or a component of a compound key field""" - interceptMessage[ScalaJackError](msg){ - mongoScalaJack.read[Five](dbo) - } - } - - test("DBKey Annotation (_id field generation) - compound key") { - val six = Six("Fred", 12, Two("blah", true)) - val dbo = mongoScalaJack.render(six) - assertEquals(dbo.asDocument.toJson, - """{"_id": {"name": "Fred", "num": 12}, "two": {"foo": "blah", "bar": true}}""" - ) - assertEquals(mongoScalaJack.read[Six](dbo), six) - } - - test( - "DBKey Annotation (_id field generation) - compound key -- Missing Key Field component" - ) { - val js = - """{"_id": {"name": "Fred"},"two": {"foo": "blah", "bar": true}}""" - val dbo = BsonDocument.parse(js) - val msg = - """Class co.blocke.scalajack.mongo.Six missing required fields: num""" - interceptMessage[ScalaJackError](msg){ - mongoScalaJack.read[Six](dbo) - } - } - - test("ObjectId support -- Mongo") { - describe("Mongo ObjectID") - // val oid = (new BsonObjectId()).getValue() - val oid = new ObjectId() - val seven = Seven(oid, Two("blah", true)) - val dbo = mongoScalaJack.render(seven) - assertEquals(dbo.asDocument.toJson, - s"""{"_id": {"$$oid": "${oid.toString}"}, "two": {"foo": "blah", "bar": true}}""" - ) - assertEquals(mongoScalaJack.read[Seven](dbo), seven) - } - - test("ObjectId support (null) -- Mongo") { - val seven = Seven(null, Two("blah", bar = true)) - val dbo = mongoScalaJack.render(seven) - assertEquals(mongoScalaJack.read[Seven](dbo), seven) - } - - test("Must handle a case class with default values - defaults specified") { - describe("Basic Case Class Support") - val wd = WithDefaults( - "Greg", - 49, - Some(5), - Some(false), - GrumpyPet(Cat("Fluffy"), "fish") - ) - val dbo = mongoScalaJack.render(wd) - assertEquals(dbo.asDocument.toJson, - """{"name": "Greg", "age": 49, "num": 5, "hasStuff": false, "pet": {"_hint": "co.blocke.scalajack.mongo.GrumpyPet", "kind": {"_hint": "co.blocke.scalajack.mongo.Cat", "name": "Fluffy"}, "food": "fish"}}""" - ) - val b = mongoScalaJack.read[WithDefaults](dbo) - assertEquals(b, wd) - } - - test("Case class as value for Any parameter") { - val f = Flexible("foo", Two("bar", bar = true)) - val d = mongoScalaJack.render(f) - assertEquals(d.toString, - """{"name": "foo", "dunno": {"_hint": "co.blocke.scalajack.mongo.Two", "foo": "bar", "bar": true}}""" - ) - assertEquals(mongoScalaJack.read[Flexible](d), f) - } - - test( - "Must handle a case class with default values - defaults not specified" - ) { - val wd = WithDefaults("Greg", 49, None) - val dbo = mongoScalaJack.render(wd) - assertEquals(dbo.asDocument.toJson, - """{"name": "Greg", "age": 49, "hasStuff": true, "pet": {"_hint": "co.blocke.scalajack.mongo.NicePet", "kind": {"_hint": "co.blocke.scalajack.mongo.Dog", "name": "Fido"}, "food": "bones"}}""" - ) - val b = mongoScalaJack.read[WithDefaults](dbo) - assertEquals(b, wd) - } - - test("Simple parameters - Foo[A](x:A) where A -> simple type") { - describe("Basic Parameterized Case Class") - val w = Wrap("number", true, 15) - val w2 = Wrap("number", true, "wow") - val db = mongoScalaJack.render(w) - val db2 = mongoScalaJack.render(w2) - assertEquals(db.asDocument.toJson, - """{"name": "number", "data": true, "stuff": 15}""" - ) - assertEquals(db2.asDocument.toJson, - """{"name": "number", "data": true, "stuff": "wow"}""" - ) - assertEquals(mongoScalaJack.read[Wrap[Boolean, Int]](db), w) - assertEquals(mongoScalaJack.read[Wrap[Boolean, String]](db2), w2) - } - - test( - "Non-parameter case clase as a field member - Foo[A](x:A, b:Bar) where A -> simple type" - ) { - val w = Truck(false, Two("z", true)) - val dbo = mongoScalaJack.render(w) - assertEquals(dbo.asDocument.toJson, - """{"s": false, "t": {"foo": "z", "bar": true}}""" - ) - assertEquals(mongoScalaJack.read[Truck[Boolean]](dbo), w) - } - - test("Non-parameter case class as a parameter - Foo[A](x:A) where A -> Bar") { - val w = Wrap("number", true, Two("a", false)) - val db = mongoScalaJack.render(w) - assertEquals(db.asDocument.toJson, - """{"name": "number", "data": true, "stuff": {"foo": "a", "bar": false}}""" - ) - assertEquals(mongoScalaJack.read[Wrap[Boolean, Two]](db), w) - } - - test( - "Parameterized case class as parameter - Foo[A](x:A) where A -> Bar[Int]" - ) { - describe("Advanced Parameterized Case Class") - val w = Carry("Bob", Wrap("Mary", 3, "Available")) - val x = Carry("Mary", Wrap("Greg", false, "Done")) - val y = Carry("Fred", Wrap("Mike", Two("Steam", true), "OK")) - val db = mongoScalaJack.render(w) - val db2 = mongoScalaJack.render(x) - val db3 = mongoScalaJack.render(y) - assertEquals(db.asDocument.toJson, - """{"s": "Bob", "w": {"name": "Mary", "data": 3, "stuff": "Available"}}""" - ) - assertEquals(db2.asDocument.toJson, - """{"s": "Mary", "w": {"name": "Greg", "data": false, "stuff": "Done"}}""" - ) - assertEquals(db3.asDocument.toJson, - """{"s": "Fred", "w": {"name": "Mike", "data": {"foo": "Steam", "bar": true}, "stuff": "OK"}}""" - ) - assertEquals(mongoScalaJack.read[Carry[Int]](db), w) - assertEquals(mongoScalaJack.read[Carry[Boolean]](db2), x) - assertEquals(mongoScalaJack.read[Carry[Two]](db3), y) - } - - test( - "Case class having value class parameter - Foo[A](x:A) where A -> value class (no value class handler)" - ) { - val w = Carry("Mike", Wrap("Sally", new Wrapper(15), "Fine")) - val db = mongoScalaJack.render(w) - assertEquals(db.asDocument.asDocument.toJson, - """{"s": "Mike", "w": {"name": "Sally", "data": 15, "stuff": "Fine"}}""" - ) - assertEquals(mongoScalaJack.read[Carry[Wrapper]](db), w) - } - - test( - "Case class having value class parameter - Foo[A](x:A) where A -> value class (WITH value class handler)" - ) { - val offsetDateTime = - OffsetDateTime.of(2015, 7, 1, 0, 0, 0, 0, ZoneOffset.UTC) - val w = Carry( - "Mike", - Wrap("Sally", new WrappedOffsetDateTime(offsetDateTime), "Fine") - ) - val db = mongoScalaJack.render(w) - - val timeval = offsetDateTime.toInstant.toEpochMilli - assertEquals(db.asDocument.toJson, - s"""{"s": "Mike", "w": {"name": "Sally", "data": {"$$date": $timeval}, "stuff": "Fine"}}""" - ) - assertEquals(mongoScalaJack.read[Carry[WrappedOffsetDateTime]](db), w) - } - - test( - "Case class having parameterized case class as a parameter: Foo[A](x:A) where A -> Bar[Blah[Long]]" - ) { - val w = Carry("Bill", Wrap("Betty", Zoo("dog", false), "ok")) - val db = mongoScalaJack.render(w) - assertEquals(db.asDocument.toJson, - """{"s": "Bill", "w": {"name": "Betty", "data": {"name": "dog", "z": false}, "stuff": "ok"}}""" - ) - assertEquals(mongoScalaJack.read[Carry[Zoo[Boolean]]](db), w) - } diff --git a/mongo/src/test/scala/co.blocke.scalajack/mongo/TryAndCapture.scala b/mongo/src/test/scala/co.blocke.scalajack/mongo/TryAndCapture.scala deleted file mode 100644 index f073a35c..00000000 --- a/mongo/src/test/scala/co.blocke.scalajack/mongo/TryAndCapture.scala +++ /dev/null @@ -1,57 +0,0 @@ -package co.blocke.scalajack -package mongo - -import TestUtil._ -import munit._ -import munit.internal.console -import org.bson._ -import org.bson.types.ObjectId - -import scala.util.{ Failure, Success } - -class TryAndCapture extends FunSuite: - - val sj = ScalaJack(MongoFlavor()) - - test("Try success") { - describe( - "-------------------------------\n: Try and Capture (MongoDB) :\n-------------------------------", Console.BLUE - ) - val d = BsonDocument.parse( - """{"name":"Greg","other":{"stuff":["a","b","c"],"num":2}}""" - ) - val obj = sj.read[Boom](d) - assertEquals(Boom("Greg", Success(Embed(List("a", "b", "c"), 2))), obj) - assertEquals(sj.render(obj), d) - } - - test("Try failure") { - val d = BsonDocument.parse("""{"name":"Greg","other":[1,2,3]}""") - val obj = sj.read[Boom](d) - val msg = - """Expected document (object) here, not 'BsonArray{values=[BsonInt32{value=1}, BsonInt32{value=2}, BsonInt32{value=3}]}'""".stripMargin - assertEquals(msg, obj.other.asInstanceOf[Failure[_]].exception.getMessage) - assertEquals(sj.render(obj), d) - } - - test("Try failure 2") { - val d = - BsonDocument.parse("""{"name":"Greg","other": -12.45 ,"num":2}""") - val obj = sj.read[Boom](d) - val msg = - """Expected document (object) here, not 'BsonDouble{value=-12.45}'""".stripMargin - assertEquals(msg, obj.other.asInstanceOf[Failure[_]].exception.getMessage) - assert(BsonDocument.parse("""{"name":"Greg","other":-12.45}""") == sj.render(obj)) - } - - test("SJCapture should work") { - val s = PersonCapture(new ObjectId(), "Fred", 52, Map(5 -> 1, 6 -> 2)) - val m = sj.render(s) - m.asDocument.append("extra", new BsonString("hey")) - val readIn = sj.read[PersonCapture](m) - assertEquals(readIn, s) - assert(sj.render(readIn) - .asDocument - .toJson - .endsWith(""""stuff": {"5": 1, "6": 2}, "extra": "hey"}""")) - } \ No newline at end of file diff --git a/mongo/src/test/scala/co.blocke.scalajack/mongo/TupleSpec.scala b/mongo/src/test/scala/co.blocke.scalajack/mongo/TupleSpec.scala deleted file mode 100644 index 3148b49d..00000000 --- a/mongo/src/test/scala/co.blocke.scalajack/mongo/TupleSpec.scala +++ /dev/null @@ -1,81 +0,0 @@ -package co.blocke.scalajack -package mongo - -import TestUtil._ -import munit._ -import munit.internal.console -import org.bson._ -import scala.jdk.CollectionConverters._ - -case class TT2(name: String, rec: Map[String, List[(String, Int, Boolean)]]) - -class TupleSpec extends FunSuite: - - val sjM = ScalaJack(MongoFlavor()) - - object ScalaMaster { - val r = List(("a", 1, true)) - val r2 = List(("x", 8, false), ("r", 3, true)) - val a = TT2("Larry", Map("foo" -> r, "hey" -> r2)) - } - - object MongoMaster { - val a = new BsonDocument( - List( - new BsonElement("name", new BsonString("Larry")), - new BsonElement( - "rec", - new BsonDocument( - List( - new BsonElement( - "foo", - new BsonArray( - List( - new BsonArray( - List( - new BsonString("a"), - new BsonInt32(1), - new BsonBoolean(true) - ).asJava - ) - ).asJava - ) - ), - new BsonElement( - "hey", - new BsonArray( - List( - new BsonArray( - List( - new BsonString("x"), - new BsonInt32(8), - new BsonBoolean(false), - ).asJava - ), - new BsonArray( - List( - new BsonString("r"), - new BsonInt32(3), - new BsonBoolean(true) - ).asJava - ) - ).asJava - ) - ) - ).asJava - ) - ) - ).asJava - ) - } - - test("Render Tests") { - describe( - "---------------------------\n: Tuple Tests (MongoDB) :\n---------------------------" - ) - assertEquals(sjM.render(ScalaMaster.a), MongoMaster.a) - } - - test("Read Tests") { - assertEquals(sjM.read[TT2](MongoMaster.a), ScalaMaster.a) - } \ No newline at end of file diff --git a/project/build.properties b/project/build.properties index 6a9f0388..27430827 100644 --- a/project/build.properties +++ b/project/build.properties @@ -1 +1 @@ -sbt.version=1.7.3 +sbt.version=1.9.6 diff --git a/project/metals.sbt b/project/metals.sbt new file mode 100644 index 00000000..aaf3382f --- /dev/null +++ b/project/metals.sbt @@ -0,0 +1,8 @@ +// format: off +// DO NOT EDIT! This file is auto-generated. + +// This file enables sbt-bloop to create bloop config files. + +addSbtPlugin("ch.epfl.scala" % "sbt-bloop" % "1.5.17") + +// format: on diff --git a/project/plugins.sbt b/project/plugins.sbt index acb1310c..0b0faec3 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -1,4 +1,6 @@ addSbtPlugin("co.blocke" % "gitflow-packager" % "0.1.32") addSbtPlugin("com.github.sbt" % "sbt-ci-release" % "1.5.11") addSbtPlugin("org.typelevel" % "sbt-typelevel-sonatype-ci-release" % "0.5.0-M6") -addSbtPlugin("org.typelevel" % "sbt-typelevel-github-actions" % "0.4.16") +addSbtPlugin("org.typelevel" % "sbt-typelevel-github-actions" % "0.7.1") +addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.5.2") +addSbtPlugin("org.scoverage" % "sbt-scoverage" % "2.0.12") \ No newline at end of file diff --git a/core/src/main/java/co/blocke/scalajack/Change.java b/src/main/java/co/blocke/scalajack/Change.java similarity index 100% rename from core/src/main/java/co/blocke/scalajack/Change.java rename to src/main/java/co/blocke/scalajack/Change.java diff --git a/core/src/main/java/co/blocke/scalajack/Collection.java b/src/main/java/co/blocke/scalajack/TypeHint.java similarity index 71% rename from core/src/main/java/co/blocke/scalajack/Collection.java rename to src/main/java/co/blocke/scalajack/TypeHint.java index 65cdcdef..954f5997 100644 --- a/core/src/main/java/co/blocke/scalajack/Collection.java +++ b/src/main/java/co/blocke/scalajack/TypeHint.java @@ -4,6 +4,6 @@ @Target({ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) -public @interface Collection { - String name(); +public @interface TypeHint{ + String hintValue(); } diff --git a/src/main/java/co/blocke/scalajack/json/schema/additionalProperties.java b/src/main/java/co/blocke/scalajack/json/schema/additionalProperties.java new file mode 100644 index 00000000..a3f46238 --- /dev/null +++ b/src/main/java/co/blocke/scalajack/json/schema/additionalProperties.java @@ -0,0 +1,9 @@ +package co.blocke.scalajack.json.schema; + +import java.lang.annotation.*; + +@Target({ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +public @interface additionalProperties{ + String value(); +} diff --git a/src/main/java/co/blocke/scalajack/json/schema/description.java b/src/main/java/co/blocke/scalajack/json/schema/description.java new file mode 100644 index 00000000..995e8bd5 --- /dev/null +++ b/src/main/java/co/blocke/scalajack/json/schema/description.java @@ -0,0 +1,9 @@ +package co.blocke.scalajack.json.schema; + +import java.lang.annotation.*; + +@Target({ElementType.PARAMETER}) +@Retention(RetentionPolicy.RUNTIME) +public @interface description{ + String value(); +} diff --git a/src/main/java/co/blocke/scalajack/json/schema/exclusiveMaximum.java b/src/main/java/co/blocke/scalajack/json/schema/exclusiveMaximum.java new file mode 100644 index 00000000..ad50b948 --- /dev/null +++ b/src/main/java/co/blocke/scalajack/json/schema/exclusiveMaximum.java @@ -0,0 +1,9 @@ +package co.blocke.scalajack.json.schema; + +import java.lang.annotation.*; + +@Target({ElementType.PARAMETER}) +@Retention(RetentionPolicy.RUNTIME) +public @interface exclusiveMaximum{ + String value(); +} diff --git a/src/main/java/co/blocke/scalajack/json/schema/exclusiveMinimum.java b/src/main/java/co/blocke/scalajack/json/schema/exclusiveMinimum.java new file mode 100644 index 00000000..0b31b2c2 --- /dev/null +++ b/src/main/java/co/blocke/scalajack/json/schema/exclusiveMinimum.java @@ -0,0 +1,9 @@ +package co.blocke.scalajack.json.schema; + +import java.lang.annotation.*; + +@Target({ElementType.PARAMETER}) +@Retention(RetentionPolicy.RUNTIME) +public @interface exclusiveMinimum{ + String value(); +} diff --git a/src/main/java/co/blocke/scalajack/json/schema/format.java b/src/main/java/co/blocke/scalajack/json/schema/format.java new file mode 100644 index 00000000..8864d438 --- /dev/null +++ b/src/main/java/co/blocke/scalajack/json/schema/format.java @@ -0,0 +1,9 @@ +package co.blocke.scalajack.json.schema; + +import java.lang.annotation.*; + +@Target({ElementType.PARAMETER}) +@Retention(RetentionPolicy.RUNTIME) +public @interface format{ + String value(); +} diff --git a/src/main/java/co/blocke/scalajack/json/schema/id.java b/src/main/java/co/blocke/scalajack/json/schema/id.java new file mode 100644 index 00000000..d02f7b68 --- /dev/null +++ b/src/main/java/co/blocke/scalajack/json/schema/id.java @@ -0,0 +1,9 @@ +package co.blocke.scalajack.json.schema; + +import java.lang.annotation.*; + +@Target({ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +public @interface id{ + String value(); +} diff --git a/src/main/java/co/blocke/scalajack/json/schema/items.java b/src/main/java/co/blocke/scalajack/json/schema/items.java new file mode 100644 index 00000000..64278aba --- /dev/null +++ b/src/main/java/co/blocke/scalajack/json/schema/items.java @@ -0,0 +1,9 @@ +package co.blocke.scalajack.json.schema; + +import java.lang.annotation.*; + +@Target({ElementType.PARAMETER}) +@Retention(RetentionPolicy.RUNTIME) +public @interface items{ + String value(); +} diff --git a/src/main/java/co/blocke/scalajack/json/schema/maxItems.java b/src/main/java/co/blocke/scalajack/json/schema/maxItems.java new file mode 100644 index 00000000..63141cfe --- /dev/null +++ b/src/main/java/co/blocke/scalajack/json/schema/maxItems.java @@ -0,0 +1,9 @@ +package co.blocke.scalajack.json.schema; + +import java.lang.annotation.*; + +@Target({ElementType.PARAMETER}) +@Retention(RetentionPolicy.RUNTIME) +public @interface maxItems{ + String value(); +} diff --git a/src/main/java/co/blocke/scalajack/json/schema/maxLength.java b/src/main/java/co/blocke/scalajack/json/schema/maxLength.java new file mode 100644 index 00000000..7a336c91 --- /dev/null +++ b/src/main/java/co/blocke/scalajack/json/schema/maxLength.java @@ -0,0 +1,9 @@ +package co.blocke.scalajack.json.schema; + +import java.lang.annotation.*; + +@Target({ElementType.PARAMETER}) +@Retention(RetentionPolicy.RUNTIME) +public @interface maxLength{ + String value(); +} diff --git a/src/main/java/co/blocke/scalajack/json/schema/maximum.java b/src/main/java/co/blocke/scalajack/json/schema/maximum.java new file mode 100644 index 00000000..694d7d5c --- /dev/null +++ b/src/main/java/co/blocke/scalajack/json/schema/maximum.java @@ -0,0 +1,9 @@ +package co.blocke.scalajack.json.schema; + +import java.lang.annotation.*; + +@Target({ElementType.PARAMETER}) +@Retention(RetentionPolicy.RUNTIME) +public @interface maximum{ + String value(); +} diff --git a/src/main/java/co/blocke/scalajack/json/schema/minItems.java b/src/main/java/co/blocke/scalajack/json/schema/minItems.java new file mode 100644 index 00000000..8dfdfe51 --- /dev/null +++ b/src/main/java/co/blocke/scalajack/json/schema/minItems.java @@ -0,0 +1,9 @@ +package co.blocke.scalajack.json.schema; + +import java.lang.annotation.*; + +@Target({ElementType.PARAMETER}) +@Retention(RetentionPolicy.RUNTIME) +public @interface minItems{ + String value(); +} diff --git a/src/main/java/co/blocke/scalajack/json/schema/minLength.java b/src/main/java/co/blocke/scalajack/json/schema/minLength.java new file mode 100644 index 00000000..fc843077 --- /dev/null +++ b/src/main/java/co/blocke/scalajack/json/schema/minLength.java @@ -0,0 +1,9 @@ +package co.blocke.scalajack.json.schema; + +import java.lang.annotation.*; + +@Target({ElementType.PARAMETER}) +@Retention(RetentionPolicy.RUNTIME) +public @interface minLength{ + String value(); +} diff --git a/src/main/java/co/blocke/scalajack/json/schema/minimum.java b/src/main/java/co/blocke/scalajack/json/schema/minimum.java new file mode 100644 index 00000000..bb719e6d --- /dev/null +++ b/src/main/java/co/blocke/scalajack/json/schema/minimum.java @@ -0,0 +1,9 @@ +package co.blocke.scalajack.json.schema; + +import java.lang.annotation.*; + +@Target({ElementType.PARAMETER}) +@Retention(RetentionPolicy.RUNTIME) +public @interface minimum{ + String value(); +} diff --git a/src/main/java/co/blocke/scalajack/json/schema/multipleOf.java b/src/main/java/co/blocke/scalajack/json/schema/multipleOf.java new file mode 100644 index 00000000..88e25cb3 --- /dev/null +++ b/src/main/java/co/blocke/scalajack/json/schema/multipleOf.java @@ -0,0 +1,9 @@ +package co.blocke.scalajack.json.schema; + +import java.lang.annotation.*; + +@Target({ElementType.PARAMETER}) +@Retention(RetentionPolicy.RUNTIME) +public @interface multipleOf{ + String value(); +} diff --git a/src/main/java/co/blocke/scalajack/json/schema/pattern.java b/src/main/java/co/blocke/scalajack/json/schema/pattern.java new file mode 100644 index 00000000..c3e370e8 --- /dev/null +++ b/src/main/java/co/blocke/scalajack/json/schema/pattern.java @@ -0,0 +1,9 @@ +package co.blocke.scalajack.json.schema; + +import java.lang.annotation.*; + +@Target({ElementType.PARAMETER}) +@Retention(RetentionPolicy.RUNTIME) +public @interface pattern{ + String value(); +} diff --git a/src/main/java/co/blocke/scalajack/json/schema/title.java b/src/main/java/co/blocke/scalajack/json/schema/title.java new file mode 100644 index 00000000..a7c6e1a6 --- /dev/null +++ b/src/main/java/co/blocke/scalajack/json/schema/title.java @@ -0,0 +1,9 @@ +package co.blocke.scalajack.json.schema; + +import java.lang.annotation.*; + +@Target({ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +public @interface title{ + String value(); +} diff --git a/src/main/java/co/blocke/scalajack/json/schema/uniqueItems.java b/src/main/java/co/blocke/scalajack/json/schema/uniqueItems.java new file mode 100644 index 00000000..d8dad30c --- /dev/null +++ b/src/main/java/co/blocke/scalajack/json/schema/uniqueItems.java @@ -0,0 +1,9 @@ +package co.blocke.scalajack.json.schema; + +import java.lang.annotation.*; + +@Target({ElementType.PARAMETER}) +@Retention(RetentionPolicy.RUNTIME) +public @interface uniqueItems{ + String value(); +} diff --git a/src/main/java/co/blocke/scalajack/util/ByteArrayAccess.java b/src/main/java/co/blocke/scalajack/util/ByteArrayAccess.java new file mode 100644 index 00000000..e02d2d2e --- /dev/null +++ b/src/main/java/co/blocke/scalajack/util/ByteArrayAccess.java @@ -0,0 +1,50 @@ +package co.blocke.scalajack.util; + +import java.lang.invoke.MethodHandles; +import java.lang.invoke.VarHandle; +import java.nio.ByteOrder; + +public class ByteArrayAccess { // FIXME: Use Java wrapper as w/a for missing support of @PolymorphicSignature methods in Scala 3, see: https://github.com/lampepfl/dotty/issues/11332 + private static final VarHandle VH_LONG = + MethodHandles.byteArrayViewVarHandle(long[].class, ByteOrder.LITTLE_ENDIAN); + private static final VarHandle VH_INT = + MethodHandles.byteArrayViewVarHandle(int[].class, ByteOrder.LITTLE_ENDIAN); + private static final VarHandle VH_SHORT = + MethodHandles.byteArrayViewVarHandle(short[].class, ByteOrder.LITTLE_ENDIAN); + private static final VarHandle VH_LONG_REVERSED = + MethodHandles.byteArrayViewVarHandle(long[].class, ByteOrder.BIG_ENDIAN); + private static final VarHandle VH_INT_REVERSED = + MethodHandles.byteArrayViewVarHandle(int[].class, ByteOrder.BIG_ENDIAN); + + static public void setLong(byte[] buf, int pos, long value) { + VH_LONG.set(buf, pos, value); + } + + static public long getLong(byte[] buf, int pos) { + return (long) VH_LONG.get(buf, pos); + } + + static public void setInt(byte[] buf, int pos, int value) { + VH_INT.set(buf, pos, value); + } + + static public int getInt(byte[] buf, int pos) { + return (int) VH_INT.get(buf, pos); + } + + static public short getShort(byte[] buf, int pos) { + return (short) VH_SHORT.get(buf, pos); + } + + static public void setShort(byte[] buf, int pos, short value) { + VH_SHORT.set(buf, pos, value); + } + + static public void setLongReversed(byte[] buf, int pos, long value) { + VH_LONG_REVERSED.set(buf, pos, value); + } + + static public int getIntReversed(byte[] buf, int pos) { + return (int) VH_INT_REVERSED.get(buf, pos); + } +} \ No newline at end of file diff --git a/src/main/scala/co.blocke.scalajack/SJConfig.scala b/src/main/scala/co.blocke.scalajack/SJConfig.scala new file mode 100644 index 00000000..aa0ddb01 --- /dev/null +++ b/src/main/scala/co.blocke.scalajack/SJConfig.scala @@ -0,0 +1,241 @@ +package co.blocke.scalajack + +import co.blocke.scala_reflection.TypedName +import co.blocke.scala_reflection.reflect.* +import co.blocke.scala_reflection.reflect.rtypeRefs.* +import scala.quoted.* +import scala.util.control.NoStackTrace + +class ConfigError(msg: String) extends Throwable(msg) with NoStackTrace + +class SJConfig private[scalajack] ( + val noneAsNull: Boolean, + val tryFailureHandling: TryPolicy, + val eitherLeftHandling: EitherLeftPolicy, + // -------------------------- + val typeHintLabel: String, + val typeHintPolicy: TypeHintPolicy, + // -------------------------- + val enumsAsIds: List[String], // Default: string values. Nil=all enums as ids, List(...)=specified classes enums as ids + val _writeNonConstructorFields: Boolean, + val _suppressEscapedStrings: Boolean, + val _suppressTypeHints: Boolean +): + def withNoneAsNull(): SJConfig = copy(noneAsNull = true) + def withTryFailureHandling(tryPolicy: TryPolicy): SJConfig = copy(tryFailureHandling = tryPolicy) + def withEitherLeftHandling(eitherPolicy: EitherLeftPolicy): SJConfig = copy(eitherLeftHandling = eitherPolicy) + def withTypeHintLabel(label: String): SJConfig = copy(typeHintLabel = label) + def withTypeHintPolicy(hintPolicy: TypeHintPolicy): SJConfig = copy(typeHintPolicy = hintPolicy) + def withEnumsAsIds(asIds: List[String]): SJConfig = copy(enumsAsIds = asIds) + def writeNonConstructorFields(): SJConfig = copy(_writeNonConstructorFields = true) + def suppressEscapedStrings(): SJConfig = copy(_suppressEscapedStrings = true) + def suppressTypeHints(): SJConfig = copy(_suppressTypeHints = true) + + private def copy( + noneAsNull: Boolean = noneAsNull, + tryFailureHandling: TryPolicy = tryFailureHandling, + eitherLeftHandling: EitherLeftPolicy = eitherLeftHandling, + typeHintLabel: String = typeHintLabel, + typeHintPolicy: TypeHintPolicy = typeHintPolicy, + enumsAsIds: List[String] = enumsAsIds, + _writeNonConstructorFields: Boolean = _writeNonConstructorFields, + _suppressEscapedStrings: Boolean = _suppressEscapedStrings, + _suppressTypeHints: Boolean = _suppressTypeHints + ): SJConfig = new SJConfig( + noneAsNull, + tryFailureHandling, + eitherLeftHandling, + typeHintLabel, + typeHintPolicy, + enumsAsIds, + _writeNonConstructorFields, + _suppressEscapedStrings, + _suppressTypeHints + ) + +enum TryPolicy: + case AS_NULL, ERR_MSG_STRING, THROW_EXCEPTION + +enum EitherLeftPolicy: + case AS_VALUE, AS_NULL, ERR_MSG_STRING, THROW_EXCEPTION + +enum TypeHintPolicy: + case SIMPLE_CLASSNAME, SCRAMBLE_CLASSNAME, USE_ANNOTATION + +object SJConfig + extends SJConfig( + noneAsNull = false, + tryFailureHandling = TryPolicy.AS_NULL, + eitherLeftHandling = EitherLeftPolicy.AS_VALUE, + typeHintLabel = "_hint", + typeHintPolicy = TypeHintPolicy.SIMPLE_CLASSNAME, + enumsAsIds = List("-"), // default -> enum as value + _writeNonConstructorFields = false, + _suppressEscapedStrings = false, + _suppressTypeHints = false + ): + import scala.quoted.FromExpr.* + + private[scalajack] given ToExpr[SJConfig] with { + def apply(x: SJConfig)(using Quotes): Expr[SJConfig] = + '{ + val jc = SJConfig + .withTryFailureHandling(${ Expr(x.tryFailureHandling) }) + .withEitherLeftHandling(${ Expr(x.eitherLeftHandling) }) + .withTypeHintLabel(${ Expr(x.typeHintLabel) }) + .withTypeHintPolicy(${ Expr(x.typeHintPolicy) }) + .withEnumsAsIds(${ Expr(x.enumsAsIds) }) + val jc2 = ${ + if x.noneAsNull then '{ jc.withNoneAsNull() } + else '{ jc } + } + val jc3 = ${ + if !x._suppressEscapedStrings then '{ jc2.suppressEscapedStrings() } + else '{ jc2 } + } + val jc4 = ${ + if !x._suppressTypeHints then '{ jc3.suppressTypeHints() } + else '{ jc3 } + } + val jc5 = ${ + if !x._writeNonConstructorFields then '{ jc4.writeNonConstructorFields() } + else '{ jc4 } + } + jc5 + } + } + + private[scalajack] given FromExpr[SJConfig] with { + + def extract[X: FromExpr](name: String, x: Expr[X])(using Quotes): X = + import quotes.reflect.* + summon[FromExpr[X]].unapply(x).getOrElse(throw ConfigError(s"Can't parse $name: ${x.show}, tree: ${x.asTerm}")) + + def unapply(x: Expr[SJConfig])(using Quotes): Option[SJConfig] = + import quotes.reflect.* + + x match + case '{ + SJConfig( + $noneAsNullE, + $tryFailureHandlerE, + $eitherLeftHandlerE, + // $undefinedFieldHandlingE, + $typeHintLabelE, + $typeHintPolicyE, + $enumsAsIdsE, + $writeNonConstructorFieldsE, + $suppressEscapedStringsE, + $suppressTypeHintsE + ) + } => + try + Some( + SJConfig( + extract("noneAsNull", noneAsNullE), + extract("tryFailureHandler", tryFailureHandlerE), + extract("eitherLeftHandler", eitherLeftHandlerE), + extract("typeHintLabel", typeHintLabelE), + extract("typeHintPolicy", typeHintPolicyE), + extract("enumsAsIds", enumsAsIdsE), + extract("_writeNonConstructorFields", writeNonConstructorFieldsE), + extract("_suppressEscapedStrings", suppressEscapedStringsE), + extract("_suppressTypeHints", suppressTypeHintsE) + ) + ) + catch { + case x => + println("ERROR: " + x.getMessage) + None + } + case '{ SJConfig } => Some(SJConfig) + case '{ ($x: SJConfig).withNoneAsNull() } => Some(x.valueOrAbort.withNoneAsNull()) + case '{ ($x: SJConfig).withTryFailureHandling($v) } => Some(x.valueOrAbort.withTryFailureHandling(v.valueOrAbort)) + case '{ ($x: SJConfig).withEitherLeftHandling($v) } => Some(x.valueOrAbort.withEitherLeftHandling(v.valueOrAbort)) + case '{ ($x: SJConfig).withTypeHintLabel($v) } => Some(x.valueOrAbort.withTypeHintLabel(v.valueOrAbort)) + case '{ ($x: SJConfig).withTypeHintPolicy($v) } => Some(x.valueOrAbort.withTypeHintPolicy(v.valueOrAbort)) + case '{ ($x: SJConfig).withEnumsAsIds($v) } => Some(x.valueOrAbort.withEnumsAsIds(v.valueOrAbort)) + case '{ ($x: SJConfig).writeNonConstructorFields() } => Some(x.valueOrAbort.writeNonConstructorFields()) + case '{ ($x: SJConfig).suppressEscapedStrings() } => Some(x.valueOrAbort.suppressEscapedStrings()) + case '{ ($x: SJConfig).suppressTypeHints() } => Some(x.valueOrAbort.suppressTypeHints()) + } + + private[scalajack] given ToExpr[TryPolicy] with { + def apply(x: TryPolicy)(using Quotes): Expr[TryPolicy] = + x match + case TryPolicy.AS_NULL => '{ TryPolicy.AS_NULL } + case TryPolicy.ERR_MSG_STRING => '{ TryPolicy.ERR_MSG_STRING } + case TryPolicy.THROW_EXCEPTION => '{ TryPolicy.THROW_EXCEPTION } + } + + private[scalajack] given ToExpr[EitherLeftPolicy] with { + def apply(x: EitherLeftPolicy)(using Quotes): Expr[EitherLeftPolicy] = + x match + case EitherLeftPolicy.AS_VALUE => '{ EitherLeftPolicy.AS_VALUE } + case EitherLeftPolicy.AS_NULL => '{ EitherLeftPolicy.AS_NULL } + case EitherLeftPolicy.ERR_MSG_STRING => '{ EitherLeftPolicy.ERR_MSG_STRING } + case EitherLeftPolicy.THROW_EXCEPTION => '{ EitherLeftPolicy.THROW_EXCEPTION } + } + + private[scalajack] given ToExpr[TypeHintPolicy] with { + def apply(x: TypeHintPolicy)(using Quotes): Expr[TypeHintPolicy] = + x match + case TypeHintPolicy.SIMPLE_CLASSNAME => '{ TypeHintPolicy.SIMPLE_CLASSNAME } + case TypeHintPolicy.SCRAMBLE_CLASSNAME => '{ TypeHintPolicy.SCRAMBLE_CLASSNAME } + case TypeHintPolicy.USE_ANNOTATION => '{ TypeHintPolicy.USE_ANNOTATION } + } + + private[scalajack] given FromExpr[TryPolicy] with { + def unapply(x: Expr[TryPolicy])(using Quotes): Option[TryPolicy] = + import quotes.reflect.* + x match + case '{ TryPolicy.AS_NULL } => Some(TryPolicy.AS_NULL) + case '{ TryPolicy.ERR_MSG_STRING } => Some(TryPolicy.ERR_MSG_STRING) + case '{ TryPolicy.THROW_EXCEPTION } => Some(TryPolicy.THROW_EXCEPTION) + } + + private[scalajack] given FromExpr[EitherLeftPolicy] with { + def unapply(x: Expr[EitherLeftPolicy])(using Quotes): Option[EitherLeftPolicy] = + import quotes.reflect.* + x match + case '{ EitherLeftPolicy.AS_VALUE } => Some(EitherLeftPolicy.AS_VALUE) + case '{ EitherLeftPolicy.AS_NULL } => Some(EitherLeftPolicy.AS_NULL) + case '{ EitherLeftPolicy.ERR_MSG_STRING } => Some(EitherLeftPolicy.ERR_MSG_STRING) + case '{ EitherLeftPolicy.THROW_EXCEPTION } => Some(EitherLeftPolicy.THROW_EXCEPTION) + } + + private[scalajack] given FromExpr[TypeHintPolicy] with { + def unapply(x: Expr[TypeHintPolicy])(using Quotes): Option[TypeHintPolicy] = + import quotes.reflect.* + x match + case '{ TypeHintPolicy.SIMPLE_CLASSNAME } => Some(TypeHintPolicy.SIMPLE_CLASSNAME) + case '{ TypeHintPolicy.SCRAMBLE_CLASSNAME } => Some(TypeHintPolicy.SCRAMBLE_CLASSNAME) + case '{ TypeHintPolicy.USE_ANNOTATION } => Some(TypeHintPolicy.USE_ANNOTATION) + } + + /* + Here's how we use Quotes to get default values from a class...def + + // Constructor argument list, preloaded with optional 'None' values and any default values specified + val preloaded = Expr + .ofList(r.fields.map { f => + val scalaF = f.asInstanceOf[ScalaFieldInfoRef] + if scalaF.defaultValueAccessorName.isDefined then + r.refType match + case '[t] => + val tpe = TypeRepr.of[t].widen + val sym = tpe.typeSymbol + val companionBody = sym.companionClass.tree.asInstanceOf[ClassDef].body + val companion = Ref(sym.companionModule) + companionBody + .collect { + case defaultMethod @ DefDef(name, _, _, _) if name.startsWith("$lessinit$greater$default$" + (f.index + 1)) => + companion.select(defaultMethod.symbol).appliedToTypes(tpe.typeArgs).asExpr + } + .headOption + .getOrElse(Expr(null.asInstanceOf[Boolean])) + else if scalaF.fieldRef.isInstanceOf[OptionRef[_]] then Expr(None) + else Expr(null.asInstanceOf[Int]) + }) + + */ diff --git a/src/main/scala/co.blocke.scalajack/ScalaJack.scala b/src/main/scala/co.blocke.scalajack/ScalaJack.scala new file mode 100644 index 00000000..9b36c0be --- /dev/null +++ b/src/main/scala/co.blocke.scalajack/ScalaJack.scala @@ -0,0 +1,39 @@ +package co.blocke.scalajack + +import co.blocke.scala_reflection.{RTypeRef, TypedName} +import co.blocke.scala_reflection.reflect.ReflectOnType +import scala.quoted.* + +import quoted.Quotes +import json.* + +case class ScalaJack[T](jsonCodec: JsonCodec[T]): + def fromJson(js: String): T = + jsonCodec.decodeValue(reading.JsonSource(js)) + + val out = writing.JsonOutput() + def toJson(a: T): String = + jsonCodec.encodeValue(a, out.clear()) + out.result + +// --------------------------------------- + +object ScalaJack: + + // ----- Use default JsonConfig + inline def sjCodecOf[T]: ScalaJack[T] = ${ codecOfImpl[T] } + def codecOfImpl[T: Type](using Quotes): Expr[ScalaJack[T]] = + import quotes.reflect.* + val classRef = ReflectOnType[T](quotes)(TypeRepr.of[T], true)(using scala.collection.mutable.Map.empty[TypedName, Boolean]) + val jsonCodec = JsonCodecMaker.generateCodecFor(classRef, SJConfig) + + '{ ScalaJack($jsonCodec) } + + // ----- Use given JsonConfig + inline def sjCodecOf[T](inline cfg: SJConfig): ScalaJack[T] = ${ codecOfImplWithConfig[T]('cfg) } + def codecOfImplWithConfig[T: Type](cfgE: Expr[SJConfig])(using Quotes): Expr[ScalaJack[T]] = + import quotes.reflect.* + val cfg = summon[FromExpr[SJConfig]].unapply(cfgE) + val classRef = ReflectOnType[T](quotes)(TypeRepr.of[T], true)(using scala.collection.mutable.Map.empty[TypedName, Boolean]) + val jsonCodec = JsonCodecMaker.generateCodecFor(classRef, cfg.getOrElse(SJConfig)) + '{ ScalaJack($jsonCodec) } diff --git a/src/main/scala/co.blocke.scalajack/internal/CodePrinter.scala b/src/main/scala/co.blocke.scalajack/internal/CodePrinter.scala new file mode 100644 index 00000000..fde8eb7f --- /dev/null +++ b/src/main/scala/co.blocke.scalajack/internal/CodePrinter.scala @@ -0,0 +1,34 @@ +package co.blocke.scalajack +package internal + +import scala.quoted.* + +/** This is a fantastic object contributed by Aleksander Rainko. + * It will display the code generated by a macro to the console on compile: + * + * internal.CodePrinter.code { + * sj[Record] + * } + */ +//$COVERAGE-OFF$3rd party library +object CodePrinter { + inline def structure[A](inline value: A) = ${ structureMacro('value) } + + private def structureMacro[A: Type](value: Expr[A])(using Quotes): Expr[A] = { + import quotes.reflect.* + + val struct = Printer.TreeStructure.show(value.asTerm) + report.info(struct) + value.asTerm.changeOwner(Symbol.spliceOwner).asExprOf[A] + } + + inline def code[A](inline value: A): A = ${ codeMacro('value) } + + private def codeMacro[A: Type](value: Expr[A])(using Quotes): Expr[A] = { + import quotes.reflect.* + val struct = Printer.TreeShortCode.show(value.asTerm) + report.info(struct) + value + } +} +//$COVERAGE-ON$ diff --git a/src/main/scala/co.blocke.scalajack/internal/Numbers.scala b/src/main/scala/co.blocke.scalajack/internal/Numbers.scala new file mode 100644 index 00000000..fba4ba19 --- /dev/null +++ b/src/main/scala/co.blocke.scalajack/internal/Numbers.scala @@ -0,0 +1,840 @@ +/* + * Copyright 2019-2022 John A. De Goes and the ZIO Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package co.blocke.scalajack +package internal + +import java.io.* +import scala.util.control.NoStackTrace +import json.reading.JsonSource +import json.* + +// $COVERAGE-OFF$3rd party library + +/** Total, fast, number parsing. + * + * The Java and Scala standard libraries throw exceptions when we attempt to + * parse an invalid number. Unfortunately, exceptions are very expensive, and + * untrusted data can be maliciously constructed to DOS a server. + * + * This suite of functions mitigates against such attacks by building up the + * numbers one character at a time, which has been shown through extensive + * benchmarking to be orders of magnitude faster than exception-throwing stdlib + * parsers, for valid and invalid inputs. This approach, proposed by alexknvl, + * was also benchmarked against regexp-based pre-validation. + * + * Note that although the behaviour is identical to the Java stdlib when given + * the canonical form of a primitive (i.e. the .toString) of a number there may + * be differences in behaviour for non-canonical forms. e.g. the Java stdlib + * may reject "1.0" when parsed as an `BigInteger` but we may parse it as a + * `1`, although "1.1" would be rejected. Parsing of `BigDecimal` preserves the + * trailing zeros on the right but not on the left, e.g. "000.00001000" will be + * "1.000e-5", which is useful in cases where the trailing zeros denote + * measurement accuracy. + * + * `BigInteger`, `BigDecimal`, `Float` and `Double` have a configurable bit + * limit on the size of the significand, to avoid OOM style attacks, which is + * 128 bits by default. + * + * Results are contained in a specialisation of Option that avoids boxing. + */ +// TODO hex radix +// TODO octal radix +object SafeNumbers { + import UnsafeNumbers.UnsafeNumber + + def byte(num: String): ByteOption = + try ByteSome(UnsafeNumbers.byte(num)) + catch { case UnsafeNumber => ByteNone } + + def short(num: String): ShortOption = + try ShortSome(UnsafeNumbers.short(num)) + catch { case UnsafeNumber => ShortNone } + + def int(num: String): IntOption = + try IntSome(UnsafeNumbers.int(num)) + catch { case UnsafeNumber => IntNone } + + def long(num: String): LongOption = + try LongSome(UnsafeNumbers.long(num)) + catch { case UnsafeNumber => LongNone } + + def bigInteger( + num: String, + max_bits: Int = 128 + ): Option[java.math.BigInteger] = + try Some(UnsafeNumbers.bigInteger(num, max_bits)) + catch { case UnsafeNumber => None } + + def float(num: String, max_bits: Int = 128): FloatOption = + try FloatSome(UnsafeNumbers.float(num, max_bits)) + catch { case UnsafeNumber => FloatNone } + + def double(num: String, max_bits: Int = 128): DoubleOption = + try DoubleSome(UnsafeNumbers.double(num, max_bits)) + catch { case UnsafeNumber => DoubleNone } + + def bigDecimal( + num: String, + max_bits: Int = 128 + ): Option[java.math.BigDecimal] = + try Some(UnsafeNumbers.bigDecimal(num, max_bits)) + catch { case UnsafeNumber => None } + + // Based on the amazing work of Raffaello Giulietti + // "The Schubfach way to render doubles": https://drive.google.com/file/d/1luHhyQF9zKlM8yJ1nebU0OgVYhfC6CBN/view + // Sources with the license are here: https://github.com/c4f7fcce9cb06515/Schubfach/blob/3c92d3c9b1fead540616c918cdfef432bca53dfa/todec/src/math/DoubleToDecimal.java + def toString(x: Double): String = { + val bits = java.lang.Double.doubleToLongBits(x) + val ieeeExponent = (bits >> 52).toInt & 0x7ff + val ieeeMantissa = bits & 0xfffffffffffffL + if ieeeExponent == 2047 then { + if x != x then """"NaN"""" + else if bits < 0 then """"-Infinity"""" + else """"Infinity"""" + } else + { + val s = new java.lang.StringBuilder(24) + if bits < 0 then s.append('-') + if x == 0.0f then s.append('0').append('.').append('0') + else { + var e = ieeeExponent - 1075 + var m = ieeeMantissa | 0x10000000000000L + var dv = 0L + var exp = 0 + if e == 0 then dv = m + else if e >= -52 && e < 0 && m << e == 0 then dv = m >> -e + else { + var expShift, expCorr = 0 + var cblShift = 2 + if ieeeExponent == 0 then { + e = -1074 + m = ieeeMantissa + if ieeeMantissa < 3 then { + m *= 10 + expShift = 1 + } + } else if ieeeMantissa == 0 && ieeeExponent > 1 then { + expCorr = 131007 + cblShift = 1 + } + exp = e * 315653 - expCorr >> 20 + val i = exp + 324 << 1 + val g1 = gs(i) + val g0 = gs(i + 1) + val h = (-exp * 108853 >> 15) + e + 2 + val cb = m << 2 + val outm1 = (m.toInt & 0x1) - 1 + val vb = rop(g1, g0, cb << h) + val vbls = rop(g1, g0, cb - cblShift << h) + outm1 + val vbrd = outm1 - rop(g1, g0, cb + 2 << h) + val s = vb >> 2 + if s < 100 || { + dv = s / 10 // FIXME: Use Math.multiplyHigh(s, 1844674407370955168L) instead after dropping JDK 8 support + val sp40 = dv * 40 + val upin = (vbls - sp40).toInt + (((sp40 + vbrd).toInt + 40) ^ upin) >= 0 || { + dv += ~upin >>> 31 + exp += 1 + false + } + } + then { + val s4 = s << 2 + val uin = (vbls - s4).toInt + dv = (~ { + if (((s4 + vbrd).toInt + 4) ^ uin) < 0 then uin + else (vb.toInt & 0x3) + (s.toInt & 0x1) - 3 + } >>> 31) + s + exp -= expShift + } + } + val len = digitCount(dv) + exp += len - 1 + if exp < -3 || exp >= 7 then { + val dotOff = s.length + 1 + s.append(dv) + var i = s.length - 1 + while i > dotOff && s.charAt(i) == '0' do i -= 1 + s.setLength(i + 1) + s.insert(dotOff, '.').append('E').append(exp) + } else if exp < 0 then { + s.append('0').append('.') + while { + exp += 1 + exp != 0 + } do s.append('0') + s.append(dv) + var i = s.length - 1 + while s.charAt(i) == '0' do i -= 1 + s.setLength(i + 1) + s + } else if exp + 1 < len then { + val dotOff = s.length + exp + 1 + s.append(dv) + var i = s.length - 1 + while s.charAt(i) == '0' do i -= 1 + s.setLength(i + 1) + s.insert(dotOff, '.') + } else s.append(dv).append('.').append('0') + } + }.toString + } + + def toString(x: Float): String = { + val bits = java.lang.Float.floatToIntBits(x) + val ieeeExponent = (bits >> 23) & 0xff + val ieeeMantissa = bits & 0x7fffff + if ieeeExponent == 255 then { + if x != x then """"NaN"""" + else if bits < 0 then """"-Infinity"""" + else """"Infinity"""" + } else + { + val s = new java.lang.StringBuilder(16) + if bits < 0 then s.append('-') + if x == 0.0f then s.append('0').append('.').append('0') + else { + var e = ieeeExponent - 150 + var m = ieeeMantissa | 0x800000 + var dv, exp = 0 + if e == 0 then dv = m + else if e >= -23 && e < 0 && m << e == 0 then dv = m >> -e + else { + var expShift, expCorr = 0 + var cblShift = 2 + if ieeeExponent == 0 then { + e = -149 + m = ieeeMantissa + if ieeeMantissa < 8 then { + m *= 10 + expShift = 1 + } + } else if ieeeMantissa == 0 && ieeeExponent > 1 then { + expCorr = 131007 + cblShift = 1 + } + exp = e * 315653 - expCorr >> 20 + val g1 = gs(exp + 324 << 1) + 1 + val h = (-exp * 108853 >> 15) + e + 1 + val cb = m << 2 + val outm1 = (m & 0x1) - 1 + val vb = rop(g1, cb << h) + val vbls = rop(g1, cb - cblShift << h) + outm1 + val vbrd = outm1 - rop(g1, cb + 2 << h) + val s = vb >> 2 + if s < 100 || { + dv = (s * 3435973837L >>> 35).toInt // divide a positive int by 10 + val sp40 = dv * 40 + val upin = vbls - sp40 + ((sp40 + vbrd + 40) ^ upin) >= 0 || { + dv += ~upin >>> 31 + exp += 1 + false + } + } + then { + val s4 = s << 2 + val uin = vbls - s4 + dv = (~ { + if ((s4 + vbrd + 4) ^ uin) < 0 then uin + else (vb & 0x3) + (s & 0x1) - 3 + } >>> 31) + s + exp -= expShift + } + } + val len = digitCount(dv.toLong) + exp += len - 1 + if exp < -3 || exp >= 7 then { + val dotOff = s.length + 1 + s.append(dv) + var i = s.length - 1 + while i > dotOff && s.charAt(i) == '0' do i -= 1 + s.setLength(i + 1) + s.insert(dotOff, '.').append('E').append(exp) + } else if exp < 0 then { + s.append('0').append('.') + while { + exp += 1 + exp != 0 + } do s.append('0') + s.append(dv) + var i = s.length - 1 + while s.charAt(i) == '0' do i -= 1 + s.setLength(i + 1) + s + } else if exp + 1 < len then { + val dotOff = s.length + exp + 1 + s.append(dv) + var i = s.length - 1 + while s.charAt(i) == '0' do i -= 1 + s.setLength(i + 1) + s.insert(dotOff, '.') + } else s.append(dv).append('.').append('0') + } + }.toString + } + + private def rop(g1: Long, g0: Long, cp: Long): Long = { + val x1 = multiplyHigh(g0, cp) // FIXME: Use Math.multiplyHigh after dropping JDK 8 support + val z = (g1 * cp >>> 1) + x1 + val y1 = multiplyHigh(g1, cp) // FIXME: Use Math.multiplyHigh after dropping JDK 8 support + (z >>> 63) + y1 | -(z & 0x7fffffffffffffffL) >>> 63 + } + + private def rop(g: Long, cp: Int): Int = { + val x1 = + ((g & 0xffffffffL) * cp >>> 32) + (g >>> 32) * cp // FIXME: Use Math.multiplyHigh after dropping JDK 8 support + (x1 >>> 31).toInt | -x1.toInt >>> 31 + } + + private def multiplyHigh(x: Long, y: Long): Long = { + val x2 = x & 0xffffffffL + val y2 = y & 0xffffffffL + val b = x2 * y2 + val x1 = x >>> 32 + val y1 = y >>> 32 + val a = x1 * y1 + (((b >>> 32) + (x1 + x2) * (y1 + y2) - b - a) >>> 32) + a + } + + // Adoption of a nice trick form Daniel Lemire's blog that works for numbers up to 10^18: + // https://lemire.me/blog/2021/06/03/computing-the-number-of-digits-of-an-integer-even-faster/ + private def digitCount(x: Long): Int = (offsets(java.lang.Long.numberOfLeadingZeros(x)) + x >> 58).toInt + + final private val offsets = Array( + 5088146770730811392L, 5088146770730811392L, 5088146770730811392L, 5088146770730811392L, 5088146770730811392L, 5088146770730811392L, 5088146770730811392L, 5088146770730811392L, 4889916394579099648L, 4889916394579099648L, 4889916394579099648L, + 4610686018427387904L, 4610686018427387904L, 4610686018427387904L, 4610686018427387904L, 4323355642275676160L, 4323355642275676160L, 4323355642275676160L, 4035215266123964416L, 4035215266123964416L, 4035215266123964416L, 3746993889972252672L, + 3746993889972252672L, 3746993889972252672L, 3746993889972252672L, 3458764413820540928L, 3458764413820540928L, 3458764413820540928L, 3170534127668829184L, 3170534127668829184L, 3170534127668829184L, 2882303760517117440L, 2882303760517117440L, + 2882303760517117440L, 2882303760517117440L, 2594073385265405696L, 2594073385265405696L, 2594073385265405696L, 2305843009203693952L, 2305843009203693952L, 2305843009203693952L, 2017612633060982208L, 2017612633060982208L, 2017612633060982208L, + 2017612633060982208L, 1729382256910170464L, 1729382256910170464L, 1729382256910170464L, 1441151880758548720L, 1441151880758548720L, 1441151880758548720L, 1152921504606845976L, 1152921504606845976L, 1152921504606845976L, 1152921504606845976L, + 864691128455135132L, 864691128455135132L, 864691128455135132L, 576460752303423478L, 576460752303423478L, 576460752303423478L, 576460752303423478L, 576460752303423478L, 576460752303423478L, 576460752303423478L + ) + + private val gs: Array[Long] = Array( + 5696189077778435540L, 6557778377634271669L, 9113902524445496865L, 1269073367360058862L, 7291122019556397492L, 1015258693888047090L, 5832897615645117993L, 6346230177223303157L, 4666318092516094394L, 8766332956520552849L, 7466108948025751031L, + 8492109508320019073L, 5972887158420600825L, 4949013199285060097L, 4778309726736480660L, 3959210559428048077L, 7645295562778369056L, 6334736895084876923L, 6116236450222695245L, 3223115108696946377L, 4892989160178156196L, 2578492086957557102L, + 7828782656285049914L, 436238524390181040L, 6263026125028039931L, 2193665226883099993L, 5010420900022431944L, 9133629810990300641L, 8016673440035891111L, 9079784475471615541L, 6413338752028712889L, 5419153173006337271L, 5130671001622970311L, + 6179996945776024979L, 8209073602596752498L, 6198646298499729642L, 6567258882077401998L, 8648265853541694037L, 5253807105661921599L, 1384589460720489745L, 8406091369059074558L, 5904691951894693915L, 6724873095247259646L, 8413102376257665455L, + 5379898476197807717L, 4885807493635177203L, 8607837561916492348L, 438594360332462878L, 6886270049533193878L, 4040224303007880625L, 5509016039626555102L, 6921528257148214824L, 8814425663402488164L, 3695747581953323071L, 7051540530721990531L, + 4801272472933613619L, 5641232424577592425L, 1996343570975935733L, 9025971879324147880L, 3194149713561497173L, 7220777503459318304L, 2555319770849197738L, 5776622002767454643L, 3888930224050313352L, 4621297602213963714L, 6800492993982161005L, + 7394076163542341943L, 5346765568258592123L, 5915260930833873554L, 7966761269348784022L, 4732208744667098843L, 8218083422849982379L, 7571533991467358150L, 2080887032334240837L, 6057227193173886520L, 1664709625867392670L, 4845781754539109216L, + 1331767700693914136L, 7753250807262574745L, 7664851543223128102L, 6202600645810059796L, 6131881234578502482L, 4962080516648047837L, 3060830580291846824L, 7939328826636876539L, 6742003335837910079L, 6351463061309501231L, 7238277076041283225L, + 5081170449047600985L, 3945947253462071419L, 8129872718476161576L, 6313515605539314269L, 6503898174780929261L, 3206138077060496254L, 5203118539824743409L, 720236054277441842L, 8324989663719589454L, 4841726501585817270L, 6659991730975671563L, + 5718055608639608977L, 5327993384780537250L, 8263793301653597505L, 8524789415648859601L, 3998697245790980200L, 6819831532519087681L, 1354283389261828999L, 5455865226015270144L, 8462124340893283845L, 8729384361624432231L, 8005375723316388668L, + 6983507489299545785L, 4559626171282155773L, 5586805991439636628L, 3647700937025724618L, 8938889586303418605L, 3991647091870204227L, 7151111669042734884L, 3193317673496163382L, 5720889335234187907L, 4399328546167885867L, 9153422936374700651L, + 8883600081239572549L, 7322738349099760521L, 5262205657620702877L, 5858190679279808417L, 2365090118725607140L, 4686552543423846733L, 7426095317093351197L, 7498484069478154774L, 813706063123630946L, 5998787255582523819L, 2495639257869859918L, + 4799029804466019055L, 3841185813666843096L, 7678447687145630488L, 6145897301866948954L, 6142758149716504390L, 8606066656235469486L, 4914206519773203512L, 6884853324988375589L, 7862730431637125620L, 3637067690497580296L, 6290184345309700496L, + 2909654152398064237L, 5032147476247760397L, 483048914547496228L, 8051435961996416635L, 2617552670646949126L, 6441148769597133308L, 2094042136517559301L, 5152919015677706646L, 5364582523955957764L, 8244670425084330634L, 4893983223587622099L, + 6595736340067464507L, 5759860986241052841L, 5276589072053971606L, 918539974250931950L, 8442542515286354569L, 7003687180914356604L, 6754034012229083655L, 7447624152102440445L, 5403227209783266924L, 5958099321681952356L, 8645163535653227079L, + 3998935692578258285L, 6916130828522581663L, 5043822961433561789L, 5532904662818065330L, 7724407183888759755L, 8852647460508904529L, 3135679457367239799L, 7082117968407123623L, 4353217973264747001L, 5665694374725698898L, 7171923193353707924L, + 9065110999561118238L, 407030665140201709L, 7252088799648894590L, 4014973346854071690L, 5801671039719115672L, 3211978677483257352L, 4641336831775292537L, 8103606164099471367L, 7426138930840468060L, 5587072233075333540L, 5940911144672374448L, + 4469657786460266832L, 4752728915737899558L, 7265075043910123789L, 7604366265180639294L, 556073626030467093L, 6083493012144511435L, 2289533308195328836L, 4866794409715609148L, 1831626646556263069L, 7786871055544974637L, 1085928227119065748L, + 6229496844435979709L, 6402765803808118083L, 4983597475548783767L, 6966887050417449628L, 7973755960878054028L, 3768321651184098759L, 6379004768702443222L, 6704006135689189330L, 5103203814961954578L, 1673856093809441141L, 8165126103939127325L, + 833495342724150664L, 6532100883151301860L, 666796274179320531L, 5225680706521041488L, 533437019343456425L, 8361089130433666380L, 8232196860433350926L, 6688871304346933104L, 6585757488346680741L, 5351097043477546483L, 7113280398048299755L, + 8561755269564074374L, 313202192651548637L, 6849404215651259499L, 2095236161492194072L, 5479523372521007599L, 3520863336564710419L, 8767237396033612159L, 99358116390671185L, 7013789916826889727L, 1924160900483492110L, 5611031933461511781L, + 7073351942499659173L, 8977651093538418850L, 7628014293257544353L, 7182120874830735080L, 6102411434606035483L, 5745696699864588064L, 4881929147684828386L, 9193114719783340903L, 2277063414182859933L, 7354491775826672722L, 5510999546088198270L, + 5883593420661338178L, 719450822128648293L, 4706874736529070542L, 4264909472444828957L, 7530999578446512867L, 8668529563282681493L, 6024799662757210294L, 3245474835884234871L, 4819839730205768235L, 4441054276078343059L, 7711743568329229176L, + 7105686841725348894L, 6169394854663383341L, 3839875066009323953L, 4935515883730706673L, 1227225645436504001L, 7896825413969130677L, 118886625327451240L, 6317460331175304541L, 5629132522374826477L, 5053968264940243633L, 2658631610528906020L, + 8086349223904389813L, 2409136169475294470L, 6469079379123511850L, 5616657750322145900L, 5175263503298809480L, 4493326200257716720L, 8280421605278095168L, 7189321920412346751L, 6624337284222476135L, 217434314217011916L, 5299469827377980908L, + 173947451373609533L, 8479151723804769452L, 7657013551681595899L, 6783321379043815562L, 2436262026603366396L, 5426657103235052449L, 7483032843395558602L, 8682651365176083919L, 6438829327320028278L, 6946121092140867135L, 6995737869226977784L, + 5556896873712693708L, 5596590295381582227L, 8891034997940309933L, 7109870065239576402L, 7112827998352247947L, 153872830078795637L, 5690262398681798357L, 5657121486175901994L, 9104419837890877372L, 1672696748397622544L, 7283535870312701897L, + 6872180620830963520L, 5826828696250161518L, 1808395681922860493L, 4661462957000129214L, 5136065360280198718L, 7458340731200206743L, 2683681354335452463L, 5966672584960165394L, 5836293898210272294L, 4773338067968132315L, 6513709525939172997L, + 7637340908749011705L, 1198563204647900987L, 6109872726999209364L, 958850563718320789L, 4887898181599367491L, 2611754858345611793L, 7820637090558987986L, 489458958611068546L, 6256509672447190388L, 7770264796372675483L, 5005207737957752311L, + 682188614985274902L, 8008332380732403697L, 6625525006089305327L, 6406665904585922958L, 1611071190129533939L, 5125332723668738366L, 4978205766845537474L, 8200532357869981386L, 4275780412210949635L, 6560425886295985109L, 1575949922397804547L, + 5248340709036788087L, 3105434345289198799L, 8397345134458860939L, 6813369359833673240L, 6717876107567088751L, 7295369895237893754L, 5374300886053671001L, 3991621508819359841L, 8598881417685873602L, 2697245599369065423L, 6879105134148698881L, + 7691819701608117823L, 5503284107318959105L, 4308781353915539097L, 8805254571710334568L, 6894050166264862555L, 7044203657368267654L, 9204588947753800367L, 5635362925894614123L, 9208345565573995455L, 9016580681431382598L, 3665306460692661759L, + 7213264545145106078L, 6621593983296039730L, 5770611636116084862L, 8986624001378742108L, 4616489308892867890L, 3499950386361083363L, 7386382894228588624L, 5599920618177733380L, 5909106315382870899L, 6324610901913141866L, 4727285052306296719L, + 6904363128901468655L, 7563656083690074751L, 5512957784129484362L, 6050924866952059801L, 2565691819932632328L, 4840739893561647841L, 207879048575150701L, 7745183829698636545L, 5866629699833106606L, 6196147063758909236L, 4693303759866485285L, + 4956917651007127389L, 1909968600522233067L, 7931068241611403822L, 6745298575577483229L, 6344854593289123058L, 1706890045720076260L, 5075883674631298446L, 5054860851317971332L, 8121413879410077514L, 4398428547366843807L, 6497131103528062011L, + 5363417245264430207L, 5197704882822449609L, 2446059388840589004L, 8316327812515919374L, 7603043836886852730L, 6653062250012735499L, 7927109476880437346L, 5322449800010188399L, 8186361988875305038L, 8515919680016301439L, 7564155960087622576L, + 6812735744013041151L, 7895999175441053223L, 5450188595210432921L, 4472124932981887417L, 8720301752336692674L, 3466051078029109543L, 6976241401869354139L, 4617515269794242796L, 5580993121495483311L, 5538686623206349399L, 8929588994392773298L, + 5172549782388248714L, 7143671195514218638L, 7827388640652509295L, 5714936956411374911L, 727887690409141951L, 9143899130258199857L, 6698643526767492606L, 7315119304206559886L, 1669566006672083762L, 5852095443365247908L, 8714350434821487656L, + 4681676354692198327L, 1437457125744324640L, 7490682167507517323L, 4144605808561874585L, 5992545734006013858L, 7005033461591409992L, 4794036587204811087L, 70003547160262509L, 7670458539527697739L, 1956680082827375175L, 6136366831622158191L, + 3410018473632855302L, 4909093465297726553L, 883340371535329080L, 7854549544476362484L, 8792042223940347174L, 6283639635581089987L, 8878308186523232901L, 5026911708464871990L, 3413297734476675998L, 8043058733543795184L, 5461276375162681596L, + 6434446986835036147L, 6213695507501100438L, 5147557589468028918L, 1281607591258970028L, 8236092143148846269L, 205897738643396882L, 6588873714519077015L, 2009392598285672668L, 5271098971615261612L, 1607514078628538134L, 8433758354584418579L, + 4416696933176616176L, 6747006683667534863L, 5378031953912248102L, 5397605346934027890L, 7991774377871708805L, 8636168555094444625L, 3563466967739958280L, 6908934844075555700L, 2850773574191966624L, 5527147875260444560L, 2280618859353573299L, + 8843436600416711296L, 3648990174965717279L, 7074749280333369037L, 1074517732601618662L, 5659799424266695229L, 6393637408194160414L, 9055679078826712367L, 4695796630997791177L, 7244543263061369894L, 67288490056322619L, 5795634610449095915L, + 1898505199416013257L, 4636507688359276732L, 1518804159532810606L, 7418412301374842771L, 4274761062623452130L, 5934729841099874217L, 1575134442727806543L, 4747783872879899373L, 6794130776295110719L, 7596454196607838997L, 9025934834701221989L, + 6077163357286271198L, 3531399053019067268L, 4861730685829016958L, 6514468057157164137L, 7778769097326427133L, 8578474484080507458L, 6223015277861141707L, 1328756365151540482L, 4978412222288913365L, 6597028314234097870L, 7965459555662261385L, + 1331873265919780784L, 6372367644529809108L, 1065498612735824627L, 5097894115623847286L, 4541747704930570025L, 8156630584998155658L, 3577447513147001717L, 6525304467998524526L, 6551306825259511697L, 5220243574398819621L, 3396371052836654196L, + 8352389719038111394L, 1744844869796736390L, 6681911775230489115L, 3240550303208344274L, 5345529420184391292L, 2592440242566675419L, 8552847072295026067L, 5992578795477635832L, 6842277657836020854L, 1104714221640198342L, 5473822126268816683L, + 2728445784683113836L, 8758115402030106693L, 2520838848122026975L, 7006492321624085354L, 5706019893239531903L, 5605193857299268283L, 6409490321962580684L, 8968310171678829253L, 8410510107769173933L, 7174648137343063403L, 1194384864102473662L, + 5739718509874450722L, 4644856706023889253L, 9183549615799121156L, 53073100154402158L, 7346839692639296924L, 7421156109607342373L, 5877471754111437539L, 7781599295056829060L, 4701977403289150031L, 8069953843416418410L, 7523163845262640050L, + 9222577334724359132L, 6018531076210112040L, 7378061867779487306L, 4814824860968089632L, 5902449494223589845L, 7703719777548943412L, 2065221561273923105L, 6162975822039154729L, 7186200471132003969L, 4930380657631323783L, 7593634784276558337L, + 7888609052210118054L, 1081769210616762369L, 6310887241768094443L, 2710089775864365057L, 5048709793414475554L, 5857420635433402369L, 8077935669463160887L, 3837849794580578305L, 6462348535570528709L, 8604303057777328129L, 5169878828456422967L, + 8728116853592817665L, 8271806125530276748L, 6586289336264687617L, 6617444900424221398L, 8958380283753660417L, 5293955920339377119L, 1632681004890062849L, 8470329472543003390L, 6301638422566010881L, 6776263578034402712L, 5041310738052808705L, + 5421010862427522170L, 343699775700336641L, 8673617379884035472L, 549919641120538625L, 6938893903907228377L, 5973958935009296385L, 5551115123125782702L, 1089818333265526785L, 8881784197001252323L, 3588383740595798017L, 7105427357601001858L, + 6560055807218548737L, 5684341886080801486L, 8937393460516749313L, 9094947017729282379L, 1387108685230112769L, 7275957614183425903L, 2954361355555045377L, 5820766091346740722L, 6052837899185946625L, 4656612873077392578L, 1152921504606846977L, + 7450580596923828125L, 1L, 5960464477539062500L, 1L, 4768371582031250000L, 1L, 7629394531250000000L, 1L, 6103515625000000000L, 1L, 4882812500000000000L, 1L, 7812500000000000000L, 1L, 6250000000000000000L, 1L, 5000000000000000000L, 1L, + 8000000000000000000L, 1L, 6400000000000000000L, 1L, 5120000000000000000L, 1L, 8192000000000000000L, 1L, 6553600000000000000L, 1L, 5242880000000000000L, 1L, 8388608000000000000L, 1L, 6710886400000000000L, 1L, 5368709120000000000L, 1L, + 8589934592000000000L, 1L, 6871947673600000000L, 1L, 5497558138880000000L, 1L, 8796093022208000000L, 1L, 7036874417766400000L, 1L, 5629499534213120000L, 1L, 9007199254740992000L, 1L, 7205759403792793600L, 1L, 5764607523034234880L, 1L, + 4611686018427387904L, 1L, 7378697629483820646L, 3689348814741910324L, 5902958103587056517L, 1106804644422573097L, 4722366482869645213L, 6419466937650923963L, 7555786372591432341L, 8426472692870523179L, 6044629098073145873L, 4896503746925463381L, + 4835703278458516698L, 7606551812282281028L, 7737125245533626718L, 1102436455425918676L, 6189700196426901374L, 4571297979082645264L, 4951760157141521099L, 5501712790637071373L, 7922816251426433759L, 3268717242906448711L, 6338253001141147007L, + 4459648201696114131L, 5070602400912917605L, 9101741783469756789L, 8112963841460668169L, 5339414816696835055L, 6490371073168534535L, 6116206260728423206L, 5192296858534827628L, 4892965008582738565L, 8307674973655724205L, 5984069606361426541L, + 6646139978924579364L, 4787255685089141233L, 5316911983139663491L, 5674478955442268148L, 8507059173023461586L, 5389817513965718714L, 6805647338418769269L, 2467179603801619810L, 5444517870735015415L, 3818418090412251009L, 8711228593176024664L, + 6109468944659601615L, 6968982874540819731L, 6732249563098636453L, 5575186299632655785L, 3541125243107954001L, 8920298079412249256L, 5665800388972726402L, 7136238463529799405L, 2687965903807225960L, 5708990770823839524L, 2150372723045780768L, + 9134385233318143238L, 7129945171615159552L, 7307508186654514591L, 169932915179262157L, 5846006549323611672L, 7514643961627230372L, 4676805239458889338L, 2322366354559873974L, 7482888383134222941L, 1871111759924843197L, 5986310706507378352L, + 8875587037423695204L, 4789048565205902682L, 3411120815197045840L, 7662477704329444291L, 7302467711686228506L, 6129982163463555433L, 3997299761978027643L, 4903985730770844346L, 6887188624324332438L, 7846377169233350954L, 7330152984177021577L, + 6277101735386680763L, 7708796794712572423L, 5021681388309344611L, 633014213657192454L, 8034690221294951377L, 6546845963964373411L, 6427752177035961102L, 1548127956429588405L, 5142201741628768881L, 6772525587256536209L, 8227522786606030210L, + 7146692124868547611L, 6582018229284824168L, 5717353699894838089L, 5265614583427859334L, 8263231774657780795L, 8424983333484574935L, 7687147617339583786L, 6739986666787659948L, 6149718093871667029L, 5391989333430127958L, 8609123289839243947L, + 8627182933488204734L, 2706550819517059345L, 6901746346790563787L, 4009915062984602637L, 5521397077432451029L, 8741955272500547595L, 8834235323891921647L, 8453105213888010667L, 7067388259113537318L, 3073135356368498210L, 5653910607290829854L, + 6147857099836708891L, 9046256971665327767L, 4302548137625868741L, 7237005577332262213L, 8976061732213560478L, 5789604461865809771L, 1646826163657982898L, 4631683569492647816L, 8696158560410206965L, 7410693711188236507L, 1001132845059645012L, + 5928554968950589205L, 6334929498160581494L, 4742843975160471364L, 5067943598528465196L, 7588550360256754183L, 2574686535532678828L, 6070840288205403346L, 5749098043168053386L, 4856672230564322677L, 2754604027163487547L, 7770675568902916283L, + 6252040850832535236L, 6216540455122333026L, 8690981495407938512L, 4973232364097866421L, 5108110788955395648L, 7957171782556586274L, 4483628447586722714L, 6365737426045269019L, 5431577165440333333L, 5092589940836215215L, 6189936139723221828L, + 8148143905337944345L, 680525786702379117L, 6518515124270355476L, 544420629361903293L, 5214812099416284380L, 7814234132973343281L, 8343699359066055009L, 3279402575902573442L, 6674959487252844007L, 4468196468093013915L, 5339967589802275205L, + 9108580396587276617L, 8543948143683640329L, 5350356597684866779L, 6835158514946912263L, 6124959685518848585L, 5468126811957529810L, 8589316563156989191L, 8749002899132047697L, 4519534464196406897L, 6999202319305638157L, 9149650793469991003L, + 5599361855444510526L, 3630371820034082479L, 8958978968711216842L, 2119246097312621643L, 7167183174968973473L, 7229420099962962799L, 5733746539975178779L, 249512857857504755L, 9173994463960286046L, 4088569387313917931L, 7339195571168228837L, + 1426181102480179183L, 5871356456934583069L, 6674968104097008831L, 4697085165547666455L, 7184648890648562227L, 7515336264876266329L, 2272066188182923754L, 6012269011901013063L, 3662327357917294165L, 4809815209520810450L, 6619210701075745655L, + 7695704335233296721L, 1367365084866417240L, 6156563468186637376L, 8472589697376954439L, 4925250774549309901L, 4933397350530608390L, 7880401239278895842L, 4204086946107063100L, 6304320991423116673L, 8897292778998515965L, 5043456793138493339L, + 1583811001085947287L, 8069530869021589342L, 6223446416479425982L, 6455624695217271474L, 1289408318441630463L, 5164499756173817179L, 2876201062124259532L, 8263199609878107486L, 8291270514140725574L, 6610559687902485989L, 4788342003941625298L, + 5288447750321988791L, 5675348010524255400L, 8461516400515182066L, 5391208002096898316L, 6769213120412145653L, 2468291994306563491L, 5415370496329716522L, 5663982410187161116L, 8664592794127546436L, 1683674226815637140L, 6931674235302037148L, + 8725637010936330358L, 5545339388241629719L, 1446486386636198802L, 8872543021186607550L, 6003727033359828406L, 7098034416949286040L, 4802981626687862725L, 5678427533559428832L, 3842385301350290180L, 9085484053695086131L, 7992490889531419449L, + 7268387242956068905L, 4549318304254180398L, 5814709794364855124L, 3639454643403344318L, 4651767835491884099L, 4756238122093630616L, 7442828536787014559L, 2075957773236943501L, 5954262829429611647L, 3505440625960509963L, 4763410263543689317L, + 8338375722881273455L, 7621456421669902908L, 5962703527126216881L, 6097165137335922326L, 8459511636442883828L, 4877732109868737861L, 4922934901783351901L, 7804371375789980578L, 4187347028111452718L, 6243497100631984462L, 7039226437231072498L, + 4994797680505587570L, 1942032335042947675L, 7991676288808940112L, 3107251736068716280L, 6393341031047152089L, 8019824610967838509L, 5114672824837721671L, 8260534096145225969L, 8183476519740354675L, 304133702235675419L, 6546781215792283740L, + 243306961788540335L, 5237424972633826992L, 194645569430832268L, 8379879956214123187L, 2156107318460286790L, 6703903964971298549L, 7258909076881094917L, 5363123171977038839L, 7651801668875831096L, 8580997075163262143L, 6708859448088464268L, + 6864797660130609714L, 9056436373212681737L, 5491838128104487771L, 9089823505941100552L, 8786941004967180435L, 1630996757909074751L, 7029552803973744348L, 1304797406327259801L, 5623642243178995478L, 4733186739803718164L, 8997827589086392765L, + 5728424376314993901L, 7198262071269114212L, 4582739501051995121L, 5758609657015291369L, 9200214822954461581L, 9213775451224466191L, 9186320494614273045L, 7371020360979572953L, 5504381988320463275L, 5896816288783658362L, 8092854405398280943L, + 4717453031026926690L, 2784934709576714431L, 7547924849643082704L, 4455895535322743090L, 6038339879714466163L, 5409390835629149634L, 4830671903771572930L, 8016861483245230030L, 7729075046034516689L, 3603606336337592240L, 6183260036827613351L, + 4727559476441028954L, 4946608029462090681L, 1937373173781868001L, 7914572847139345089L, 8633820300163854287L, 6331658277711476071L, 8751730647502038591L, 5065326622169180857L, 5156710110630675711L, 8104522595470689372L, 872038547525260492L, + 6483618076376551497L, 6231654060133073878L, 5186894461101241198L, 1295974433364548779L, 8299031137761985917L, 228884686012322885L, 6639224910209588733L, 5717130970922723793L, 5311379928167670986L, 8263053591480089358L, 8498207885068273579L, + 308164894771456841L, 6798566308054618863L, 2091206323188120634L, 5438853046443695090L, 5362313873292406831L, 8702164874309912144L, 8579702197267850929L, 6961731899447929715L, 8708436165185235905L, 5569385519558343772L, 6966748932148188724L, + 8911016831293350036L, 3768100661953281312L, 7128813465034680029L, 1169806122191669888L, 5703050772027744023L, 2780519305124291072L, 9124881235244390437L, 2604156480827910553L, 7299904988195512349L, 7617348406775193928L, 5839923990556409879L, + 7938553132791110304L, 4671939192445127903L, 8195516913603843405L, 7475102707912204646L, 2044780617540418478L, 5980082166329763716L, 9014522123516155429L, 4784065733063810973L, 5366943291441969181L, 7654505172902097557L, 6742434858936195528L, + 6123604138321678046L, 1704599072407046100L, 4898883310657342436L, 8742376887409457526L, 7838213297051747899L, 1075082168258445910L, 6270570637641398319L, 2704740141977711890L, 5016456510113118655L, 4008466520953124674L, 8026330416180989848L, + 6413546433524999478L, 6421064332944791878L, 8820185961561909905L, 5136851466355833503L, 1522125547136662440L, 8218962346169333605L, 590726468047704741L, 6575169876935466884L, 472581174438163793L, 5260135901548373507L, 2222739346921486196L, + 8416217442477397611L, 5401057362445333075L, 6732973953981918089L, 2476171482585311299L, 5386379163185534471L, 3825611593439204201L, 8618206661096855154L, 2431629734760816398L, 6894565328877484123L, 3789978195179608280L, 5515652263101987298L, + 6721331370885596947L, 8825043620963179677L, 8909455786045999954L, 7060034896770543742L, 3438215814094889640L, 5648027917416434993L, 8284595873388777197L, 9036844667866295990L, 2187306953196312545L, 7229475734293036792L, 1749845562557050036L, + 5783580587434429433L, 6933899672158505514L, 4626864469947543547L, 13096515613938926L, 7402983151916069675L, 1865628832353257443L, 5922386521532855740L, 1492503065882605955L, 4737909217226284592L, 1194002452706084764L, 7580654747562055347L, + 3755078331700690783L, 6064523798049644277L, 8538085887473418112L, 4851619038439715422L, 3141119895236824166L, 7762590461503544675L, 6870466239749873827L, 6210072369202835740L, 5496372991799899062L, 4968057895362268592L, 4397098393439919250L, + 7948892632579629747L, 8880031836874825961L, 6359114106063703798L, 3414676654757950445L, 5087291284850963038L, 6421090138548270680L, 8139666055761540861L, 8429069814306277926L, 6511732844609232689L, 4898581444074067179L, 5209386275687386151L, + 5763539562630208905L, 8335018041099817842L, 5532314485466423924L, 6668014432879854274L, 736502773631228816L, 5334411546303883419L, 2433876626275938215L, 8535058474086213470L, 7583551416783411467L, 6828046779268970776L, 6066841133426729173L, + 5462437423415176621L, 3008798499370428177L, 8739899877464282594L, 1124728784250774760L, 6991919901971426075L, 2744457434771574970L, 5593535921577140860L, 2195565947817259976L, 8949657474523425376L, 3512905516507615961L, 7159725979618740301L, + 965650005835137607L, 5727780783694992240L, 8151217634151930732L, 9164449253911987585L, 3818576177788313364L, 7331559403129590068L, 3054860942230650691L, 5865247522503672054L, 6133237568526430876L, 4692198018002937643L, 6751264462192099863L, + 7507516828804700229L, 8957348732136404618L, 6006013463043760183L, 9010553393080078856L, 4804810770435008147L, 1674419492351197600L, 7687697232696013035L, 4523745595132871322L, 6150157786156810428L, 3618996476106297057L, 4920126228925448342L, + 6584545995626947969L, 7872201966280717348L, 3156575963519296104L, 6297761573024573878L, 6214609585557347207L, 5038209258419659102L, 8661036483187788089L, 8061134813471454564L, 6478960743616640295L, 6448907850777163651L, 7027843002264267398L, + 5159126280621730921L, 3777599994440458757L, 8254602048994769474L, 2354811176362823687L, 6603681639195815579L, 3728523348461214111L, 5282945311356652463L, 4827493086139926451L, 8452712498170643941L, 5879314530452927160L, 6762169998536515153L, + 2858777216991386566L, 5409735998829212122L, 5976370588335019576L, 8655577598126739396L, 2183495311852210675L, 6924462078501391516L, 9125493878965589187L, 5539569662801113213L, 5455720695801516188L, 8863311460481781141L, 6884478705911470739L, + 7090649168385424913L, 3662908557358221429L, 5672519334708339930L, 6619675660628487467L, 9076030935533343889L, 1368109020150804139L, 7260824748426675111L, 2939161623491598473L, 5808659798741340089L, 506654891422323617L, 4646927838993072071L, + 2249998320508814055L, 7435084542388915313L, 9134020534926967972L, 5948067633911132251L, 1773193205828708893L, 4758454107128905800L, 8797252194146787761L, 7613526571406249281L, 4852231473780084609L, 6090821257124999425L, 2037110771653112526L, + 4872657005699999540L, 1629688617322490021L, 7796251209119999264L, 2607501787715984033L, 6237000967295999411L, 3930675837543742388L, 4989600773836799529L, 1299866262664038749L, 7983361238138879246L, 5769134835004372321L, 6386688990511103397L, + 2770633460632542696L, 5109351192408882717L, 7750529990618899641L, 8174961907854212348L, 5022150355506418780L, 6539969526283369878L, 7707069099147045347L, 5231975621026695903L, 631632057204770793L, 8371160993642713444L, 8389308921011453915L, + 6696928794914170755L, 8556121544180118293L, 5357543035931336604L, 6844897235344094635L, 8572068857490138567L, 5417812354437685931L, 6857655085992110854L, 644901068808238421L, 5486124068793688683L, 2360595262417545899L, 8777798510069901893L, + 1932278012497118276L, 7022238808055921514L, 5235171224739604944L, 5617791046444737211L, 6032811387162639117L, 8988465674311579538L, 5963149404718312264L, 7190772539449263630L, 8459868338516560134L, 5752618031559410904L, 6767894670813248108L, + 9204188850495057447L, 5294608251188331487L + ) +} + +// specialised Options to avoid boxing. Prefer .isEmpty guarded access to .value +// for higher performance: pattern matching is slightly slower. + +sealed abstract class ByteOption { + def isEmpty: Boolean + def value: Byte +} +case object ByteNone extends ByteOption { + def isEmpty = true + def value: Byte = throw new java.util.NoSuchElementException +} +case class ByteSome(value: Byte) extends ByteOption { + def isEmpty = false +} + +sealed abstract class ShortOption { + def isEmpty: Boolean + def value: Short +} +case object ShortNone extends ShortOption { + def isEmpty = true + def value: Short = throw new java.util.NoSuchElementException +} +case class ShortSome(value: Short) extends ShortOption { + def isEmpty = false +} + +sealed abstract class IntOption { + def isEmpty: Boolean + def value: Int +} +case object IntNone extends IntOption { + def isEmpty = true + def value: Int = throw new java.util.NoSuchElementException +} +case class IntSome(value: Int) extends IntOption { + def isEmpty = false +} + +sealed abstract class LongOption { + def isEmpty: Boolean + def value: Long +} +case object LongNone extends LongOption { + def isEmpty = true + def value: Long = throw new java.util.NoSuchElementException +} +case class LongSome(value: Long) extends LongOption { + def isEmpty = false +} + +sealed abstract class FloatOption { + def isEmpty: Boolean + def value: Float +} +case object FloatNone extends FloatOption { + def isEmpty = true + def value: Float = throw new java.util.NoSuchElementException +} +case class FloatSome(value: Float) extends FloatOption { + def isEmpty = false +} + +sealed abstract class DoubleOption { + def isEmpty: Boolean + def value: Double +} +case object DoubleNone extends DoubleOption { + def isEmpty = true + def value: Double = throw new java.util.NoSuchElementException +} +case class DoubleSome(value: Double) extends DoubleOption { + def isEmpty = false +} + +// The underlying implementation uses an exception that has no stack trace for +// the failure case, which is 20x faster than retaining stack traces. Therefore, +// we require no boxing of the results on the happy path. This slows down the +// unhappy path a little bit, but it's still on the same order of magnitude as +// the happy path. +// +// This API should only be used by people who know what they are doing. Note +// that JsonSource implementations consume one character beyond the number that is +// parsed, because there is no terminator character. +object UnsafeNumbers { + + // should never escape into user code + case object UnsafeNumber + extends Exception( + "if you see this a dev made a mistake using UnsafeNumbers" + ) + with NoStackTrace + + def byte(num: String): Byte = + byte_(new JsonSource(num), true) + def byte_(in: JsonSource, consume: Boolean): Byte = + long__(in, Byte.MinValue, Byte.MaxValue, consume).toByte + + def short(num: String): Short = + short_(new JsonSource(num), true) + def short_(in: JsonSource, consume: Boolean): Short = + long__(in, Short.MinValue, Short.MaxValue, consume).toShort + + def int(num: String): Int = + int_(new JsonSource(num), true) + def int_(in: JsonSource, consume: Boolean): Int = + long__(in, Int.MinValue, Int.MaxValue, consume).toInt + + def long(num: String): Long = + long_(new JsonSource(num), true) + def long_(in: JsonSource, consume: Boolean): Long = + long__(in, Long.MinValue, Long.MaxValue, consume) + + def bigInteger(num: String, max_bits: Int): java.math.BigInteger = + bigInteger_(new JsonSource(num), true, max_bits) + def bigInteger_( + in: JsonSource, + consume: Boolean, + max_bits: Int + ): java.math.BigInteger = { + var current: Int = in.readChar() + var negative = false + + if current == '-' then { + negative = true + current = in.readChar() + } else if current == '+' then current = in.readChar() + if current == -1 then throw UnsafeNumber + + bigDecimal__(in, consume, negative, current, true, max_bits).unscaledValue + } + + // measured faster than Character.isDigit + @inline private def isDigit(i: Int): Boolean = + '0' <= i && i <= '9' + + // is it worth keeping this custom long__ instead of using bigInteger since it + // is approximately double the performance. + def long__(in: JsonSource, lower: Long, upper: Long, consume: Boolean): Long = { + var current: Int = 0 + + current = in.readChar() + if current == BUFFER_EXCEEDED then throw JsonParseError("Read buffer exceeded", in) + var negative = false + if current == '-' then { + negative = true + current = in.readChar() + if current == BUFFER_EXCEEDED then throw JsonParseError("Read buffer exceeded", in) + } else if current == '+' then { + current = in.readChar() + if current == BUFFER_EXCEEDED then throw JsonParseError("Read buffer exceeded", in) + } + + if !isDigit(current) then + in.backspace() + throw JsonParseError("Unexpected character in Int/Long value: " + current.toChar, in) + + var accum: Long = 0L + while { + { + val c = current - '0' + if accum <= longunderflow then + if accum < longunderflow then throw UnsafeNumber + else if accum == longunderflow && c == 9 then throw UnsafeNumber + // count down, not up, because it is larger + accum = accum * 10 - c // should never underflow + current = in.readChar() + }; current != -1 && isDigit(current) + } do () + + if consume && current != BUFFER_EXCEEDED then throw JsonParseError("Read buffer exceeded", in) + + if negative then + if accum < lower || upper < accum then throw UnsafeNumber + else accum + else if accum == Long.MinValue then throw UnsafeNumber + else { + accum = -accum + if accum < lower || upper < accum then throw UnsafeNumber + else accum + } + } + + def float(num: String, max_bits: Int): Float = + float_(new JsonSource(num), true, max_bits) + + def float_(in: JsonSource, consume: Boolean, max_bits: Int): Float = { + var current: Int = in.readChar() + var negative = false + + def readAll(s: String): Unit = { + var i = 0 + val len = s.length + + while i < len do { + current = in.readChar() + if current != s(i) then + in.backspace() + throw JsonParseError("Unexpected character in Int/Long value: " + current.toChar, in) + i += 1 + } + + current = in.readChar() // to be consistent read the terminator + + if consume && current != BUFFER_EXCEEDED then throw JsonParseError("Read buffer exceeded", in) + } + + if current == 'N' then { + readAll("aN") + return Float.NaN + } + + if current == '-' then { + negative = true + current = in.readChar() + } else if current == '+' then { + current = in.readChar() + } + + if current == 'I' then { + readAll("nfinity") + + if negative then return Float.NegativeInfinity + else return Float.PositiveInfinity + } + + if current == BUFFER_EXCEEDED then throw JsonParseError("Read buffer exceeded", in) + + val res = bigDecimal__(in, consume, negative = negative, initial = current, int_only = false, max_bits = max_bits) + + if negative && res.unscaledValue == java.math.BigInteger.ZERO then -0.0f + else res.floatValue + } + + def double(num: String, max_bits: Int): Double = + double_(new JsonSource(num), true, max_bits) + + def double_(in: JsonSource, consume: Boolean, max_bits: Int): Double = { + var current: Int = in.readChar() + var negative = false + + def readall(s: String): Unit = { + var i = 0 + val len = s.length + while i < len do { + current = in.readChar() + if current != s(i) then throw UnsafeNumber + i += 1 + } + current = in.readChar() // to be consistent read the terminator + if consume && current != -1 then throw UnsafeNumber + } + + if current == 'N' then { + readall("aN") + return Double.NaN + } + + if current == '-' then { + negative = true + current = in.readChar() + } else if current == '+' then current = in.readChar() + + if current == 'I' then { + readall("nfinity") + if negative then return Double.NegativeInfinity + else return Double.PositiveInfinity + } + + if current == -1 then throw UnsafeNumber + + // we could avoid going via BigDecimal if we wanted to do something like + // https://github.com/plokhotnyuk/jsoniter-scala/blob/56ff2a60e28aa27bd4788caf3b1557a558c00fa1/jsoniter-scala-core/jvm/src/main/scala/com/github/plokhotnyuk/jsoniter_scala/core/JsonJsonReader.scala#L1395-L1425 + // based on + // https://www.reddit.com/r/rust/comments/a6j5j1/making_rust_float_parsing_fast_and_correct + // + // the fallback of .doubleValue tends to call out to parseDouble which + // ultimately uses strtod from the system libraries and they may loop until + // the answer converges + // https://github.com/rust-lang/rust/pull/27307/files#diff-fe6c36003393c49bf7e5c413458d6d9cR43-R84 + val res = bigDecimal__(in, consume, negative, current, false, max_bits) + // BigDecimal doesn't have a negative zero, so we need to apply manually + if negative && res.unscaledValue == java.math.BigInteger.ZERO then -0.0 + // TODO implement Algorithm M or Bigcomp and avoid going via BigDecimal + else res.doubleValue + } + + def bigDecimal(num: String, max_bits: Int): java.math.BigDecimal = + bigDecimal_(new JsonSource(num), true, max_bits) + def bigDecimal_( + in: JsonSource, + consume: Boolean, + max_bits: Int + ): java.math.BigDecimal = { + var current: Int = in.readChar() + var negative = false + + if current == '-' then { + negative = true + current = in.readChar() + } else if current == '+' then current = in.readChar() + if current == -1 then throw UnsafeNumber + + bigDecimal__(in, consume, negative, current, false, max_bits) + } + + def bigDecimal__( + in: JsonSource, + consume: Boolean, + negative: Boolean, + initial: Int, + int_only: Boolean, + max_bits: Int + ): java.math.BigDecimal = { + var current: Int = initial + // record the significand as Long until it overflows, then swap to BigInteger + var sig: Long = -1 // -1 means it hasn't been seen yet + var sig_ : java.math.BigInteger = null // non-null wins over sig + var dot: Int = 0 // counts from the right + var exp: Int = 0 // implied + + def advance(): Boolean = { + current = in.readChar() + current != BUFFER_EXCEEDED + } + + // skip trailing zero on the left + while current == '0' do { + sig = 0 + if !advance() then return java.math.BigDecimal.ZERO + } + + def push_sig(): Unit = { + val c = current - '0' + // would be nice if there was a fused instruction... + if sig_ != null then { + sig_ = sig_ + .multiply(java.math.BigInteger.TEN) + .add(bigIntegers(c)) + // arbitrary limit on BigInteger size to avoid OOM attacks + if sig_.bitLength >= max_bits then throw JsonParseError("Number of bits exceeded for Float/Double/BigDecimal", in) + } else if sig >= longoverflow then + sig_ = java.math.BigInteger + .valueOf(sig) + .multiply(java.math.BigInteger.TEN) + .add(bigIntegers(c)) + else if sig < 0 then sig = c.toLong + else sig = sig * 10 + c + } + + def significand() = + if sig <= 0 then java.math.BigDecimal.ZERO + else { + val res = + if sig_ != null then new java.math.BigDecimal(sig_) + else new java.math.BigDecimal(sig) + if negative then res.negate else res + } + + while isDigit(current) do { + push_sig() + if !advance() then return significand() + } + + if int_only then { + if consume && current != BUFFER_EXCEEDED then throw JsonParseError("Read buffer exceeded", in) + return significand() + } + + if current == '.' then { + if sig < 0 then sig = 0 // e.g. ".1" is shorthand for "0.1" + if !advance() then return significand() + while isDigit(current) do { + dot += 1 + if sig > 0 || current != '0' then push_sig() + // overflowed... + if dot < 0 then throw JsonParseError("Read buffer exceeded", in) + advance() + } + } + + if sig < 0 then + in.backspace() + throw JsonParseError("Malformed Float/Double/BigDecimal", in) // no significand + + if current == 'E' || current == 'e' then exp = int_(in, consume) + else if consume && current != BUFFER_EXCEEDED then throw JsonParseError("Read buffer exceeded", in) + + val scale = if dot < 1 then exp else exp - dot + val res = significand() + if scale != 0 then res.scaleByPowerOfTen(scale) + else res + } + // note that bigDecimal does not have a negative zero + private val bigIntegers: Array[java.math.BigInteger] = + (0L to 9L).map(java.math.BigInteger.valueOf).toArray + private val longunderflow: Long = Long.MinValue / 10L + private val longoverflow: Long = Long.MaxValue / 10L +} +// $COVERAGE-ON$ diff --git a/src/main/scala/co.blocke.scalajack/json/FastStringBuilder.scala b/src/main/scala/co.blocke.scalajack/json/FastStringBuilder.scala new file mode 100644 index 00000000..e02925b2 --- /dev/null +++ b/src/main/scala/co.blocke.scalajack/json/FastStringBuilder.scala @@ -0,0 +1,94 @@ +package co.blocke.scalajack +package json + +import java.nio.CharBuffer +import java.util.Arrays +import scala.annotation.tailrec + +// like StringBuilder but doesn't have any encoding or range checks +final class FastStringBuilder(initial: Int = 16) { + private var chars: Array[Char] = new Array[Char](initial) + private var i: Int = 0 + + def clear() = i = 0 + def length = i + def setLength(ni: Int) = i = ni + + @tailrec + final def ensureCapacity(size: Int): Unit = + if i + size < chars.length then () + else + chars = Arrays.copyOf(chars, chars.length * 2) + ensureCapacity(size) + + def append(c: Char): Unit = + if i == chars.length then chars = Arrays.copyOf(chars, chars.length * 2) + chars(i) = c + i += 1 + + final private val escapedChars: Array[Byte] = Array( + -1, -1, -1, -1, -1, -1, -1, -1, 98, 116, 110, -1, 102, 114, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 0, 0, 34, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 92, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1 + ) + final private val lowerCaseHexDigits: Array[Short] = Array( + 12336, 12592, 12848, 13104, 13360, 13616, 13872, 14128, 14384, 14640, 24880, 25136, 25392, 25648, 25904, 26160, 12337, 12593, 12849, 13105, 13361, 13617, 13873, 14129, 14385, 14641, 24881, 25137, 25393, 25649, 25905, 26161, 12338, 12594, 12850, 13106, + 13362, 13618, 13874, 14130, 14386, 14642, 24882, 25138, 25394, 25650, 25906, 26162, 12339, 12595, 12851, 13107, 13363, 13619, 13875, 14131, 14387, 14643, 24883, 25139, 25395, 25651, 25907, 26163, 12340, 12596, 12852, 13108, 13364, 13620, 13876, 14132, + 14388, 14644, 24884, 25140, 25396, 25652, 25908, 26164, 12341, 12597, 12853, 13109, 13365, 13621, 13877, 14133, 14389, 14645, 24885, 25141, 25397, 25653, 25909, 26165, 12342, 12598, 12854, 13110, 13366, 13622, 13878, 14134, 14390, 14646, 24886, 25142, + 25398, 25654, 25910, 26166, 12343, 12599, 12855, 13111, 13367, 13623, 13879, 14135, 14391, 14647, 24887, 25143, 25399, 25655, 25911, 26167, 12344, 12600, 12856, 13112, 13368, 13624, 13880, 14136, 14392, 14648, 24888, 25144, 25400, 25656, 25912, 26168, + 12345, 12601, 12857, 13113, 13369, 13625, 13881, 14137, 14393, 14649, 24889, 25145, 25401, 25657, 25913, 26169, 12385, 12641, 12897, 13153, 13409, 13665, 13921, 14177, 14433, 14689, 24929, 25185, 25441, 25697, 25953, 26209, 12386, 12642, 12898, 13154, + 13410, 13666, 13922, 14178, 14434, 14690, 24930, 25186, 25442, 25698, 25954, 26210, 12387, 12643, 12899, 13155, 13411, 13667, 13923, 14179, 14435, 14691, 24931, 25187, 25443, 25699, 25955, 26211, 12388, 12644, 12900, 13156, 13412, 13668, 13924, 14180, + 14436, 14692, 24932, 25188, 25444, 25700, 25956, 26212, 12389, 12645, 12901, 13157, 13413, 13669, 13925, 14181, 14437, 14693, 24933, 25189, 25445, 25701, 25957, 26213, 12390, 12646, 12902, 13158, 13414, 13670, 13926, 14182, 14438, 14694, 24934, 25190, + 25446, 25702, 25958, 26214 + ) + + private def appendEscapedUnicode(c: Char): Unit = { + append('\\') + append('u') + append("%04x".format(c.toInt)) + } + + @tailrec + final def appendEscaped(s: String, from: Int, to: Int): Unit = + ensureCapacity(2) + if from >= to then () + else + val ch1 = s.charAt(from) + if ch1 < 0x80 then + val esc = escapedChars(ch1) + if esc == 0 then + chars(i) = ch1 + i += 1 + appendEscaped(s, from + 1, to) + else if esc > 0 then + chars(i) = 0x5c // double quote + chars(i + 1) = esc.toChar + i += 2 + appendEscaped(s, from + 1, to) + else appendEscapedUnicode(ch1) + else if (ch1 & 0xf800) != 0xd800 then appendEscapedUnicode(ch1) + else + var ch2 = 0 + if ch1 >= 0xdc00 || from + 1 >= to || { + ch2 = s.charAt(from + 1).toInt + (ch2 & 0xfc00) != 0xdc00 + } + then throw new JsonIllegalCharacterError("Illegal encoded text character in string value: " + ch2) + appendEscapedUnicode(ch2.toChar) + + def append(s: String): Unit = + ensureCapacity(s.length) + s.getChars(0, s.length, chars, i) + i += s.length + + def append(v: scala.math.BigDecimal): Unit = append(v.toString) + def append(v: scala.math.BigInt): Unit = append(v.toString) + def append(v: Boolean): Unit = append(v.toString) + def append(v: Double): Unit = append(v.toString) + def append(v: Float): Unit = append(v.toString) + def append(v: Int): Unit = append(v.toString) + def append(v: Long): Unit = append(v.toString) + def append(v: Short): Unit = append(v.toString) + def append(v: java.lang.Number): Unit = append(v.toString) + + def result = CharBuffer.wrap(chars, 0, i).toString +} diff --git a/src/main/scala/co.blocke.scalajack/json/JsonCodec.scala b/src/main/scala/co.blocke.scalajack/json/JsonCodec.scala new file mode 100644 index 00000000..fd42dd6d --- /dev/null +++ b/src/main/scala/co.blocke.scalajack/json/JsonCodec.scala @@ -0,0 +1,10 @@ +package co.blocke.scalajack +package json + +import writing.* +import reading.* + +trait JsonCodec[A] { + def encodeValue(in: A, out: JsonOutput): Unit + def decodeValue(in: JsonSource): A +} diff --git a/src/main/scala/co.blocke.scalajack/json/JsonCodecMaker.scala b/src/main/scala/co.blocke.scalajack/json/JsonCodecMaker.scala new file mode 100644 index 00000000..5e2f0fbb --- /dev/null +++ b/src/main/scala/co.blocke.scalajack/json/JsonCodecMaker.scala @@ -0,0 +1,2200 @@ +package co.blocke.scalajack +package json + +import writing.* +import co.blocke.scala_reflection.{RTypeRef, TypedName} +import co.blocke.scala_reflection.reflect.ReflectOnType +import co.blocke.scala_reflection.reflect.rtypeRefs.* +import co.blocke.scala_reflection.rtypes.{EnumRType, JavaClassRType, NonConstructorFieldInfo} +import reading.JsonSource +import scala.jdk.CollectionConverters.* +import scala.quoted.* +import scala.reflect.ClassTag +import scala.annotation.{switch, tailrec} +import scala.collection.Factory +import scala.util.{Failure, Success, Try} +import dotty.tools.dotc.ast.Trees.EmptyTree +import org.apache.commons.text.StringEscapeUtils +import org.apache.commons.lang3.text.translate.CharSequenceTranslator +import dotty.tools.dotc.core.TypeComparer.AnyConstantType +import scala.jdk.CollectionConverters.* + +object JsonCodecMaker: + + def generateCodecFor[T](ref: RTypeRef[T], cfg: SJConfig)(using q: Quotes)(using tt: Type[T]) = + import q.reflect.* + + // Cache generated method Symbols + an array of the generated functions (DefDef) + case class MethodKey(ref: RTypeRef[?], isNonConstructorFields: Boolean) + + val writeMethodSyms = new scala.collection.mutable.HashMap[MethodKey, Symbol] + val writeMethodDefs = new scala.collection.mutable.ArrayBuffer[DefDef] + + // Fantastic Dark Magic here--lifted from Jasoniter. Props! This thing will create a DefDef, and a Symbol to it. + // The Symbol will let you call the generated function later from other macro-generated code. The goal is to use + // generated functions to create cleaner/faster macro code than what straight quotes/splices would create unaided. + def makeWriteFn[U: Type](methodKey: MethodKey, arg: Expr[U], out: Expr[JsonOutput])(f: (Expr[U], Expr[JsonOutput]) => Expr[Unit]): Expr[Unit] = + // Get a symbol, if one already created for this key... else make one. + Apply( + Ref( + writeMethodSyms.getOrElse( + methodKey, { + val sym = Symbol.newMethod( + Symbol.spliceOwner, + "w" + writeMethodSyms.size, // 'w' is for Writer! + MethodType(List("in", "out"))(_ => List(TypeRepr.of[U], TypeRepr.of[JsonOutput]), _ => TypeRepr.of[Unit]) + ) + writeMethodSyms.update(methodKey, sym) + writeMethodDefs += DefDef( + sym, + params => { + val List(List(in, out)) = params + Some(f(in.asExprOf[U], out.asExprOf[JsonOutput]).asTerm.changeOwner(sym)) + } + ) + sym + } + ) + ), + List(arg.asTerm, out.asTerm) + ).asExprOf[Unit] + + val readMethodSyms = new scala.collection.mutable.HashMap[MethodKey, Symbol] + val readMethodDefs = new scala.collection.mutable.ArrayBuffer[DefDef] + + def makeReadFn[U: Type](methodKey: MethodKey, in: Expr[JsonSource])(f: Expr[JsonSource] => Expr[U])(using Quotes)(using Type[JsonSource]): Expr[Unit] = + readMethodSyms.getOrElse( + methodKey, { + val sym = Symbol.newMethod( + Symbol.spliceOwner, + "r" + readMethodSyms.size, + MethodType(List("in"))(_ => List(TypeRepr.of[JsonSource]), _ => TypeRepr.of[U]) + // (_ => List(input_params,...), _ => resultType) + ) + readMethodSyms.update(methodKey, sym) + readMethodDefs += DefDef( + sym, + params => { + val List(List(in)) = params + Some(f(in.asExprOf[JsonSource]).asTerm.changeOwner(sym)) + } + ) + } + ) + '{} + + val classFieldMatrixSyms = new scala.collection.mutable.HashMap[MethodKey, Symbol] + val classFieldMatrixValDefs = new scala.collection.mutable.ArrayBuffer[ValDef] + + def makeClassFieldMatrixValDef(methodKey: MethodKey, className: String, fieldNames: Array[String])(using Quotes): Expr[Unit] = + classFieldMatrixSyms.getOrElse( + methodKey, { + val sym = Symbol.newVal( + Symbol.spliceOwner, + s"__$className" + "_fields", + TypeRepr.of[StringMatrix], + Flags.EmptyFlags, + Symbol.noSymbol + ) + classFieldMatrixSyms.update(methodKey, sym) + val names = Expr(fieldNames) + classFieldMatrixValDefs += ValDef(sym, Some('{ new StringMatrix($names) }.asTerm)) + } + ) + '{} + + inline def changeFieldName(fr: FieldInfoRef): String = fr.annotations.get("co.blocke.scalajack.Change").flatMap(_.get("name")).getOrElse(fr.name) + + // --------------------------------------------------------------------------------------------- + + def testValidMapKey(testRef: RTypeRef[?]): Boolean = + val isValid = testRef match + case _: PrimitiveRef => true + case _: TimeRef => true + case _: NetRef => true + case c: ScalaClassRef[?] if c.isValueClass => true + case _: EnumRef[?] => true + case a: AliasRef[?] => testValidMapKey(a.unwrappedType) + case t: TraitRef[?] if t.childrenAreObject => true + case _ => false + if !isValid then throw new JsonTypeError(s"For JSON serialization, map keys must be a simple type. ${testRef.name} is too complex.") + isValid + + def maybeWrite[T](label: String, aE: Expr[T], ref: RTypeRef[T], out: Expr[JsonOutput], cfg: SJConfig): Expr[Unit] = + val labelE = Expr(label) + _maybeWrite[T]( + '{ $out.label($labelE) }, + aE, + ref, + out, + cfg + ) + + def maybeWriteMap[K, V](keyE: Expr[K], valueE: Expr[V], keyRef: RTypeRef[K], valueRef: RTypeRef[V], out: Expr[JsonOutput], cfg: SJConfig): Expr[Unit] = + keyRef.refType match + case '[k] => + _maybeWrite[V]( + '{ + $out.maybeComma() + ${ genWriteVal(keyE.asExprOf[k], keyRef.asInstanceOf[RTypeRef[k]], out, false, true) } + $out.colon() + }, + valueE, + valueRef, + out, + cfg + ) + + // Tests whether we should write something or not--mainly in the case of Option, or wrapped Option + // Affected types: Option, java.util.Optional, Left/Right, Try/Failure + // Returns Expr[Unit] containing either the original phrase (if ok to write) or the phrase + // prepended with the type-appropriate runtime check. This may seem like drama, but the idea + // is to avoid slowing runtime down with extra "if" checks unless they're absolutely needed. + def _maybeWrite[T](prefix: Expr[Unit], aE: Expr[T], ref: RTypeRef[T], out: Expr[JsonOutput], cfg: SJConfig): Expr[Unit] = + ref match + case t: ScalaOptionRef[?] if !cfg.noneAsNull => + t.optionParamType.refType match + case '[e] => + val tin = aE.asExprOf[Option[e]] + '{ + $tin match + case None => () + case Some(v) => ${ _maybeWrite[e](prefix, '{ v }.asExprOf[e], t.optionParamType.asInstanceOf[RTypeRef[e]], out, cfg) } + } + case t: JavaOptionalRef[?] if !cfg.noneAsNull => + t.optionParamType.refType match + case '[e] => + val tin = aE.asExprOf[java.util.Optional[e]] + '{ + if ! $tin.isEmpty then ${ _maybeWrite[e](prefix, '{ $tin.get }.asExprOf[e], t.optionParamType.asInstanceOf[RTypeRef[e]], out, cfg) } + } + case t: TryRef[?] => + t.tryRef.refType match + case '[e] => + val tin = aE.asExprOf[scala.util.Try[e]] + '{ + $tin match + case scala.util.Failure(v) => + ${ + cfg.tryFailureHandling match + case TryPolicy.AS_NULL => + '{ + $prefix + $out.burpNull() + } + case TryPolicy.ERR_MSG_STRING => + '{ + $prefix + $out.value("Try Failure with msg: " + v.getMessage()) + } + case TryPolicy.THROW_EXCEPTION => '{ throw v } + } + case _ => ${ _maybeWrite[e](prefix, '{ $tin.get }.asExprOf[e], t.tryRef.asInstanceOf[RTypeRef[e]], out, cfg) } + } + case t: LeftRightRef[?] if t.lrkind == LRKind.EITHER => + t.refType match + case '[u] => + val tin = aE.asExprOf[u] + t.rightRef.refType match + case '[rt] => + cfg.eitherLeftHandling match + case EitherLeftPolicy.AS_NULL => + '{ + if $tin == null then $out.burpNull() + $tin match + case Left(_) => + $prefix + $out.burpNull() + case Right(v) => ${ _maybeWrite[rt](prefix, '{ v.asInstanceOf[rt] }, t.rightRef.asInstanceOf[RTypeRef[rt]], out, cfg) } + } + case EitherLeftPolicy.ERR_MSG_STRING => + '{ + if $tin == null then $out.burpNull() + $tin match + case Left(err) => + $prefix + $out.value("Left Error: " + err.toString) + case Right(v) => ${ _maybeWrite[rt](prefix, '{ v.asInstanceOf[rt] }, t.rightRef.asInstanceOf[RTypeRef[rt]], out, cfg) } + } + case EitherLeftPolicy.THROW_EXCEPTION => + '{ + if $tin == null then $out.burpNull() + $tin match + case Left(err) => throw new JsonEitherLeftError("Left Error: " + err.toString) + case Right(v) => ${ _maybeWrite[rt](prefix, '{ v.asInstanceOf[rt] }, t.rightRef.asInstanceOf[RTypeRef[rt]], out, cfg) } + } + case EitherLeftPolicy.AS_VALUE => + t.leftRef.refType match + case '[lt] => + '{ + if $tin == null then $out.burpNull() + $tin match + case Left(v) => + ${ _maybeWrite[lt](prefix, '{ v.asInstanceOf[lt] }, t.leftRef.asInstanceOf[RTypeRef[lt]], out, cfg) } + case Right(v) => + ${ _maybeWrite[rt](prefix, '{ v.asInstanceOf[rt] }, t.rightRef.asInstanceOf[RTypeRef[rt]], out, cfg) } + } + case t: LeftRightRef[?] if t.lrkind != LRKind.EITHER => + t.refType match + case '[e] => + t.rightRef.refType match + case '[rt] => + t.leftRef.refType match + case '[lt] => + val tin = aE.asExprOf[e] + '{ + if $tin == null then $out.burpNull() + else + $out.mark() + scala.util.Try { + ${ _maybeWrite[rt](prefix, '{ $tin.asInstanceOf[rt] }, t.rightRef.asInstanceOf[RTypeRef[rt]], out, cfg) } + } match + case scala.util.Success(_) => () + case scala.util.Failure(f) => + $out.revert() + ${ _maybeWrite[lt](prefix, '{ $tin.asInstanceOf[lt] }, t.leftRef.asInstanceOf[RTypeRef[lt]], out, cfg) } + } + case t: AnyRef => + AnyWriter.isOkToWrite(prefix, aE, out, cfg) + case _ => + ref.refType match + case '[u] => + '{ + $prefix + ${ genWriteVal[u](aE.asExprOf[u], ref.asInstanceOf[RTypeRef[u]], out) } + } + + // --------------------------------------------------------------------------------------------- + + def genEncFnBody[T](r: RTypeRef[?], aE: Expr[T], out: Expr[JsonOutput], emitDiscriminator: Boolean = false, inTuple: Boolean = false, isMapKey: Boolean = false)(using Quotes): Expr[Unit] = + r.refType match + case '[b] => + r match + case t: ArrayRef[?] => + makeWriteFn[b](MethodKey(t, false), aE.asInstanceOf[Expr[b]], out) { (in, out) => + t.elementRef.refType match + case '[e] => + val tin = in.asInstanceOf[Expr[Array[e]]] + '{ + if $tin == null then $out.burpNull() + else + $out.startArray() + $tin.foreach { i => + ${ genWriteVal('{ i }, t.elementRef.asInstanceOf[RTypeRef[e]], out) } + } + $out.endArray() + } + } + + case t: IterableRef[?] => + makeWriteFn[b](MethodKey(t, false), aE.asInstanceOf[Expr[b]], out) { (in, out) => + t.elementRef.refType match + case '[e] => + val tin = in.asInstanceOf[Expr[Iterable[e]]] + '{ + if $tin == null then $out.burpNull() + else + $out.startArray() + $tin.foreach { i => + ${ genWriteVal('{ i }, t.elementRef.asInstanceOf[RTypeRef[e]], out) } + } + $out.endArray() + } + } + + case t: SeqRef[?] => + makeWriteFn[b](MethodKey(t, false), aE.asInstanceOf[Expr[b]], out) { (in, out) => + t.elementRef.refType match + case '[e] => + val tin = if t.isMutable then in.asExprOf[scala.collection.mutable.Seq[e]] else in.asExprOf[Seq[e]] + '{ + if $tin == null then $out.burpNull() + else + $out.startArray() + $tin.foreach { i => + ${ genWriteVal('{ i }, t.elementRef.asInstanceOf[RTypeRef[e]], out) } + } + $out.endArray() + } + } + + case t: SetRef[?] => + makeWriteFn[b](MethodKey(t, false), aE.asInstanceOf[Expr[b]], out) { (in, out) => + t.elementRef.refType match + case '[e] => + val tin = if t.isMutable then in.asExprOf[scala.collection.mutable.Set[e]] else in.asExprOf[Set[e]] + '{ + if $tin == null then $out.burpNull() + else + $out.startArray() + $tin.foreach { i => + ${ genWriteVal('{ i }.asExprOf[e], t.elementRef.asInstanceOf[RTypeRef[e]], out) } + } + $out.endArray() + } + } + + // case t: ScalaClassRef[?] if t.isSealed => // basically just like sealed trait... + case t: Sealable if t.isSealed => // basically just like sealed trait... + if t.childrenAreObject then + val tin = aE.asExprOf[b] + // case object -> just write the simple name of the object + '{ + if $tin == null then $out.burpNull() + else $out.value($tin.getClass.getName.split('.').last.stripSuffix("$")) + } + else + val cases = t.sealedChildren.map { child => + child.refType match + case '[c] => + val subtype = TypeRepr.of[c] + val sym = Symbol.newBind(Symbol.spliceOwner, "t", Flags.EmptyFlags, subtype) + CaseDef(Bind(sym, Typed(Wildcard(), Inferred(subtype))), None, genEncFnBody[c](child, Ref(sym).asExprOf[c], out, true).asTerm) + } :+ CaseDef(Literal(NullConstant()), None, '{ $out.burpNull() }.asTerm) + val matchExpr = Match(aE.asTerm, cases).asExprOf[Unit] + matchExpr + + // We don't use makeWriteFn here because a value class is basically just a "box" around a simple type + case t: ScalaClassRef[?] if t.isValueClass => + val theField = t.fields.head.fieldRef + theField.refType match + case '[e] => + val fieldValue = Select.unique(aE.asTerm, t.fields.head.name).asExprOf[e] + genWriteVal(fieldValue, theField.asInstanceOf[RTypeRef[e]], out, isMapKey = isMapKey) + + case t: ScalaClassRef[?] => + makeWriteFn[b](MethodKey(t, false), aE.asInstanceOf[Expr[b]], out) { (in, out) => + val body = { + val eachField = t.fields.map { f => + f.fieldRef.refType match + case '[z] => + val fieldValue = Select.unique(in.asTerm, f.name).asExprOf[z] + val fieldName = changeFieldName(f) + maybeWrite[z](fieldName, fieldValue, f.fieldRef.asInstanceOf[RTypeRef[z]], out, cfg) + } + if emitDiscriminator then + val cname = cfg.typeHintPolicy match + case TypeHintPolicy.SIMPLE_CLASSNAME => Expr(lastPart(t.name)) + case TypeHintPolicy.SCRAMBLE_CLASSNAME => '{ scramble(${ Expr(lastPart(t.name).hashCode) }) } + case TypeHintPolicy.USE_ANNOTATION => + Expr(t.annotations.get("co.blocke.scalajack.TypeHint").flatMap(_.get("hintValue")).getOrElse(lastPart(t.name))) + val withDisc = '{ + $out.label(${ Expr(cfg.typeHintLabel) }) + $out.value($cname) + } +: eachField + Expr.block(withDisc.init, withDisc.last) + else if eachField.length == 1 then eachField.head + else Expr.block(eachField.init, eachField.last) + } + + if !t.isCaseClass && cfg._writeNonConstructorFields then + val eachField = t.nonConstructorFields.map { f => + f.fieldRef.refType match + case '[e] => + val fieldValue = Select.unique(in.asTerm, f.getterLabel).asExprOf[e] + val fieldName = changeFieldName(f) + maybeWrite[e](fieldName, fieldValue, f.fieldRef.asInstanceOf[RTypeRef[e]], out, cfg) + } + val subBody = eachField.length match + case 0 => '{} + case 1 => eachField.head + case _ => Expr.block(eachField.init, eachField.last) + '{ + if $in == null then $out.burpNull() + else + $out.startObject() + $body + $subBody + $out.endObject() + } + else + '{ + if $in == null then $out.burpNull() + else + $out.startObject() + $body + $out.endObject() + } + } + + case t: MapRef[?] => + makeWriteFn[b](MethodKey(t, false), aE.asInstanceOf[Expr[b]], out) { (in, out) => + t.elementRef.refType match + case '[k] => + t.elementRef2.refType match + case '[v] => + val tin = if t.isMutable then in.asExprOf[scala.collection.mutable.Map[k, v]] else in.asExprOf[Map[k, v]] + '{ + if $tin == null then $out.burpNull() + else + $out.startObject() + $tin.foreach { case (key, value) => + ${ + (t.elementRef, t.elementRef2) match + case (aliasK: AliasRef[?], aliasV: AliasRef[?]) => + aliasK.unwrappedType.refType match + case '[ak] => + aliasV.unwrappedType.refType match + case '[av] => + testValidMapKey(aliasK.unwrappedType) + maybeWriteMap[ak, av]( + '{ key.asInstanceOf[ak] }, + '{ value.asInstanceOf[av] }, + aliasK.unwrappedType.asInstanceOf[RTypeRef[ak]], + aliasV.unwrappedType.asInstanceOf[RTypeRef[av]], + out, + cfg + ) + case (_, aliasV: AliasRef[?]) => + aliasV.unwrappedType.refType match + case '[av] => + testValidMapKey(t.elementRef) + maybeWriteMap[k, av]( + '{ key }.asExprOf[k], + '{ value.asInstanceOf[av] }, + t.elementRef.asInstanceOf[RTypeRef[k]], + aliasV.unwrappedType.asInstanceOf[RTypeRef[av]], + out, + cfg + ) + case (aliasK: AliasRef[?], _) => + aliasK.unwrappedType.refType match + case '[ak] => + testValidMapKey(aliasK.unwrappedType) + maybeWriteMap[ak, v]( + '{ key.asInstanceOf[ak] }, + '{ value }.asExprOf[v], + aliasK.unwrappedType.asInstanceOf[RTypeRef[ak]], + t.elementRef2.asInstanceOf[RTypeRef[v]], + out, + cfg + ) + case (_, _) => + testValidMapKey(t.elementRef) + maybeWriteMap[k, v]('{ key }, '{ value }.asExprOf[v], t.elementRef.asInstanceOf[RTypeRef[k]], t.elementRef2.asInstanceOf[RTypeRef[v]], out, cfg) + } + } + $out.endObject() + } + } + + case t: JavaCollectionRef[?] => + makeWriteFn[b](MethodKey(t, false), aE.asInstanceOf[Expr[b]], out) { (in, out) => + t.elementRef.refType match + case '[e] => + val tin = '{ $in.asInstanceOf[java.util.Collection[?]] } + '{ + if $tin == null then $out.burpNull() + else + $out.startArray() + $tin.toArray.foreach { elem => + ${ genWriteVal('{ elem.asInstanceOf[e] }, t.elementRef.asInstanceOf[RTypeRef[e]], out) } + } + $out.endArray() + } + } + + case t: JavaMapRef[?] => + makeWriteFn[b](MethodKey(t, false), aE.asInstanceOf[Expr[b]], out) { (in, out) => + t.elementRef.refType match + case '[k] => + t.elementRef2.refType match + case '[v] => + val tin = in.asExprOf[java.util.Map[k, v]] + '{ + if $tin == null then $out.burpNull() + else + $out.startObject() + $tin.asScala.foreach { case (key, value) => + ${ + maybeWriteMap[k, v]('{ key }, '{ value }.asExprOf[v], t.elementRef.asInstanceOf[RTypeRef[k]], t.elementRef2.asInstanceOf[RTypeRef[v]], out, cfg) + } + } + $out.endObject() + } + } + + case t: JavaClassRef[?] => + makeWriteFn[b](MethodKey(t, false), aE.asInstanceOf[Expr[b]], out) { (in, out) => + t.refType match + case '[p] => + val rtype = t.expr.asExprOf[JavaClassRType[p]] + val tin = in.asExprOf[b] + var fieldRefs = t.fields.asInstanceOf[List[NonConstructorFieldInfoRef]] + val sref = ReflectOnType[String](q)(TypeRepr.of[String])(using scala.collection.mutable.Map.empty[TypedName, Boolean]) + '{ + if $tin == null then $out.burpNull() + else + $out.startObject() + val rt = $rtype + rt.fields.foreach { f => + val field = f.asInstanceOf[NonConstructorFieldInfo] + val m = $tin.getClass.getMethod(field.getterLabel) + m.setAccessible(true) + val fieldValue = m.invoke($tin) + val fieldName = field.annotations.get("co.blocke.scalajack.Change").flatMap(_.get("name")).getOrElse(f.name) + ${ + val ref = fieldRefs.head + fieldRefs = fieldRefs.tail + ref.fieldRef.refType match + case '[e] => + maybeWriteMap[String, e]('{ fieldName }, '{ fieldValue.asInstanceOf[e] }, sref, ref.fieldRef.asInstanceOf[RTypeRef[e]], out, cfg) + } + } + $out.endObject() + } + } + + case t: TraitRef[?] => throw JsonUnsupportedType("Non-sealed traits are not supported") + + // No makeWriteFn here--Option is just a wrapper to the real thingy + case t: ScalaOptionRef[?] => + t.optionParamType.refType match + case '[e] => + val tin = aE.asExprOf[b] + '{ + $tin match + case null => $out.burpNull() + case None => + ${ + if cfg.noneAsNull || inTuple then '{ $out.burpNull() } + else '{ () } + } + case Some(v) => + val vv = v.asInstanceOf[e] + ${ genWriteVal[e]('{ vv }, t.optionParamType.asInstanceOf[RTypeRef[e]], out) } + } + case t: JavaOptionalRef[?] => + t.optionParamType.refType match + case '[e] => + val tin = aE.asExprOf[b] + '{ + $tin.asInstanceOf[java.util.Optional[e]] match + case null => $out.burpNull() + case o if o.isEmpty => + ${ + if cfg.noneAsNull || inTuple then '{ $out.burpNull() } + else '{ () } + } + case o => + val vv = o.get().asInstanceOf[e] + ${ genWriteVal[e]('{ vv }, t.optionParamType.asInstanceOf[RTypeRef[e]], out) } + } + + // No makeWriteFn here... SelfRef is referring to something we've already seen before. There absolutely should already be a geneated + // and cached function for this thing that we can call. + case t: SelfRefRef[?] => + t.refType match + case '[e] => + val ref = ReflectOnType[e](q)(TypeRepr.of[e])(using scala.collection.mutable.Map.empty[TypedName, Boolean]) + val key = MethodKey(ref, false) + val sym = writeMethodSyms(key) + val tin = aE.asExprOf[b] + '{ + if $tin == null then $out.burpNull() + else ${ Ref(sym).appliedTo(tin.asTerm, out.asTerm).asExprOf[Unit] } + } + + // No makeWriteFn here. All LeftRight types (Either, Union, Intersection) are just type wrappers + case t: LeftRightRef[?] => + val tin = aE.asExprOf[b] + t.leftRef.refType match + case '[lt] => + t.rightRef.refType match + case '[rt] => + // This is a close parallel with maybeWrite handling of Either. If the Either is a field in a class or + // Map, the maybeWrite logic applies--because we need to not write both the Either value AND the field label. + // If the Either is part of a tuple, Seq, etc., then this logic applies. + if t.lrkind == LRKind.EITHER then + cfg.eitherLeftHandling match + case EitherLeftPolicy.AS_VALUE => + '{ + if $tin == null then $out.burpNull() + else + $tin match + case Left(v) => + ${ genWriteVal[lt]('{ v.asInstanceOf[lt] }, t.leftRef.asInstanceOf[RTypeRef[lt]], out, inTuple = inTuple) } + case Right(v) => + ${ genWriteVal[rt]('{ v.asInstanceOf[rt] }, t.rightRef.asInstanceOf[RTypeRef[rt]], out, inTuple = inTuple) } + } + case EitherLeftPolicy.AS_NULL => + '{ + if $tin == null then $out.burpNull() + else + $tin match + case Left(v) => $out.burpNull() + case Right(v) => + ${ genWriteVal[rt]('{ v.asInstanceOf[rt] }, t.rightRef.asInstanceOf[RTypeRef[rt]], out, inTuple = inTuple) } + } + case EitherLeftPolicy.ERR_MSG_STRING => + '{ + if $tin == null then $out.burpNull() + else + $tin match + case Left(v) => $out.value("Left Error: " + v.toString) + case Right(v) => + ${ genWriteVal[rt]('{ v.asInstanceOf[rt] }, t.rightRef.asInstanceOf[RTypeRef[rt]], out, inTuple = inTuple) } + } + case EitherLeftPolicy.THROW_EXCEPTION => + '{ + if $tin == null then $out.burpNull() + else + $tin match + case Left(v) => throw new JsonEitherLeftError("Left Error: " + v.toString) + case Right(v) => + ${ genWriteVal[rt]('{ v.asInstanceOf[rt] }, t.rightRef.asInstanceOf[RTypeRef[rt]], out, inTuple = inTuple) } + } + else + '{ + $out.mark() + scala.util.Try { + ${ genWriteVal[rt]('{ $tin.asInstanceOf[rt] }, t.rightRef.asInstanceOf[RTypeRef[rt]], out, inTuple = inTuple) } + } match + case scala.util.Success(_) => () // do nothing further--write to out already happened + case scala.util.Failure(_) => + $out.revert() + ${ genWriteVal[lt]('{ $tin.asInstanceOf[lt] }, t.leftRef.asInstanceOf[RTypeRef[lt]], out, inTuple = inTuple) } + } + + // No makeWriteFn here. Try is just a wrapper + case t: TryRef[?] => + t.tryRef.refType match + case '[e] => + val tin = aE.asExprOf[scala.util.Try[e]] + '{ + if $tin == null then $out.burpNull() + else + $tin match + case scala.util.Success(v) => + ${ genWriteVal[e]('{ v }.asInstanceOf[Expr[e]], t.tryRef.asInstanceOf[RTypeRef[e]], out, inTuple = inTuple) } + case scala.util.Failure(v) => + ${ + cfg.tryFailureHandling match + case _ if inTuple => '{ $out.burpNull() } + case TryPolicy.AS_NULL => '{ $out.burpNull() } + case TryPolicy.ERR_MSG_STRING => '{ $out.value("Try Failure with msg: " + v.getMessage()) } + case TryPolicy.THROW_EXCEPTION => '{ throw v } + } + } + + case t: TupleRef[?] => + makeWriteFn[b](MethodKey(t, false), aE.asInstanceOf[Expr[b]], out) { (in, out) => + '{ + if $in == null then $out.burpNull() + else + $out.startArray() + ${ + // Note: Don't use maybeWrite here... Tuples are fixed-length. We need to write + // something for every position, so write null for None or other "bad" values + val elementsE = t.tupleRefs.zipWithIndex.map { case (ref, i) => + ref.refType match + case '[e] => + val fieldValue = Select.unique(in.asTerm, "_" + (i + 1)).asExprOf[e] + genWriteVal[e](fieldValue, ref.asInstanceOf[RTypeRef[e]], out, inTuple = true) + } + if elementsE.size == 1 then elementsE.head + else Expr.block(elementsE.init, elementsE.last) + } + $out.endArray() + } + } + + case t => throw new JsonUnsupportedType("Type represented by " + t.name + " is unsupported for JSON writes") + + // --------------------------------------------------------------------------------------------- + + def genWriteVal[T: Type]( + aE: Expr[T], + ref: RTypeRef[T], + out: Expr[JsonOutput], + inTuple: Boolean = false, + isMapKey: Boolean = false // only primitive or primitive-equiv types can be Map keys + )(using Quotes): Expr[Unit] = + val methodKey = MethodKey(ref, false) + writeMethodSyms + .get(methodKey) + .map { sym => // hit cache first... then match on Ref type + Apply(Ref(sym), List(aE.asTerm, out.asTerm)).asExprOf[Unit] + } + .getOrElse( + ref match + // First cover all primitive and simple types... + case t: BigDecimalRef => + if isMapKey then '{ $out.valueStringified(${ aE.asExprOf[scala.math.BigDecimal] }) } + else '{ $out.value(${ aE.asExprOf[scala.math.BigDecimal] }) } + case t: BigIntRef => + if isMapKey then '{ $out.valueStringified(${ aE.asExprOf[scala.math.BigInt] }) } + else '{ $out.value(${ aE.asExprOf[scala.math.BigInt] }) } + case t: BooleanRef => + if isMapKey then '{ $out.valueStringified(${ aE.asExprOf[Boolean] }) } + else '{ $out.value(${ aE.asExprOf[Boolean] }) } + case t: ByteRef => + if isMapKey then '{ $out.valueStringified(${ aE.asExprOf[Byte] }) } + else '{ $out.value(${ aE.asExprOf[Byte] }) } + case t: CharRef => + '{ $out.value(${ aE.asExprOf[Char] }) } + case t: DoubleRef => + if isMapKey then '{ $out.valueStringified(${ aE.asExprOf[Double] }) } + else '{ $out.value(${ aE.asExprOf[Double] }) } + case t: FloatRef => + if isMapKey then '{ $out.valueStringified(${ aE.asExprOf[Float] }) } + else '{ $out.value(${ aE.asExprOf[Float] }) } + case t: IntRef => + if isMapKey then '{ $out.valueStringified(${ aE.asExprOf[Int] }) } + else '{ $out.value(${ aE.asExprOf[Int] }) } + case t: LongRef => + if isMapKey then '{ $out.valueStringified(${ aE.asExprOf[Long] }) } + else '{ $out.value(${ aE.asExprOf[Long] }) } + case t: ShortRef => + if isMapKey then '{ $out.valueStringified(${ aE.asExprOf[Short] }) } + else '{ $out.value(${ aE.asExprOf[Short] }) } + + case t: StringRef => + if cfg._suppressEscapedStrings then '{ $out.value(${ aE.asExprOf[String] }) } + else '{ $out.valueEscaped(${ aE.asExprOf[String] }) } + + case t: JBigDecimalRef => + if isMapKey then '{ $out.valueStringified(${ aE.asExprOf[java.math.BigDecimal] }) } + else '{ $out.value(${ aE.asExprOf[java.math.BigDecimal] }) } + case t: JBigIntegerRef => + if isMapKey then '{ $out.valueStringified(${ aE.asExprOf[java.math.BigInteger] }) } + else '{ $out.value(${ aE.asExprOf[java.math.BigInteger] }) } + case t: JBooleanRef => + if isMapKey then '{ $out.valueStringified(${ aE.asExprOf[java.lang.Boolean] }) } + else '{ $out.value(${ aE.asExprOf[java.lang.Boolean] }) } + case t: JByteRef => + if isMapKey then '{ $out.valueStringified(${ aE.asExprOf[java.lang.Byte] }) } + else '{ $out.value(${ aE.asExprOf[java.lang.Byte] }) } + case t: JCharacterRef => + '{ $out.value(${ aE.asExprOf[java.lang.Character] }) } + case t: JDoubleRef => + if isMapKey then '{ $out.valueStringified(${ aE.asExprOf[java.lang.Double] }) } + else '{ $out.value(${ aE.asExprOf[java.lang.Double] }) } + case t: JFloatRef => + if isMapKey then '{ $out.valueStringified(${ aE.asExprOf[java.lang.Float] }) } + else '{ $out.value(${ aE.asExprOf[java.lang.Float] }) } + case t: JIntegerRef => + if isMapKey then '{ $out.valueStringified(${ aE.asExprOf[java.lang.Integer] }) } + else '{ $out.value(${ aE.asExprOf[java.lang.Integer] }) } + case t: JLongRef => + if isMapKey then '{ $out.valueStringified(${ aE.asExprOf[java.lang.Long] }) } + else '{ $out.value(${ aE.asExprOf[java.lang.Long] }) } + case t: JShortRef => + if isMapKey then '{ $out.valueStringified(${ aE.asExprOf[java.lang.Short] }) } + else '{ $out.value(${ aE.asExprOf[java.lang.Short] }) } + case t: JNumberRef => + if isMapKey then '{ $out.valueStringified(${ aE.asExprOf[java.lang.Number] }) } + else '{ $out.value(${ aE.asExprOf[java.lang.Number] }) } + + case t: DurationRef => '{ $out.value(${ aE.asExprOf[java.time.Duration] }) } + case t: InstantRef => '{ $out.value(${ aE.asExprOf[java.time.Instant] }) } + case t: LocalDateRef => '{ $out.value(${ aE.asExprOf[java.time.LocalDate] }) } + case t: LocalDateTimeRef => '{ $out.value(${ aE.asExprOf[java.time.LocalDateTime] }) } + case t: LocalTimeRef => '{ $out.value(${ aE.asExprOf[java.time.LocalTime] }) } + case t: MonthDayRef => '{ $out.value(${ aE.asExprOf[java.time.MonthDay] }) } + case t: OffsetDateTimeRef => '{ $out.value(${ aE.asExprOf[java.time.OffsetDateTime] }) } + case t: OffsetTimeRef => '{ $out.value(${ aE.asExprOf[java.time.OffsetTime] }) } + case t: PeriodRef => '{ $out.value(${ aE.asExprOf[java.time.Period] }) } + case t: YearRef => '{ $out.value(${ aE.asExprOf[java.time.Year] }) } + case t: YearMonthRef => '{ $out.value(${ aE.asExprOf[java.time.YearMonth] }) } + case t: ZonedDateTimeRef => '{ $out.value(${ aE.asExprOf[java.time.ZonedDateTime] }) } + case t: ZoneIdRef => '{ $out.value(${ aE.asExprOf[java.time.ZoneId] }) } + case t: ZoneOffsetRef => '{ $out.value(${ aE.asExprOf[java.time.ZoneOffset] }) } + + case t: URLRef => '{ $out.value(${ aE.asExprOf[java.net.URL] }) } + case t: URIRef => '{ $out.value(${ aE.asExprOf[java.net.URI] }) } + case t: UUIDRef => '{ $out.value(${ aE.asExprOf[java.util.UUID] }) } + case t: ObjectRef[?] => '{ $out.value(${ Expr(t.name) }) } + + case t: AliasRef[?] => + t.unwrappedType.refType match + case '[e] => + genWriteVal[e](aE.asInstanceOf[Expr[e]], t.unwrappedType.asInstanceOf[RTypeRef[e]], out, inTuple = inTuple) + + // These are here becaue Enums and their various flavors can be Map keys + // (EnumRef handles: Scala 3 enum, Scala 2 Enumeration, Java Enumeration) + case t: EnumRef[?] => + val enumAsId = cfg.enumsAsIds match + case List("-") => false + case Nil => true + case list if list.contains(t.name) => true + case _ => false + val rtype = t.expr + if enumAsId then + if isMapKey then '{ $out.value($rtype.asInstanceOf[EnumRType[?]].ordinal($aE.toString).get.toString) } // stringified id + else '{ $out.value($rtype.asInstanceOf[EnumRType[?]].ordinal($aE.toString).get) } // int value of id + else '{ if $aE == null then $out.burpNull() else $out.value($aE.toString) } + + // NeoType is a bit of a puzzle-box. To get the correct underlying base type, I had to dig into + // the argument of method validate$retainedBody. It happened to have the correctly-typed parameter. + // With the correct type, we can correct write out the value. + case t: NeoTypeRef[?] => // in Quotes context + Symbol.requiredModule(t.typedName.toString).methodMember("validate$retainedBody").head.paramSymss.head.head.tree match + case ValDef(_, tt, _) => + tt.tpe.asType match + case '[u] => + val baseTypeRef = ReflectOnType.apply(q)(tt.tpe)(using scala.collection.mutable.Map.empty[TypedName, Boolean]) + genWriteVal[u]('{ $aE.asInstanceOf[u] }, baseTypeRef.asInstanceOf[RTypeRef[u]], out) + + case t: AnyRef => '{ AnyWriter.writeAny($aE, $out, ${ Expr(cfg) }) } + + // Everything else... + // case _ if isStringified => throw new JsonIllegalKeyType("Non-primitive/non-simple types cannot be map keys") + case _ => genEncFnBody(ref, aE, out, inTuple = inTuple, isMapKey = isMapKey) + ) + + // --------------------------------------------------------------------------------------------- + + def lrHasOptionChild(lr: LeftRightRef[?]): (String, Language) = + lr.rightRef match + case t: ScalaOptionRef[?] => ("r", Language.Scala) + case t: JavaOptionalRef[?] => ("r", Language.Java) + case t: LeftRightRef[?] => + val (recipe, lang) = lrHasOptionChild(t) + ("r" + recipe, lang) + case _ => + lr.leftRef match + case t: ScalaOptionRef[?] => ("l", Language.Scala) + case t: JavaOptionalRef[?] => ("l", Language.Java) + case t: LeftRightRef[?] => + val (recipe, lang) = lrHasOptionChild(t) + ("l" + recipe, lang) + case _ => ("", Language.Scala) + + def tryHasOptionChild(t: TryRef[?]): (Boolean, Language) = + t.tryRef match + case o: ScalaOptionRef[?] => (true, Language.Scala) + case o: JavaOptionalRef[?] => (true, Language.Java) + case lr: LeftRightRef[?] => + val (recipe, lang) = lrHasOptionChild(lr) + (recipe.length > 0, lang) + case _ => (false, Language.Scala) + + def genDecFnBody[T: Type](r: RTypeRef[?], in: Expr[JsonSource])(using Quotes): Expr[Unit] = + + def typeArgs(tpe: TypeRepr): List[TypeRepr] = tpe match + case AppliedType(_, typeArgs) => typeArgs.map(_.dealias) + case _ => Nil + + r.refType match // refType is Type[r.R] + case '[b] => + r match + + case t: AnyRef => + makeReadFn[T](MethodKey(t, false), in)(in => + '{ + if $in.expectNull() then null + else + $in.readToken() match + case '[' => + $in.backspace() + val parsedArray = $in.expectArray[Any](() => ${ genReadVal[Any](AnyRef(), in, false) }) + if parsedArray != null then parsedArray.toList + else null + case '{' => + $in.parseMap[String, Any]( + () => ${ genReadVal[String](StringRef(), in, false, true) }, + () => ${ genReadVal[Any](AnyRef(), in, false) }, + Map.empty[String, Any], + true + ) + case 't' | 'f' => + $in.backspace() + $in.expectBoolean() + case n if n == '-' | n == '+' | n == '.' | (n >= '0' && n <= '9') => + $in.backspace() + $in.expectNumberOrNull() match + case null => null + case s => + scala.math.BigDecimal(s) match { + case i if i.isValidInt => i.toIntExact + case i if i.isValidLong => i.toLongExact + case d if d.isDecimalDouble => d.toDouble + case d if d.ulp == 1 => d.toBigInt + case d => d + } + case '\"' => + $in.backspace() + $in.expectString() + case _ => throw new JsonParseError("Illegal JSON char while parsing Any value", $in) + }.asExprOf[T] + ) + + case t: Sealable if t.isSealed && !t.childrenAreObject => + t.sealedChildren.map(kid => + kid.refType match + case '[c] => + genDecFnBody[c](kid.asInstanceOf[RTypeRef[c]], in) + ) + makeReadFn[T](MethodKey(t, false), in)(in => + val hintLabelE = Expr(cfg.typeHintLabel) + val classPrefixE = Expr(allButLastPart(t.name)) + val caseDefs = t.sealedChildren.map { childRef => + val childNameE = Expr(childRef.name) + cfg.typeHintPolicy match + case TypeHintPolicy.SCRAMBLE_CLASSNAME => + val sym = Symbol.newBind(Symbol.spliceOwner, "t", Flags.EmptyFlags, TypeRepr.of[String]) + CaseDef( + Bind(sym, Typed(Wildcard(), Inferred(TypeRepr.of[String]))), + Some({ + val tE = Ref(sym).asExprOf[String] + '{ descramble($tE, lastPart($childNameE).hashCode) }.asTerm + }), { + val methodKey = MethodKey(childRef, false) + readMethodSyms + .get(methodKey) + .map { sym => + Apply(Ref(sym), List(in.asTerm)).asExprOf[T] + } + .get + .asTerm + } + ) + case TypeHintPolicy.USE_ANNOTATION => + val sym = Symbol.newBind(Symbol.spliceOwner, "t", Flags.EmptyFlags, TypeRepr.of[String]) + val annoOrName = + childRef match + case cr: ClassRef[?] => cr.annotations.get("co.blocke.scalajack.TypeHint").flatMap(_.get("hintValue")).getOrElse(lastPart(cr.name)) + case _ => lastPart(childRef.name) + CaseDef( + Literal(StringConstant(annoOrName)), + None, { + val methodKey = MethodKey(childRef, false) + readMethodSyms + .get(methodKey) + .map { sym => + Apply(Ref(sym), List(in.asTerm)).asExprOf[T] + } + .get + .asTerm + } + ) + case TypeHintPolicy.SIMPLE_CLASSNAME => + CaseDef( + Literal(StringConstant(childRef.name)), + None, { + val methodKey = MethodKey(childRef, false) + readMethodSyms + .get(methodKey) + .map { sym => + Apply(Ref(sym), List(in.asTerm)).asExprOf[T] + } + .get + .asTerm + } + ) + } + '{ + if $in.expectNull() then null + else + val hint = $in.findObjectField($hintLabelE).getOrElse(throw JsonParseError(s"Unable to find type hint for abstract class $$cnameE", $in)) + ${ + cfg.typeHintPolicy match + case TypeHintPolicy.SIMPLE_CLASSNAME => Match('{ $classPrefixE + "." + hint }.asTerm, caseDefs).asExprOf[T] + case TypeHintPolicy.SCRAMBLE_CLASSNAME => Match('{ hint }.asTerm, caseDefs).asExprOf[T] + case TypeHintPolicy.USE_ANNOTATION => Match('{ hint }.asTerm, caseDefs).asExprOf[T] + } + }.asExprOf[T] + ) + + case t: Sealable if t.isSealed && t.childrenAreObject => // case objects + makeReadFn[T](MethodKey(t, false), in)(in => + val classPrefixE = Expr(allButLastPart(t.name)) + val caseDefs = t.sealedChildren.map { childRef => + val nameE = Expr(childRef.name) + childRef.refType match + case '[o] => + CaseDef( + Literal(StringConstant(childRef.name)), + None, + Ref(TypeRepr.of[o].typeSymbol).asExprOf[o].asTerm + ) + } + '{ + if $in.expectNull() then null + else ${ Match('{ $classPrefixE + "." + $in.expectString() }.asTerm, caseDefs).asExprOf[T] } + }.asExprOf[T] + ) + + case t: TraitRef[?] => + throw JsonUnsupportedType("Non-sealed traits are not supported") + + case t: ScalaClassRef[?] => + makeReadFn[T](MethodKey(t, false), in)(in => + // Generate vars for each contractor argument, populated with either a "unit" value (eg 0, "") or given default value + val tpe = TypeRepr.of[b] + val classCompanion = tpe.typeSymbol.companionClass + val companionModule = tpe.typeSymbol.companionModule + val totalRequired = math.pow(2, t.fields.length).toInt - 1 + var required = 0 + val reqSym = Symbol.newVal(Symbol.spliceOwner, "required", TypeRepr.of[Int], Flags.Mutable, Symbol.noSymbol) + val allFieldNames = Expr(t.fields.map(f => changeFieldName(f)).toArray) // Used for missing required field error + + val together = t.fields.map { oneField => + oneField.fieldRef.refType match { + case '[f] => + val dvMembers = classCompanion.methodMember("$lessinit$greater$default$" + (oneField.index + 1)) + val sym = Symbol.newVal(Symbol.spliceOwner, "_" + oneField.name, TypeRepr.of[f], Flags.Mutable, Symbol.noSymbol) + val fieldSymRef = Ident(sym.termRef) + val reqBit = Expr(math.pow(2, oneField.index).toInt) + val fieldName = Expr(oneField.name) + + val caseDef = CaseDef( + Literal(IntConstant(oneField.index)), + None, + '{ + if (${ Ref(reqSym).asExprOf[Int] } & $reqBit) != 0 then + ${ Assign(Ref(reqSym), '{ ${ Ref(reqSym).asExprOf[Int] } ^ $reqBit }.asTerm).asExprOf[Unit] } + ${ Assign(fieldSymRef, genReadVal[f](oneField.fieldRef.asInstanceOf[RTypeRef[f]], in).asTerm).asExprOf[Unit] } + else throw new JsonParseError("Duplicate field " + $fieldName, $in) + }.asTerm + ) + if dvMembers.isEmpty then + // no default... required? Not if Option/Optional, or a collection + val unitVal = oneField.fieldRef match { + case _: OptionRef[?] => + oneField.fieldRef.unitVal.asTerm // not required + case _: AnyRef => + oneField.fieldRef.unitVal.asTerm // not required + case r: LeftRightRef[?] if r.lrkind == LRKind.EITHER => // maybe required + val (optionRecipe, lang) = lrHasOptionChild(r) + if optionRecipe.length == 0 then + required = required | math.pow(2, oneField.index).toInt // required + oneField.fieldRef.unitVal.asTerm + else + val recipeE = Expr(optionRecipe) + if lang == Language.Scala then + '{ + $recipeE.foldRight(None: Any)((c, acc) => if c == 'r' then Right(acc) else Left(acc)).asInstanceOf[f] + }.asTerm + else + '{ + $recipeE.foldRight(java.util.Optional.empty: Any)((c, acc) => if c == 'r' then Right(acc) else Left(acc)).asInstanceOf[f] + }.asTerm + case r: LeftRightRef[?] => // maybe required + val (optionRecipe, lang) = lrHasOptionChild(r) + if optionRecipe.length == 0 then // no Option children -> required + required = required | math.pow(2, oneField.index).toInt // required + oneField.fieldRef.unitVal.asTerm + else // at least one Option child -> optional + if lang == Language.Scala then '{ None }.asTerm + else '{ java.util.Optional.empty.asInstanceOf[f] }.asTerm + case y: TryRef[?] => + tryHasOptionChild(y) match + case (true, Language.Scala) => '{ Success(None) }.asTerm + case (true, Language.Java) => '{ Success(java.util.Optional.empty).asInstanceOf[f] }.asTerm + case _ => + required = required | math.pow(2, oneField.index).toInt // required + oneField.fieldRef.unitVal.asTerm + case _ => + required = required | math.pow(2, oneField.index).toInt // required + oneField.fieldRef.unitVal.asTerm + } + (ValDef(sym, Some(unitVal)), caseDef, fieldSymRef) + else + val methodSymbol = dvMembers.head + val dvSelectNoTArgs = Ref(companionModule).select(methodSymbol) + val dvSelect = methodSymbol.paramSymss match + case Nil => dvSelectNoTArgs + case List(params) if (params.exists(_.isTypeParam)) => + typeArgs(tpe) match + case Nil => ??? // throw JsonParseError("Expected an applied type", ???) + case typeArgs => TypeApply(dvSelectNoTArgs, typeArgs.map(Inferred(_))) + case _ => ??? // fail(s"Default method for ${symbol.name} of class ${tpe.show} have a complex " + + (ValDef(sym, Some(dvSelect)), caseDef, fieldSymRef) + } + } + val reqVarDef = ValDef(reqSym, Some(Literal(IntConstant(totalRequired)))) + val (varDefs, caseDefs, idents) = together.unzip3 + val caseDefsWithFinal = caseDefs :+ CaseDef(Wildcard(), None, '{ $in.skipValue() }.asTerm) // skip values of unrecognized fields + + val argss = List(idents) + val primaryConstructor = tpe.classSymbol.get.primaryConstructor + val constructorNoTypes = Select(New(Inferred(tpe)), primaryConstructor) + val constructor = typeArgs(tpe) match + case Nil => constructorNoTypes + case typeArgs => TypeApply(constructorNoTypes, typeArgs.map(Inferred(_))) + val instantiateClass = argss.tail.foldLeft(Apply(constructor, argss.head))((acc, args) => Apply(acc, args)) + + val exprRequired = Expr(required) + + makeClassFieldMatrixValDef(MethodKey(t, false), t.name.replaceAll("\\.", "_"), t.fields.map(f => changeFieldName(f)).toArray) + val fieldMatrixSym = classFieldMatrixSyms(MethodKey(t, false)).asInstanceOf[Symbol] + + var finalVarDefs = varDefs + val parseLoop = + if !cfg._writeNonConstructorFields || t.nonConstructorFields.isEmpty then + // When we don't care about non-constructor fields + '{ + var maybeFieldNum = $in.expectFirstObjectField(${ Ref(fieldMatrixSym).asExprOf[StringMatrix] }) + if maybeFieldNum == null then null.asInstanceOf[T] + else + while maybeFieldNum.isDefined do + ${ Match('{ maybeFieldNum.get }.asTerm, caseDefsWithFinal).asExprOf[Any] } + maybeFieldNum = $in.expectObjectField(${ Ref(fieldMatrixSym).asExprOf[StringMatrix] }) + + if (${ Ref(reqSym).asExprOf[Int] } & ${ exprRequired }) == 0 then ${ instantiateClass.asExprOf[T] } + else throw new JsonParseError("Missing required field(s) " + ${ allFieldNames }(Integer.numberOfTrailingZeros(${ Ref(reqSym).asExprOf[Int] } & ${ exprRequired })), $in) + }.asTerm + else + val instanceSym = Symbol.newVal(Symbol.spliceOwner, "_instance", TypeRepr.of[T], Flags.Mutable, Symbol.noSymbol) + finalVarDefs = finalVarDefs :+ ValDef(instanceSym, Some('{ null }.asTerm)) // add var _instance=null to gen'ed code + val instanceSymRef = Ident(instanceSym.termRef) + // When we do care about non-constructor fields + makeClassFieldMatrixValDef(MethodKey(t, true), t.name.replaceAll("\\.", "_"), t.nonConstructorFields.sortBy(_.index).map(f => changeFieldName(f)).toArray) + val fieldMatrixSymNCF = classFieldMatrixSyms(MethodKey(t, true)).asInstanceOf[Symbol] + // New Case/Match for non-constructor fields + val caseDefsWithFinalNC = t.nonConstructorFields.map(ncf => + ncf.fieldRef.refType match + case '[u] => + CaseDef( + Literal(IntConstant(ncf.index)), + None, + // Call the setter for this field here... + Apply(Select.unique(Ref(instanceSym), ncf.setterLabel), List(genReadVal[u](ncf.fieldRef.asInstanceOf[RTypeRef[u]], in).asTerm)).asExpr.asTerm + ) + ) :+ CaseDef(Wildcard(), None, '{ $in.skipValue() }.asTerm) // skip values of unrecognized fields + '{ + val mark = $in.pos + var maybeFieldNum = $in.expectFirstObjectField(${ Ref(fieldMatrixSym).asExprOf[StringMatrix] }) + if maybeFieldNum == null then null.asInstanceOf[T] + else + while maybeFieldNum.isDefined do + ${ Match('{ maybeFieldNum.get }.asTerm, caseDefsWithFinal).asExprOf[Any] } + maybeFieldNum = $in.expectObjectField(${ Ref(fieldMatrixSym).asExprOf[StringMatrix] }) + + if (${ Ref(reqSym).asExprOf[Int] } & ${ exprRequired }) == 0 then + ${ Assign(instanceSymRef, instantiateClass.asExpr.asTerm).asExprOf[Unit] } // _instance = (new instance) + $in.revertToPos(mark) // go back to re-parse object json, this time for non-constructor fields + maybeFieldNum = $in.expectFirstObjectField(${ Ref(fieldMatrixSymNCF).asExprOf[StringMatrix] }) + while maybeFieldNum.isDefined do + ${ Match('{ maybeFieldNum.get }.asTerm, caseDefsWithFinalNC).asExprOf[Any] } + maybeFieldNum = $in.expectObjectField(${ Ref(fieldMatrixSymNCF).asExprOf[StringMatrix] }) + ${ Ref(instanceSym).asExprOf[T] } + else throw new JsonParseError("Missing required field(s) " + ${ allFieldNames }(Integer.numberOfTrailingZeros(${ Ref(reqSym).asExprOf[Int] } & ${ exprRequired })), $in) + }.asTerm + + Block(finalVarDefs :+ reqVarDef, parseLoop).asExprOf[T] + ) + + case t: JavaClassRef[?] => + makeReadFn[T](MethodKey(t, false), in)(in => + val classNameE = Expr(t.name) + val tpe = TypeRepr.of[b] + val instanceSym = Symbol.newVal(Symbol.spliceOwner, "_instance", TypeRepr.of[T], Flags.Mutable, Symbol.noSymbol) + val instanceSymRef = Ident(instanceSym.termRef) + val nullConst = tpe.classSymbol.get.companionModule.declaredMethods + .find(m => m.paramSymss == List(Nil)) + .getOrElse( + throw JsonTypeError("ScalaJack only supports Java classes that have a zero-argument constructor") + ) + makeClassFieldMatrixValDef(MethodKey(t, true), t.name.replaceAll("\\.", "_"), t.fields.sortBy(_.index).map(f => changeFieldName(f)).toArray) + val fieldMatrixSym = classFieldMatrixSyms(MethodKey(t, true)).asInstanceOf[Symbol] + val caseDefs = t.fields.map(ncf => + ncf.fieldRef.refType match + case '[u] => + CaseDef( + Literal(IntConstant(ncf.index)), + None, + // Call the setter for this field here... + Apply( + Select.unique(Ref(instanceSym), ncf.asInstanceOf[NonConstructorFieldInfoRef].setterLabel), + List(genReadVal[u](ncf.fieldRef.asInstanceOf[RTypeRef[u]], in).asTerm) + ).asExpr.asTerm + ) + ) :+ CaseDef(Wildcard(), None, '{ $in.skipValue() }.asTerm) // skip values of unrecognized fields + + val parseLoop = + '{ + var maybeFieldNum = $in.expectFirstObjectField(${ Ref(fieldMatrixSym).asExprOf[StringMatrix] }) + if maybeFieldNum == null then null.asInstanceOf[T] + else + ${ Assign(instanceSymRef, '{ Class.forName($classNameE).getDeclaredConstructor().newInstance().asInstanceOf[T] }.asTerm).asExprOf[Any] } // _instance = (new instance) + // ${ Assign(instanceSymRef, Apply(Select(New(Inferred(tpe)), nullConst), Nil).asExpr.asTerm).asExprOf[Unit] } // _instance = (new instance) + while maybeFieldNum.isDefined do + ${ Match('{ maybeFieldNum.get }.asTerm, caseDefs).asExprOf[Any] } + maybeFieldNum = $in.expectObjectField(${ Ref(fieldMatrixSym).asExprOf[StringMatrix] }) + ${ Ref(instanceSym).asExprOf[T] } + }.asTerm + Block(List(ValDef(instanceSym, Some('{ null }.asTerm))), parseLoop).asExprOf[T] + ) + + case t => throw new ParseError("Not yet implemented: " + t) + + // --------------------------------------------------------------------------------------------- + + def genReadVal[T: Type]( + ref: RTypeRef[T], + in: Expr[JsonSource], + inTuple: Boolean = false, + isMapKey: Boolean = false + )(using Quotes): Expr[T] = + val methodKey = MethodKey(ref, false) + readMethodSyms + .get(methodKey) + .map { sym => // hit cache first... then match on Ref type + Apply(Ref(sym), List(in.asTerm)).asExprOf[T] + } + .getOrElse( + ref match + // First cover all primitive and simple types... + case t: BigDecimalRef => + if isMapKey then + '{ + $in.expectString() match + case null => null + case s => scala.math.BigDecimal(s) + }.asExprOf[T] + else + '{ + $in.expectNumberOrNull() match + case null => null + case s => scala.math.BigDecimal(s) + }.asExprOf[T] + case t: BigIntRef => + if isMapKey then + '{ + $in.expectString() match + case null => null + case s => scala.math.BigInt(s) + }.asExprOf[T] + else + '{ + $in.expectNumberOrNull() match + case null => null + case s => scala.math.BigInt(s) + }.asExprOf[T] + case t: BooleanRef => + if isMapKey then '{ $in.expectString().toBoolean }.asExprOf[T] + else '{ $in.expectBoolean() }.asExprOf[T] + case t: ByteRef => + if isMapKey then '{ $in.expectString().toInt.toByte }.asExprOf[T] + else '{ $in.expectInt().toByte }.asExprOf[T] + case t: CharRef => + '{ + $in.expectString() match + case null => + $in.backspace() + $in.backspace() + $in.backspace() + $in.backspace() + throw JsonParseError("Char value cannot be null", $in) + case "" => + $in.backspace() + $in.backspace() + throw JsonParseError("Char value expected but empty string found in json", $in) + case c => c.charAt(0) + }.asExprOf[T] + case t: DoubleRef => + if isMapKey then '{ $in.expectString().toDouble }.asExprOf[T] + else '{ $in.expectDouble() }.asExprOf[T] + case t: FloatRef => + if isMapKey then '{ $in.expectString().toFloat }.asExprOf[T] + else '{ $in.expectFloat() }.asExprOf[T] + case t: IntRef => + if isMapKey then '{ $in.expectString().toInt }.asExprOf[T] + else '{ $in.expectInt() }.asExprOf[T] + case t: LongRef => + if isMapKey then '{ $in.expectString().toLong }.asExprOf[T] + else '{ $in.expectLong() }.asExprOf[T] + case t: ShortRef => + if isMapKey then '{ $in.expectString().toShort }.asExprOf[T] + else '{ $in.expectInt().toShort }.asExprOf[T] + case t: StringRef => + '{ $in.expectString() }.asExprOf[T] + + case t: JBigDecimalRef => + if isMapKey then + '{ + $in.expectString() match + case null => null + case n => new java.math.BigDecimal(n) + }.asExprOf[T] + else + '{ + $in.expectNumberOrNull() match + case null => null + case n => new java.math.BigDecimal(n) + }.asExprOf[T] + case t: JBigIntegerRef => + if isMapKey then + '{ + $in.expectString() match + case null => null + case n => new java.math.BigInteger(n) + }.asExprOf[T] + else + '{ + $in.expectNumberOrNull() match + case null => null + case n => new java.math.BigInteger(n) + }.asExprOf[T] + case t: JBooleanRef => + if isMapKey then '{ java.lang.Boolean.valueOf($in.expectString()) }.asExprOf[T] + else '{ $in.expectJavaBoolean() }.asExprOf[T] + case t: JByteRef => + if isMapKey then + '{ + $in.expectString() match + case null => null + case n => java.lang.Byte.valueOf(n) + }.asExprOf[T] + else + '{ + $in.expectNumberOrNull() match + case null => null + case n => java.lang.Byte.valueOf(n) + }.asExprOf[T] + case t: JCharacterRef => + '{ + val c = $in.expectString() + if c == null then null + else if c.length == 0 then + $in.backspace() + $in.backspace() + throw JsonParseError("Character value expected but empty string found in json", $in) + else java.lang.Character.valueOf(c.charAt(0)) + }.asExprOf[T] + case t: JDoubleRef => + if isMapKey then + '{ + $in.expectString() match + case null => null + case n => java.lang.Double.valueOf(n) + }.asExprOf[T] + else + '{ + $in.expectNumberOrNull() match + case null => null + case n => java.lang.Double.valueOf(n) + }.asExprOf[T] + case t: JFloatRef => + if isMapKey then + '{ + $in.expectString() match + case null => null + case n => java.lang.Float.valueOf(n) + }.asExprOf[T] + else + '{ + $in.expectNumberOrNull() match + case null => null + case n => java.lang.Float.valueOf(n) + }.asExprOf[T] + case t: JIntegerRef => + if isMapKey then + '{ + $in.expectString() match + case null => null + case n => java.lang.Integer.valueOf(n) + }.asExprOf[T] + else + '{ + $in.expectNumberOrNull() match + case null => null + case n => java.lang.Integer.valueOf(n) + }.asExprOf[T] + case t: JLongRef => + if isMapKey then + '{ + $in.expectString() match + case null => null + case n => java.lang.Long.valueOf(n) + }.asExprOf[T] + else + '{ + $in.expectNumberOrNull() match + case null => null + case n => java.lang.Long.valueOf(n) + }.asExprOf[T] + case t: JShortRef => + if isMapKey then + '{ + $in.expectString() match + case null => null + case n => java.lang.Short.valueOf(n) + }.asExprOf[T] + else + '{ + $in.expectNumberOrNull() match + case null => null + case n => java.lang.Short.valueOf(n) + }.asExprOf[T] + case t: JNumberRef => + if isMapKey then + '{ + $in.expectString() match + case null => null + case n => + scala.math.BigDecimal(n) match { + case d if d.isValidByte => java.lang.Byte.valueOf(d.toByteExact) + case d if d.isValidShort => java.lang.Short.valueOf(d.toShortExact) + case d if d.isValidInt => java.lang.Integer.valueOf(d.toIntExact) + case d if d.isValidLong => java.lang.Long.valueOf(d.toLongExact) + case d if d.isDecimalFloat => java.lang.Float.valueOf(d.toFloat) + case d if d.isDecimalDouble => java.lang.Double.valueOf(d.toDouble) + case d => d + } + }.asExprOf[T] + else + '{ + $in.expectNumberOrNull() match + case null => null + case n => + scala.math.BigDecimal(n) match { + case d if d.isValidByte => java.lang.Byte.valueOf(d.toByteExact) + case d if d.isValidShort => java.lang.Short.valueOf(d.toShortExact) + case d if d.isValidInt => java.lang.Integer.valueOf(d.toIntExact) + case d if d.isValidLong => java.lang.Long.valueOf(d.toLongExact) + case d if d.isDecimalFloat => java.lang.Float.valueOf(d.toFloat) + case d if d.isDecimalDouble => java.lang.Double.valueOf(d.toDouble) + case d => d + } + }.asExprOf[T] + + case t: DurationRef => '{ $in.expectString(java.time.Duration.parse) }.asExprOf[T] + case t: InstantRef => '{ $in.expectString(java.time.Instant.parse) }.asExprOf[T] + case t: LocalDateRef => '{ $in.expectString(java.time.LocalDate.parse) }.asExprOf[T] + case t: LocalDateTimeRef => '{ $in.expectString(java.time.LocalDateTime.parse) }.asExprOf[T] + case t: LocalTimeRef => '{ $in.expectString(java.time.LocalTime.parse) }.asExprOf[T] + case t: MonthDayRef => '{ $in.expectString(java.time.MonthDay.parse) }.asExprOf[T] + case t: OffsetDateTimeRef => '{ $in.expectString(java.time.OffsetDateTime.parse) }.asExprOf[T] + case t: OffsetTimeRef => '{ $in.expectString(java.time.OffsetTime.parse) }.asExprOf[T] + case t: PeriodRef => '{ $in.expectString(java.time.Period.parse) }.asExprOf[T] + case t: YearRef => '{ $in.expectString(java.time.Year.parse) }.asExprOf[T] + case t: YearMonthRef => '{ $in.expectString(java.time.YearMonth.parse) }.asExprOf[T] + case t: ZonedDateTimeRef => '{ $in.expectString(java.time.ZonedDateTime.parse) }.asExprOf[T] + case t: ZoneIdRef => '{ $in.expectString(java.time.ZoneId.of) }.asExprOf[T] + case t: ZoneOffsetRef => '{ $in.expectString(java.time.ZoneOffset.of) }.asExprOf[T] + + case t: URLRef => '{ $in.expectString((s: String) => new java.net.URI(s).toURL()) }.asExprOf[T] + case t: URIRef => '{ $in.expectString((s: String) => new java.net.URI(s)) }.asExprOf[T] + case t: UUIDRef => '{ $in.expectString(java.util.UUID.fromString) }.asExprOf[T] + + case t: AliasRef[?] => + t.unwrappedType.refType match + case '[e] => + '{ + ${ + genReadVal[e]( + t.unwrappedType.asInstanceOf[RTypeRef[e]], + in, + inTuple, + isMapKey + ) + }.asInstanceOf[T] + } + + // -------------------- + // Options... + // -------------------- + case t: ScalaOptionRef[?] => + import quotes.reflect.* + t.optionParamType.refType match + case '[e] => + if cfg.noneAsNull || inTuple then + '{ + if $in.expectNull() then None + else Some(${ genReadVal[e](t.optionParamType.asInstanceOf[RTypeRef[e]], in) }) + }.asExprOf[T] + else + '{ + if $in.expectNull() then null + else ${ ofOption[e](Some(genReadVal[e](t.optionParamType.asInstanceOf[RTypeRef[e]], in))).asExprOf[T] } + }.asExprOf[T] + case t: JavaOptionalRef[?] => + import quotes.reflect.* + t.optionParamType.refType match + case '[e] => + if cfg.noneAsNull || inTuple then + '{ + if $in.expectNull() then java.util.Optional.empty + else java.util.Optional.of(${ genReadVal[e](t.optionParamType.asInstanceOf[RTypeRef[e]], in) }) + }.asExprOf[T] + else + '{ + if $in.expectNull() then null + else ${ ofOptional[e](java.util.Optional.of(genReadVal[e](t.optionParamType.asInstanceOf[RTypeRef[e]], in))).asExprOf[T] } + }.asExprOf[T] + // else ofOption[e](Some(genReadVal[e](t.optionParamType.asInstanceOf[RTypeRef[e]], in))).asExprOf[T] + + case t: LeftRightRef[?] if t.lrkind == LRKind.EITHER => + import quotes.reflect.* + t.leftRef.refType match + case '[l] => + t.rightRef.refType match + case '[r] => + '{ + val mark = $in.pos + if $in.expectNull() then null + else + scala.util.Try(${ genReadVal[r](t.rightRef.asInstanceOf[RTypeRef[r]], in, inTuple) }) match + case Success(rval) => + Right(rval) + case Failure(f) => + $in.revertToPos(mark) + scala.util.Try(${ genReadVal[l](t.leftRef.asInstanceOf[RTypeRef[l]], in, inTuple) }) match + case Success(lval) => Left(lval) + case Failure(_) => + $in.backspace() + throw JsonParseError("Failed to read either side of Either type", $in) + }.asExprOf[T] + + case t: LeftRightRef[?] if t.lrkind == LRKind.UNION => + import quotes.reflect.* + t.leftRef.refType match + case '[l] => + t.rightRef.refType match + case '[r] => + '{ + val mark = $in.pos + scala.util.Try(${ genReadVal[l](t.leftRef.asInstanceOf[RTypeRef[l]], in, true) }) match + case Success(lval) => lval + case Failure(f) => + $in.revertToPos(mark) + scala.util.Try(${ genReadVal[r](t.rightRef.asInstanceOf[RTypeRef[r]], in, true) }) match + case Success(rval) => rval + case Failure(_) => + $in.backspace() + throw JsonParseError("Failed to read either side of Union type", $in) + }.asExprOf[T] + + case t: LeftRightRef[?] if t.lrkind == LRKind.INTERSECTION => + throw JsonTypeError("Intersection types currently unsupported by ScalaJack") + + // -------------------- + // Enumerations... + // -------------------- + case t: ScalaEnumRef[?] => + import quotes.reflect.* + t.refType match + case '[e] => + '{ + $in.expectEnum() match + case null => null + case s: String => + ${ + cfg.enumsAsIds match + case List("-") => + val typeRepr = TypeRepr.of[e] + val m = typeRepr.typeSymbol.companionClass.declaredMethod("valueOf") + Apply(Ref(m(0)), List('{ s }.asTerm)).asExpr + case c if c == Nil || c.contains(t.name) => + val typeRepr = TypeRepr.of[e] + val m = typeRepr.typeSymbol.companionClass.declaredMethod("fromOrdinal") + Apply(Ref(m(0)), List('{ s.toInt }.asTerm)).asExpr + case _ => + val typeRepr = TypeRepr.of[e] + val m = typeRepr.typeSymbol.companionClass.declaredMethod("valueOf") + Apply(Ref(m(0)), List('{ s }.asTerm)).asExpr + } + case v: Int => + ${ + val typeRepr = TypeRepr.of[e] + val m = typeRepr.typeSymbol.companionClass.declaredMethod("fromOrdinal") + Apply(Ref(m(0)), List('{ v }.asTerm)).asExpr + } + }.asExprOf[T] + case t: ScalaEnumerationRef[?] => + import quotes.reflect.* + t.refType match + case '[e] => + '{ + $in.expectEnum() match + case null => null + case s: String => + ${ + cfg.enumsAsIds match + case List("-") => + val enumeration = TypeRepr.of[e] match + case TypeRef(ct, _) => Ref(ct.termSymbol).asExprOf[Enumeration] + '{ ${ enumeration }.values.iterator.find(_.toString == s).get }.asExprOf[e] + case c if c == Nil || c.contains(t.name) => + val enumeration = TypeRepr.of[e] match + case TypeRef(ct, _) => Ref(ct.termSymbol).asExprOf[Enumeration] + '{ ${ enumeration }.values.iterator.find(_.id == s.toInt).get }.asExprOf[e] + case _ => + val enumeration = TypeRepr.of[e] match + case TypeRef(ct, _) => Ref(ct.termSymbol).asExprOf[Enumeration] + '{ ${ enumeration }.values.iterator.find(_.toString == s).get }.asExprOf[e] + } + case v: Int => + ${ + val enumeration = TypeRepr.of[e] match + case TypeRef(ct, _) => Ref(ct.termSymbol).asExprOf[Enumeration] + '{ ${ enumeration }.values.iterator.find(_.id == v).get }.asExprOf[e] + } + }.asExprOf[T] + case t: JavaEnumRef[?] => + import quotes.reflect.* + t.refType match + case '[e] => + val valuesE = Expr(t.values) + '{ + $in.expectEnum() match + case null => null + case s: String => + scala.util.Try(s.toInt) match + case Success(v) => + ${ + val typeRepr = TypeRepr.of[e] + val m = typeRepr.classSymbol.get.companionModule.methodMember("valueOf") + Apply(Ref(m(0)), List('{ $valuesE(v) }.asTerm)).asExpr + } + case _ => + ${ + val typeRepr = TypeRepr.of[e] + val m = typeRepr.classSymbol.get.companionModule.methodMember("valueOf") + Apply(Ref(m(0)), List('{ s }.asTerm)).asExpr + } + case v: Int => + ${ + val typeRepr = TypeRepr.of[e] + val m = typeRepr.classSymbol.get.companionModule.methodMember("valueOf") + Apply(Ref(m(0)), List('{ $valuesE(v) }.asTerm)).asExpr + } + }.asExprOf[T] + + // -------------------- + // Collections... + // -------------------- + case t: SeqRef[?] => + ref.refType match + case '[List[?]] => + t.elementRef.refType match + case '[e] => + val rtypeRef = t.elementRef.asInstanceOf[RTypeRef[e]] + '{ + val parsedArray = $in.expectArray[e](() => ${ genReadVal[e](rtypeRef, in, inTuple) }) + if parsedArray != null then parsedArray.toList + else null + }.asExprOf[T] + case '[Vector[?]] => + t.elementRef.refType match + case '[e] => + val rtypeRef = t.elementRef.asInstanceOf[RTypeRef[e]] + '{ + val parsedArray = $in.expectArray[e](() => ${ genReadVal[e](rtypeRef, in, inTuple) }) + if parsedArray != null then parsedArray.toVector + else null + }.asExprOf[T] + case '[IndexedSeq[?]] => + t.elementRef.refType match + case '[e] => + val rtypeRef = t.elementRef.asInstanceOf[RTypeRef[e]] + '{ + val parsedArray = $in.expectArray[e](() => ${ genReadVal[e](rtypeRef, in, inTuple) }) + if parsedArray != null then parsedArray.toIndexedSeq + else null + }.asExprOf[T] + case '[Seq[?]] => + t.elementRef.refType match + case '[e] => + val rtypeRef = t.elementRef.asInstanceOf[RTypeRef[e]] + '{ + val parsedArray = $in.expectArray[e](() => ${ genReadVal[e](rtypeRef, in, inTuple) }) + if parsedArray != null then parsedArray.toSeq + else null + }.asExprOf[T] + // Catch all, with (slightly) slower type coersion to proper Seq flavor + case _ => + t.elementRef.refType match + case '[e] => + val rtypeRef = t.elementRef.asInstanceOf[RTypeRef[e]] + '{ + val parsedArray = $in.expectArray[e](() => ${ genReadVal[e](rtypeRef, in, inTuple) }) + if parsedArray != null then parsedArray.to(${ Expr.summon[Factory[e, T]].get }) // create appropriate flavor of Seq[T] here + else null + }.asExprOf[T] + + case t: IterableRef[?] => + t.elementRef.refType match + case '[e] => + val rtypeRef = t.elementRef.asInstanceOf[RTypeRef[e]] + val ct = Expr.summon[ClassTag[e]].get + '{ + val parsedArray = $in.expectArray[e](() => ${ genReadVal[e](rtypeRef, in, inTuple) }) + if parsedArray != null then + implicit val ctt = $ct + parsedArray.asInstanceOf[Iterable[e]] + else null + }.asExprOf[T] + + case t: ArrayRef[?] => + t.elementRef.refType match + case '[e] => + val rtypeRef = t.elementRef.asInstanceOf[RTypeRef[e]] + val ct = Expr.summon[ClassTag[e]].get + '{ + val parsedArray = $in.expectArray[e](() => ${ genReadVal[e](rtypeRef, in, inTuple) }) + if parsedArray != null then + implicit val ctt = $ct + parsedArray.toArray[e] + else null + }.asExprOf[T] + + case t: SetRef[?] => + t.elementRef.refType match + case '[e] => + val rtypeRef = t.elementRef.asInstanceOf[RTypeRef[e]] + '{ + val parsedArray = $in.expectArray[e](() => ${ genReadVal[e](rtypeRef, in, inTuple) }) + if parsedArray != null then parsedArray.to(${ Expr.summon[Factory[e, T]].get }) // create appropriate flavor of Set[T] here + else null + }.asExprOf[T] + + // -------------------- + // Java Collections... + // -------------------- + case t: JavaCollectionRef[?] => + t.elementRef.refType match + case '[e] => + val rtypeRef = t.elementRef.asInstanceOf[RTypeRef[e]] + ref.name match + // Non-standard concrete Java Collecitons + case "java.util.concurrent.ArrayBlockingQueue" => + '{ + val parsedArray = $in.expectArray[e](() => ${ genReadVal[e](rtypeRef, in, inTuple) }) + if parsedArray == null then null + else new java.util.concurrent.ArrayBlockingQueue(parsedArray.length, true, parsedArray.toList.asJava) + }.asExprOf[T] + // java.util.Collection interfaces + case "java.util.Stack" => + '{ + val parsedArray = $in.expectArray[e](() => ${ genReadVal[e](rtypeRef, in, inTuple) }) + if parsedArray == null then null + else + val s = new java.util.Stack[e]() + parsedArray.map(j => s.push(j)) + s + }.asExprOf[T] + case "java.util.concurrent.TransferQueue" => + '{ + val parsedArray = $in.expectArray[e](() => ${ genReadVal[e](rtypeRef, in, inTuple) }) + if parsedArray != null then new java.util.concurrent.LinkedTransferQueue(parsedArray.toList.asJava) + else null + }.asExprOf[T] + case "java.util.concurrent.BlockingQueue" => + '{ + val parsedArray = $in.expectArray[e](() => ${ genReadVal[e](rtypeRef, in, inTuple) }) + if parsedArray != null then new java.util.concurrent.LinkedBlockingQueue(parsedArray.toList.asJava) + else null + }.asExprOf[T] + case "java.util.TreeSet" | "java.util.NavigableSet" | "java.util.SortedSet" => + t.elementRef match + case _: PrimitiveRef => + t.elementRef.refType match + case '[z] => + '{ + val parsedArray = $in.expectArray[e](() => ${ genReadVal[e](rtypeRef, in, inTuple) }) + if parsedArray != null then new java.util.TreeSet(parsedArray.toList.asJava) + else null + }.asExprOf[T] + case x => throw JsonTypeError("Only primitive types supported for TreeSet, NavigableSet, or SortedSet. You've specified " + x.name) + case "java.util.Queue" | "java.util.Deque" => + '{ + val parsedArray = $in.expectArray[e](() => ${ genReadVal[e](rtypeRef, in, inTuple) }) + if parsedArray != null then new java.util.LinkedList(parsedArray.toList.asJava) + else null + }.asExprOf[T] + case "java.util.Set" => + '{ + val parsedArray = $in.expectArray[e](() => ${ genReadVal[e](rtypeRef, in, inTuple) }) + if parsedArray != null then new java.util.HashSet(parsedArray.toList.asJava) + else null + }.asExprOf[T] + case "java.util.List" | "java.lang.Iterable" => + '{ + val parsedArray = $in.expectArray[e](() => ${ genReadVal[e](rtypeRef, in, inTuple) }) + if parsedArray != null then new java.util.ArrayList(parsedArray.toList.asJava) + else null + }.asExprOf[T] + // Normal concrete Java Collecitons + case _ => + '{ + if $in.expectNull() then null + else + ${ + val arg = '{ + val parsedArray = $in.expectArray[e](() => ${ genReadVal[e](t.elementRef.asInstanceOf[RTypeRef[e]], in, inTuple) }) + parsedArray.toList.asJava.asInstanceOf[java.util.Collection[e]] + }.asTerm + Select + .overloaded( + New(Inferred(TypeRepr.of[T])), + "", + List(TypeRepr.of[e]), + List(arg) + ) + .asExprOf[T] + } + }.asExprOf[T] + + // -------------------- + // Maps... + // -------------------- + case t: MapRef[?] => + ref.refType match + case '[scala.collection.mutable.HashMap[?, ?]] => + t.elementRef.refType match + case '[k] => + t.elementRef2.refType match + case '[v] => + testValidMapKey(t.elementRef) + '{ + if $in.expectNull() then null + else + $in.expectToken('{') + scala.collection.mutable.HashMap.from( + $in.parseMap[k, v]( + () => ${ genReadVal[k](t.elementRef.asInstanceOf[RTypeRef[k]], in, inTuple, true) }, + () => ${ genReadVal[v](t.elementRef2.asInstanceOf[RTypeRef[v]], in, inTuple) }, + Map.empty[k, v], + true + ) + ) + }.asExprOf[T] + case '[scala.collection.mutable.Map[?, ?]] if ref.name == "scala.collection.mutable.Map" => + t.elementRef.refType match + case '[k] => + t.elementRef2.refType match + case '[v] => + testValidMapKey(t.elementRef) + '{ + if $in.expectNull() then null + else + $in.expectToken('{') + scala.collection.mutable.Map.from( + $in.parseMap[k, v]( + () => ${ genReadVal[k](t.elementRef.asInstanceOf[RTypeRef[k]], in, inTuple, true) }, + () => ${ genReadVal[v](t.elementRef2.asInstanceOf[RTypeRef[v]], in, inTuple) }, + Map.empty[k, v], + true + ) + ) + }.asExprOf[T] + case '[scala.collection.mutable.Map[?, ?]] => // all other mutable Maps + t.elementRef.refType match + case '[k] => + t.elementRef2.refType match + case '[v] => + testValidMapKey(t.elementRef) + '{ + if $in.expectNull() then null + else + $in.expectToken('{') + scala.collection.mutable.Map + .from( + $in.parseMap[k, v]( + () => ${ genReadVal[k](t.elementRef.asInstanceOf[RTypeRef[k]], in, inTuple, true) }, + () => ${ genReadVal[v](t.elementRef2.asInstanceOf[RTypeRef[v]], in, inTuple) }, + Map.empty[k, v], + true + ) + ) + .to(${ Expr.summon[Factory[(k, v), T]].get }) + }.asExprOf[T] + case '[scala.collection.immutable.HashMap[?, ?]] => // immutable + t.elementRef.refType match + case '[k] => + t.elementRef2.refType match + case '[v] => + testValidMapKey(t.elementRef) + '{ + if $in.expectNull() then null + else + $in.expectToken('{') + scala.collection.immutable.HashMap.from( + $in.parseMap[k, v]( + () => ${ genReadVal[k](t.elementRef.asInstanceOf[RTypeRef[k]], in, inTuple, true) }, + () => ${ genReadVal[v](t.elementRef2.asInstanceOf[RTypeRef[v]], in, inTuple) }, + Map.empty[k, v], + true + ) + ) + }.asExprOf[T] + case '[scala.collection.immutable.SeqMap[?, ?]] => // immutable + t.elementRef.refType match + case '[k] => + t.elementRef2.refType match + case '[v] => + testValidMapKey(t.elementRef) + '{ + if $in.expectNull() then null + else + $in.expectToken('{') + scala.collection.immutable.SeqMap.from( + $in.parseMap[k, v]( + () => ${ genReadVal[k](t.elementRef.asInstanceOf[RTypeRef[k]], in, inTuple, true) }, + () => ${ genReadVal[v](t.elementRef2.asInstanceOf[RTypeRef[v]], in, inTuple) }, + Map.empty[k, v], + true + ) + ) + }.asExprOf[T] + case '[Map[?, ?]] if ref.name == "scala.collection.immutable.Map" => // immutable + t.elementRef.refType match + case '[k] => + t.elementRef2.refType match + case '[v] => + testValidMapKey(t.elementRef) + '{ + if $in.expectNull() then null + else + $in.expectToken('{') + $in.parseMap[k, v]( + () => ${ genReadVal[k](t.elementRef.asInstanceOf[RTypeRef[k]], in, inTuple, true) }, + () => ${ genReadVal[v](t.elementRef2.asInstanceOf[RTypeRef[v]], in, inTuple) }, + Map.empty[k, v], + true + ) + }.asExprOf[T] + case '[Map[?, ?]] => // all other immutable Maps + t.elementRef.refType match + case '[k] => + t.elementRef2.refType match + case '[v] => + testValidMapKey(t.elementRef) + '{ + if $in.expectNull() then null + else + $in.expectToken('{') + $in + .parseMap[k, v]( + () => ${ genReadVal[k](t.elementRef.asInstanceOf[RTypeRef[k]], in, inTuple, true) }, + () => ${ genReadVal[v](t.elementRef2.asInstanceOf[RTypeRef[v]], in, inTuple) }, + Map.empty[k, v], + true + ) + .to(${ Expr.summon[Factory[(k, v), T]].get }) + }.asExprOf[T] + + // -------------------- + // Java Maps... + // -------------------- + case t: JavaMapRef[?] => + t.elementRef.refType match + case '[k] => + t.elementRef2.refType match + case '[v] => + ref.name match + case "java.util.NavigableMap" | "java.util.SortedMap" | "java.util.TreeMap" => + testValidMapKey(t.elementRef) + '{ + if $in.expectNull() then null + else + $in.expectToken('{') + new java.util.TreeMap( + $in + .parseMap[k, v]( + () => ${ genReadVal[k](t.elementRef.asInstanceOf[RTypeRef[k]], in, inTuple, true) }, + () => ${ genReadVal[v](t.elementRef2.asInstanceOf[RTypeRef[v]], in, inTuple) }, + Map.empty[k, v], + true + ) + .asJava + ) + }.asExprOf[T] + case "java.util.concurrent.ConcurrentMap" => + testValidMapKey(t.elementRef) + '{ + if $in.expectNull() then null + else + $in.expectToken('{') + new java.util.concurrent.ConcurrentHashMap( + $in + .parseMap[k, v]( + () => ${ genReadVal[k](t.elementRef.asInstanceOf[RTypeRef[k]], in, inTuple, true) }, + () => ${ genReadVal[v](t.elementRef2.asInstanceOf[RTypeRef[v]], in, inTuple) }, + Map.empty[k, v], + true + ) + .asJava + ) + }.asExprOf[T] + case "java.util.concurrent.ConcurrentNavigableMap" => + testValidMapKey(t.elementRef) + '{ + if $in.expectNull() then null + else + $in.expectToken('{') + new java.util.concurrent.ConcurrentSkipListMap( + $in + .parseMap[k, v]( + () => ${ genReadVal[k](t.elementRef.asInstanceOf[RTypeRef[k]], in, inTuple, true) }, + () => ${ genReadVal[v](t.elementRef2.asInstanceOf[RTypeRef[v]], in, inTuple) }, + Map.empty[k, v], + true + ) + .asJava + ) + }.asExprOf[T] + case "java.util.Map" => + testValidMapKey(t.elementRef) + '{ + if $in.expectNull() then null + else + $in.expectToken('{') + new java.util.HashMap( + $in + .parseMap[k, v]( + () => ${ genReadVal[k](t.elementRef.asInstanceOf[RTypeRef[k]], in, inTuple, true) }, + () => ${ genReadVal[v](t.elementRef2.asInstanceOf[RTypeRef[v]], in, inTuple) }, + Map.empty[k, v], + true + ) + .asJava + ) + }.asExprOf[T] + case _ => + testValidMapKey(t.elementRef) + '{ + if $in.expectNull() then null + else + ${ + val arg = '{ + $in.expectToken('{') + $in + .parseMap[k, v]( + () => ${ genReadVal[k](t.elementRef.asInstanceOf[RTypeRef[k]], in, inTuple, true) }, + () => ${ genReadVal[v](t.elementRef2.asInstanceOf[RTypeRef[v]], in, inTuple) }, + Map.empty[k, v], + true + ) + .asJava + }.asTerm + Select + .overloaded( + New(Inferred(TypeRepr.of[T])), + "", + List(TypeRepr.of[k], TypeRepr.of[v]), + List(arg) + ) + .asExprOf[T] + } + }.asExprOf[T] + + // -------------------- + // Tuples... + // -------------------- + case t: TupleRef[?] => + import quotes.reflect.* + t.refType match + case '[tt] => + val tpe = TypeRepr.of[tt] + val maxI = Expr(t.tupleRefs.length - 1) + val indexedTypes = tpe match + case AppliedType(_, typeArgs) => typeArgs.map(_.dealias) + case _ => Nil + + // make all the tuple terms, accounting for , and ] detection + val tupleTerms = + t.tupleRefs.zipWithIndex.map { case (tpart, i) => + tpart.refType match + case '[e] => + if i == 0 then genReadVal[e](tpart.asInstanceOf[RTypeRef[e]], in, true).asTerm + else + '{ + $in.expectToken(',') + ${ genReadVal[e](tpart.asInstanceOf[RTypeRef[e]], in, true) } + }.asTerm + } + '{ + if $in.expectNull() then null + else + $in.expectToken('[') + val tv: T = ${ + Apply(TypeApply(Select.unique(New(Inferred(tpe)), ""), indexedTypes.map(x => Inferred(x))), tupleTerms).asExprOf[T] + } + $in.expectToken(']') + tv + }.asExprOf[T] + + // -------------------- + // Try... + // -------------------- + case t: TryRef[?] => + t.tryRef.refType match + case '[e] => + '{ + val mark = $in.pos + try + if $in.expectNull() then null + else scala.util.Success(${ genReadVal[e](t.tryRef.asInstanceOf[RTypeRef[e]], in) }) + catch { + case t: Throwable => + $in.revertToPos(mark) + throw JsonParseError("Unsuccessful attempt to read Try type with failure: " + t.getMessage(), $in) + } + }.asExprOf[T] + + // -------------------- + // Value Class... + // -------------------- + case t: ScalaClassRef[?] if t.isValueClass => + val theField = t.fields.head.fieldRef + t.refType match + case '[e] => + theField.refType match + case '[f] => + val tpe = TypeRepr.of[e] + val constructor = Select(New(Inferred(tpe)), tpe.classSymbol.get.primaryConstructor) + val arg = genReadVal[f](theField.asInstanceOf[RTypeRef[f]], in, isMapKey = isMapKey).asTerm + Apply(constructor, List(arg)).asExprOf[T] + + case t: SelfRefRef[?] => + t.refType match + case '[e] => + val ref = ReflectOnType[e](q)(TypeRepr.of[e])(using scala.collection.mutable.Map.empty[TypedName, Boolean]) + val key = MethodKey(ref, false) + val sym = readMethodSyms(key) + Ref(sym).appliedTo(in.asTerm).asExprOf[T] + + // -------------------- + // NeoType... + // -------------------- + case t: NeoTypeRef[?] => // in Quotes context + val module = Symbol.requiredModule(t.typedName.toString) + val myMake = module.methodMember("make").head + val tm = Ref(module) + t.wrappedTypeRef.refType match + case '[e] => + val res = Apply( + Select.unique(tm, "make"), + List( + genReadVal[e](t.wrappedTypeRef.asInstanceOf[RTypeRef[e]], in, isMapKey = isMapKey).asTerm + ) + ).asExprOf[Either[String, T]] + val tnameE = Expr(t.name) + '{ + $res match + case Right(r) => r + case Left(m) => + $in.backspace() + throw JsonParseError("NeoType validation for " + $tnameE + " failed", $in) + } + + case _ => + // Classes, traits, etc. + genDecFnBody[T](ref, in) // Create a new decoder function (presumably for class, trait, etc) + genReadVal(ref, in, inTuple) + + // Just-created function is present now and will be called + ) + + // --------------------------------------------------------------------------------------------- + + // ================================================================ + // You've made it this far! Ok, now we sew everything together. + // We generate a codec class and then kick off a deep traversal of + // generation from the given root ref (refer waaay back at the top of this fn...). + // ================================================================ + val codecDef = '{ // FIXME: generate a type class instance using `ClassDef.apply` and `Symbol.newClass` calls after graduating from experimental API: https://www.scala-lang.org/blog/2022/06/21/scala-3.1.3-released.html + new JsonCodec[T] { + def encodeValue(in: T, out: JsonOutput): Unit = ${ genWriteVal('in, ref, 'out) } + def decodeValue(in: JsonSource): T = ${ genReadVal(ref, 'in) } + } + }.asTerm + val codec = Block((classFieldMatrixValDefs ++ writeMethodDefs ++ readMethodDefs).toList, codecDef).asExprOf[JsonCodec[T]] + // println(s"Codec: ${codec.show}") + codec diff --git a/src/main/scala/co.blocke.scalajack/json/JsonError.scala b/src/main/scala/co.blocke.scalajack/json/JsonError.scala new file mode 100644 index 00000000..6e3f8832 --- /dev/null +++ b/src/main/scala/co.blocke.scalajack/json/JsonError.scala @@ -0,0 +1,30 @@ +package co.blocke.scalajack +package json + +import scala.util.control.NoStackTrace + +class JsonIllegalKeyType(msg: String) extends Throwable(msg) with NoStackTrace +class JsonNullKeyValue(msg: String) extends Throwable(msg) with NoStackTrace +class JsonUnsupportedType(msg: String) extends Throwable(msg) with NoStackTrace +class JsonEitherLeftError(msg: String) extends Throwable(msg) with NoStackTrace +class JsonIllegalCharacterError(msg: String) extends Throwable(msg) with NoStackTrace + +class ParseError(val msg: String) extends Throwable(msg) with NoStackTrace: + val show: String = "" + +// Thrown at compile-time only! +case class JsonTypeError(override val msg: String) extends ParseError(msg) with NoStackTrace: + override val show: String = "" + +// Thrown at runtime only! +case class JsonParseError(override val msg: String, context: reading.JsonSource) extends ParseError(msg + " at position " + context.pos) with NoStackTrace: + override val show: String = + val js = context.js.toString + val (clip, dashes) = context.pos match { + case ep if ep <= 50 && context.max < 80 => (js, ep) + case ep if ep <= 50 => (js.substring(0, 77) + "...", ep) + case ep if ep > 50 && ep + 30 >= context.max => + ("..." + js.substring(context.pos - 49), 52) + case ep => ("..." + js.substring(ep - 49, ep + 27) + "...", 52) + } + msg + s" at position [${context.pos}]" + "\n" + clip.replaceAll("[\n\t]", "~") + "\n" + ("-" * dashes) + "^" diff --git a/src/main/scala/co.blocke.scalajack/json/StringMatrix.scala b/src/main/scala/co.blocke.scalajack/json/StringMatrix.scala new file mode 100644 index 00000000..ffbb799f --- /dev/null +++ b/src/main/scala/co.blocke.scalajack/json/StringMatrix.scala @@ -0,0 +1,84 @@ +package co.blocke.scalajack +package json + +// A data structure encoding a simple algorithm for Trie pruning: Given a list +// of strings, and a sequence of incoming characters, find the strings that +// match, by manually maintaining a bitset. Empty strings are not allowed. +// +// ScalaJack: Removal of ZIO's original aliases feature for speed--we don't need this. +// +final class StringMatrix(val xs: Array[String]) { + require(xs.forall(_.nonEmpty)) + require(xs.nonEmpty) + require(xs.length < 64) + + val width = xs.length + val height: Int = xs.map(_.length).max + val lengths: Array[Int] = xs.map(_.length) + val initial: Long = (0 until width).foldLeft(0L)((bs, r) => bs | (1L << r)) + + private val matrix: Array[Int] = { + val m = Array.fill[Int](width * height)(-1) + var string: Int = 0 + while string < width do { + val s = xs(string) + val len = s.length + var char: Int = 0 + while char < len do { + m(width * char + string) = s.codePointAt(char) + char += 1 + } + string += 1 + } + m + } + + private val resolve: Array[Int] = Array.tabulate[Int](xs.length)(identity) + + // must be called with increasing `char` (starting with bitset obtained from a + // call to 'initial', char = 0) + def update(bitset: Long, char: Int, c: Int): Long = + if char >= height then 0L // too long + else if bitset == 0L then 0L // everybody lost + else { + var latest: Long = bitset + val base: Int = width * char + + if bitset == initial then { // special case when it is dense since it is simple + var string: Int = 0 + while string < width do { + if matrix(base + string) != c then latest = latest ^ (1L << string) + string += 1 + } + } else { + var remaining: Long = bitset + while remaining != 0L do { + val string: Int = java.lang.Long.numberOfTrailingZeros(remaining) + val bit: Long = 1L << string + if matrix(base + string) != c then latest = latest ^ bit + remaining = remaining ^ bit + } + } + + latest + } + + // excludes entries that are not the given exact length + def exact(bitset: Long, length: Int): Long = + if length > height then 0L // too long + else { + var latest: Long = bitset + var remaining: Long = bitset + while remaining != 0L do { + val string: Int = java.lang.Long.numberOfTrailingZeros(remaining) + val bit: Long = 1L << string + if lengths(string) != length then latest = latest ^ bit + remaining = remaining ^ bit + } + latest + } + + def first(bitset: Long): Int = + if bitset == 0L then -1 + else resolve(java.lang.Long.numberOfTrailingZeros(bitset)) // never returns 64 +} diff --git a/src/main/scala/co.blocke.scalajack/json/package.scala b/src/main/scala/co.blocke.scalajack/json/package.scala new file mode 100644 index 00000000..5a78546b --- /dev/null +++ b/src/main/scala/co.blocke.scalajack/json/package.scala @@ -0,0 +1,20 @@ +package co.blocke.scalajack +package json + +import scala.util.Failure +import scala.quoted.{Expr, Quotes, Type} +import java.util.Optional + +val BUFFER_EXCEEDED: Char = 7 // Old "BELL" ASCII value, used as a marker when we've run off the end of the known world +val END_OF_STRING: Char = 3 + +def ofOption[T](xs: Option[Expr[T]])(using Type[T])(using q: Quotes): Expr[Option[T]] = + import q.reflect.* + if xs.isEmpty then '{ None } + else '{ Some(${ xs.get }) } + +// Java variant of ofOption +def ofOptional[T](xs: Optional[Expr[T]])(using Type[T])(using q: Quotes): Expr[Optional[T]] = + import q.reflect.* + if xs.isEmpty then '{ Optional.empty } + else '{ Optional.of(${ xs.get }) } diff --git a/src/main/scala/co.blocke.scalajack/json/reading/JsonSource.scala b/src/main/scala/co.blocke.scalajack/json/reading/JsonSource.scala new file mode 100644 index 00000000..8c436ac1 --- /dev/null +++ b/src/main/scala/co.blocke.scalajack/json/reading/JsonSource.scala @@ -0,0 +1,451 @@ +package co.blocke.scalajack +package json +package reading + +import scala.annotation.{switch, tailrec} +import co.blocke.scalajack.internal.SafeNumbers.double +import co.blocke.scalajack.internal.UnsafeNumbers + +object JsonSource: + val ull: Array[Char] = "ull".toCharArray + protected val alse: Array[Char] = "alse".toCharArray + protected val rue: Array[Char] = "rue".toCharArray + protected val falseBytes = 'f' | 'a' << 8 | 'l' << 16 | 's' << 24 | 'e' << 32 + protected val trueBytes = 't' | 'r' << 8 | 'u' << 16 | 'e' << 24 + +// ZIO-Json defines a series of different Readers. Not exactly sure why--maybe to support different +// modes (streaming, ...)? At least for now we only need one, so merged key bits of Readers into one. +case class JsonSource(js: CharSequence): + + private var i = 0 + val max = js.length + + // Navigation... + // ======================================================= + + def pos = i + + inline def here = js.charAt(i) + + inline def backspace() = i -= 1 + + inline def revertToPos(p: Int) = i = p + inline def captureMark(mark: Int): String = js.subSequence(mark, i).toString + + @tailrec + final def readToken(): Char = + if i == max then throw new JsonParseError("Unexpected end of buffer", this) + else + val b = here + i += 1 + if !(b == ' ' || b == '\n' || b == '\t' || (b | 0x4) == '\r') then b + else readToken() + + @tailrec + final def expectToken(t: Char): Unit = + if i == max then throw new JsonParseError("Unexpected end of buffer", this) + else + val b = here + i += 1 + if !(b == ' ' || b == '\n' || b == '\t' || (b | 0x4) == '\r') then + if b != t then + backspace() + throw JsonParseError(s"Expected '$t' here", this) + else () + else expectToken(t) + + def expectNull(): Boolean = + if readToken() == 'n' then + readChars(JsonSource.ull, "null") + true + else + backspace() + false + + // Enum... + // ======================================================= + def expectEnum(): Int | String = + readToken() match + case t if t >= '0' && t <= '9' => + backspace() + expectInt() + case t if t == '"' => + val endI = parseString(i) + val str = js.subSequence(i, endI).toString + i = endI + 1 + str + case t if t == 'n' => + readChars(JsonSource.ull, "null") + null.asInstanceOf[String] + case _ => + throw JsonParseError("Expected valid enumeration value or null here", this) + + // Object... + // ======================================================= + + // returns false if 'null' found + def expectFirstObjectField(fieldNameMatrix: StringMatrix): Option[Int] = + val t = readToken() + if t == '{' then + val tt = readToken() + if tt == '"' then + val foundIndex = parseObjectKey(fieldNameMatrix) + Some(foundIndex) + else if tt == '}' then None + else throw new JsonParseError(s"Expected object field name or '}' but found '$tt'", this) + else if t == 'n' then + readChars(JsonSource.ull, "null") + null + else + backspace() + throw new JsonParseError(s"Expected object start '{' or null", this) + + def expectObjectField(fieldNameMatrix: StringMatrix): Option[Int] = + val t = readToken() + if t == ',' then + val tt = readToken() + if tt == '"' then + val foundIndex = parseObjectKey(fieldNameMatrix) + Some(foundIndex) + else throw new JsonParseError(s"Expected object field name but found '$tt'", this) + else if t == '}' then None + else + backspace() + throw new JsonParseError(s"Expected ',' or '}' but found '$t'", this) + + final def parseObjectKey(fieldNameMatrix: StringMatrix): Int = // returns index of field name or -1 if not found + var fi: Int = 0 + var bs: Long = fieldNameMatrix.initial + var c: Int = here + while c != '"' do { + bs = fieldNameMatrix.update(bs, fi, c) + fi += 1 + i += 1 + c = here + } + i += 1 + bs = fieldNameMatrix.exact(bs, fi) + if readToken() != ':' then throw new JsonParseError(s"Expected ':' field separator but found $here", this) + val ret = fieldNameMatrix.first(bs) + ret + + @tailrec + final def parseMap[K, V](kf: () => K, vf: () => V, acc: Map[K, V], isFirst: Boolean = true): Map[K, V] = // initial '{' already consumed + readToken() match + case '}' => acc + case ',' => + val key = kf() + expectToken(':') + val value = vf() + parseMap[K, V](kf, vf, acc + (key -> value), false) + case _ if isFirst => + backspace() + val key = kf() + expectToken(':') + val value = vf() + parseMap[K, V](kf, vf, acc + (key -> value), false) + case c => throw JsonParseError(s"Expected either object end '}' or field separator ',' here but got '$c'", this) + + @tailrec + final def findObjectField(fieldName: String): Option[String] = + val mark = i + expectToken('{') + readToken() match + case '}' => + i = mark + None + case '"' => + val endI = parseString(i) + val str = js.subSequence(i, endI).toString + i = endI + 1 + expectToken(':') + if str == fieldName then + val found = Some(expectString()) + i = mark + found + else + skipValue() + if readToken() == '}' then backspace() // else consume ',' + findObjectField(fieldName) + case t => + backspace() + throw JsonParseError(s"Expected either string start '\"' or object end '}' but got '$t'", this) + + // Array and Tuple... + // ======================================================= + + @tailrec + final private def addAllArray[E](s: scala.collection.mutable.ListBuffer[E], f: () => E, isFirst: Boolean): scala.collection.mutable.ListBuffer[E] = + if i == max then throw JsonParseError("Unexpected end of buffer", this) + val tt = readToken() + if tt == ']' then s + else if !isFirst && tt != ',' then throw JsonParseError(s"Expected ',' or ']' got '$tt'", this) + else + if isFirst then backspace() + s.addOne(f()) + addAllArray(s, f, false) + + def expectArray[E](f: () => E): scala.collection.mutable.ListBuffer[E] = + val t = readToken() + if t == '[' then + val seq = scala.collection.mutable.ListBuffer.empty[E] + addAllArray(seq, f, true) + seq + else if t == 'n' then + readChars(JsonSource.ull, "null") + null + else throw JsonParseError(s"Expected array start '[' or null but got '$t'", this) + + // String... + // ======================================================= + + // Value might be null! + // expectString() will look for leading '"'. parseString() presumes the '"' has already been consumed. + inline def expectString(): String = + val mark = i + val t = readToken() + if t == '"' then + val endI = parseString(i) + if endI >= 0 then + val str = js.subSequence(i, endI).toString + i = endI + 1 + str + else // slower-parseString looking for escaped special chars + val buf = FastStringBuilder() + expectEncodedString(buf) + buf.result + else if t == 'n' then + readChars(JsonSource.ull, "null") + null + else + i = mark + throw new JsonParseError(s"Expected a String value but got '$t'", this) + + @tailrec + final private def expectEncodedString(buf: FastStringBuilder): Unit = + readChar() match + case '"' => () // done + case '\\' => + readChar() match + case '"' => + buf.append('\"') + expectEncodedString(buf) + case '\\' => + buf.append('\\') + expectEncodedString(buf) + case 'b' => + buf.append('\b') + expectEncodedString(buf) + case 'f' => + buf.append('\f') + expectEncodedString(buf) + case 'n' => + buf.append('\n') + expectEncodedString(buf) + case 'r' => + buf.append('\r') + expectEncodedString(buf) + case 't' => + buf.append('\t') + expectEncodedString(buf) + case 'u' => + val hexEncoded = js.subSequence(i, i + 4) + i = i + 4 + val unicodeChar = Integer.parseInt(hexEncoded.toString, 16).toChar + buf.append(unicodeChar.toString) + expectEncodedString(buf) + case c => + buf.append(c) + expectEncodedString(buf) + case c => + buf.append(c) + expectEncodedString(buf) + + def expectString[T](parseFn: String => T): T = + expectString() match + case s: String => parseFn(s) + case null => null.asInstanceOf[T] + + @tailrec + final def parseString(pos: Int): Int = + if pos + 3 < max then // Based on SWAR routine of JSON string parsing: https://github.com/sirthias/borer/blob/fde9d1ce674d151b0fee1dd0c2565020c3f6633a/core/src/main/scala/io/bullet/borer/json/JsonParser.scala#L456 + val bs = (js.charAt(pos)) | (js.charAt(pos + 1) << 8) | (js.charAt(pos + 2) << 16) | js.charAt(pos + 3) << 24 + val mask = ((bs - 0x20202020 ^ 0x3c3c3c3c) - 0x1010101 | (bs ^ 0x5d5d5d5d) + 0x1010101) & 0x80808080 + if mask != 0 then { + val offset = java.lang.Integer.numberOfTrailingZeros(mask) >> 3 + if (bs >> (offset << 3)).toByte == '"' then pos + offset + else -1 // special char found + } else parseString(pos + 4) + else if pos == max then throw new Exception("Unterminated string value") + else + val b = js.charAt(pos) + if b == '"' then pos + else if (b - 0x20 ^ 0x3c) <= 0 then -1 // special char found + else parseString(pos + 1) + + // Boolean... + // ======================================================= + + def expectBoolean(): Boolean = + skipWS() + val bs = (js.charAt(pos)) | (js.charAt(pos + 1) << 8) | (js.charAt(pos + 2) << 16) | js.charAt(pos + 3) << 24 + if (bs ^ JsonSource.trueBytes) == 0 then + i += 4 + true + else if pos + 4 < max && ((bs | js.charAt(pos + 4)) ^ JsonSource.falseBytes) == 0 then + i += 5 + false + else throw JsonParseError(s"Expected 'true' or 'false'", this) + + // Java Boolean can be null + def expectJavaBoolean(): java.lang.Boolean = + skipWS() + val bs = (js.charAt(pos)) | (js.charAt(pos + 1) << 8) | (js.charAt(pos + 2) << 16) | js.charAt(pos + 3) << 24 + if (bs ^ JsonSource.trueBytes) == 0 then + i += 4 + java.lang.Boolean.TRUE + else if pos + 4 < max && ((bs | js.charAt(pos + 4)) ^ JsonSource.falseBytes) == 0 then + i += 5 + java.lang.Boolean.FALSE + else if readChar() == 'n' then + readChars(JsonSource.ull, "null") + null.asInstanceOf[java.lang.Boolean] + else + backspace() + throw JsonParseError(s"Expected 'true', 'false', or null here", this) + + // Characters... + // ======================================================= + + private var c: Char = 0 + inline def readChar(): Char = + if i < max then + c = here + i += 1 + c + else BUFFER_EXCEEDED + + inline def readChars( + expect: Array[Char], + errMsg: String + ): Unit = + var i: Int = 0 + while i < expect.length do + if readChar() != expect(i) then throw JsonParseError(s"Expected $errMsg", this) + i += 1 + + inline def expectChar(): Char = + expectString() match { + case s if s.length == 1 => s.charAt(0) + case s => throw new JsonParseError(s"Expected a Char value but got '$s'", this) + } + + // Numbers... + // ======================================================= + + @inline private def isNumber(c: Char): Boolean = + (c: @switch) match + case '+' | '-' | '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' | '.' | 'e' | 'E' => true + case _ => false + + def expectFloat(): Float = + val result = UnsafeNumbers.float_(this, false, 32) + backspace() + result + + def expectDouble(): Double = + val result = UnsafeNumbers.double_(this, false, 64) + backspace() + result + + def expectNumberOrNull(): String = + skipWS() + val mark = i + skipNumber() + if i != mark then captureMark(mark) + else if readChar() == 'n' then + readChars(JsonSource.ull, "null") + null + else + backspace() + throw new JsonParseError("Expected a numerical value or null here", this) + + def expectInt(): Int = + var b = readToken() + var s = -1 + if b == '-' then + b = readChar() + s = 0 + if b < '0' || b > '9' then + backspace() + throw JsonParseError("Non-numeric character found when integer value expected", this) + var x = '0' - b + while { b = readChar(); b >= '0' && b <= '9' } do + if x < -214748364 || { + x = x * 10 + ('0' - b) + x > 0 + } + then throw JsonParseError("Integer value overflow", this) + x ^= s + x -= s + if (s & x) == -2147483648 then throw JsonParseError("Integer value overflow", this) + if (b | 0x20) == 'e' || b == '.' then + backspace() + throw JsonParseError("Decimal digit 'e' or '.' found when integer value expected", this) + backspace() + x + + def expectLong(): Long = + val result = UnsafeNumbers.long_(this, false) + backspace() + result + + // Skip things... + // ======================================================= + + inline def skipWS(): Unit = + while (here == ' ' || here == '\n' || (here | 0x4) == '\r' || here == '\t') && i < max do i += 1 + if i == max then throw new JsonParseError("Unexpected end of buffer", this) + + def skipValue(): Unit = + (readToken(): @switch) match { + case 'n' => readChars(JsonSource.ull, "null") + case 'f' => readChars(JsonSource.alse, "false") + case 't' => readChars(JsonSource.rue, "true") + case '{' => skipObjectValue() + case '[' => skipArrayValue() + case '"' => + i += 1 + val endI = parseString(i) + i = endI + 1 + case '-' | '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' | '.' => + skipNumber() + case c => throw JsonParseError(s"Unexpected '$c'", this) + } + + @tailrec + final def skipNumber(): Unit = + if !isNumber(readChar()) then backspace() + else skipNumber() + + @tailrec + final def skipArrayValue(k: Int = 0): Unit = + readChar() match + case ']' if k == 0 => () + case '"' => + i = parseString(i) + 1 + skipArrayValue(k) + case ']' => skipArrayValue(k - 1) + case '[' => skipArrayValue(k + 1) + case _ => skipArrayValue(k) + + @tailrec + final def skipObjectValue(k: Int = 0): Unit = + readChar() match + case '}' if k == 0 => () + case '"' => + i = parseString(i) + 1 + skipObjectValue(k) + case '}' => skipObjectValue(k - 1) + case '{' => skipObjectValue(k + 1) + case _ => skipObjectValue(k) diff --git a/src/main/scala/co.blocke.scalajack/json/writing/AnyWriter.scala b/src/main/scala/co.blocke.scalajack/json/writing/AnyWriter.scala new file mode 100644 index 00000000..95a478e6 --- /dev/null +++ b/src/main/scala/co.blocke.scalajack/json/writing/AnyWriter.scala @@ -0,0 +1,157 @@ +package co.blocke.scalajack +package json +package writing + +import co.blocke.scala_reflection.{RType, TypedName} +import co.blocke.scala_reflection.rtypes.* +import scala.jdk.CollectionConverters.* +import org.apache.commons.text.StringEscapeUtils +import org.apache.commons.lang3.text.translate.CharSequenceTranslator +import scala.util.* +import scala.quoted.* + +/** Writing an Any-typed value is a hot mess. You don't know until runtime exactly what the target object will actually + * be, so all the compile-time macro magic will be useless. It is 100% runtime, so it will be much slower as well. + * Basically straightforward things (which is 80-90% of what people do) should work fine. If you have a case that + * misbehaves, my best advice is to use a more specific type. As a rule, it's a much better design choice anyway! + */ + +object AnyWriter: + + def writeAny(target: Any, out: JsonOutput, cfg: SJConfig, inTuple: Boolean = false): Unit = + // val rt = RType.of(target.getClass) + target match + case null => out.burpNull() + case v: BigDecimal => out.value(v) + case v: BigInt => out.value(v) + case v: Boolean => out.value(v) + case v: Byte => out.value(v) + case v: Char => out.value(v) + case v: Double => out.value(v) + case v: Float => out.value(v) + case v: Int => out.value(v) + case v: Long => out.value(v) + case v: Short => out.value(v) + case v: String => out.value(StringEscapeUtils.escapeJson(v)) + case v: java.lang.Boolean => out.value(v) + case v: java.lang.Byte => out.value(v) + case v: java.lang.Character => out.value(v) + case v: java.lang.Double => out.value(v) + case v: java.lang.Float => out.value(v) + case v: java.lang.Integer => out.value(v) + case v: java.lang.Long => out.value(v) + case v: java.lang.Short => out.value(v) + case v: java.lang.Number => out.value(v) + case v: java.time.Duration => out.value(v) + case v: java.time.Instant => out.value(v) + case v: java.time.LocalDate => out.value(v) + case v: java.time.LocalDateTime => out.value(v) + case v: java.time.LocalTime => out.value(v) + case v: java.time.MonthDay => out.value(v) + case v: java.time.OffsetDateTime => out.value(v) + case v: java.time.OffsetTime => out.value(v) + case v: java.time.Period => out.value(v) + case v: java.time.Year => out.value(v) + case v: java.time.YearMonth => out.value(v) + case v: java.time.ZoneOffset => out.value(v) + case v: java.time.ZonedDateTime => out.value(v) + case v: java.time.ZoneId => out.value(v) + case v: java.util.UUID => out.value(v) + + case v: Array[?] => + out.startArray() + v.map(e => writeAny(e, out, cfg)) + out.endArray() + + case v: Set[?] => + out.startArray() + v.map(e => writeAny(e, out, cfg)) + out.endArray() + + case v: Seq[?] => + out.startArray() + v.map(e => writeAny(e, out, cfg)) + out.endArray() + + case v: Map[?, ?] => + out.startObject() + v.map { case (k, v) => _okToWrite(k.toString, v, out, cfg) } + out.endObject() + + case v: Option[?] => + v match + case None => + if inTuple then out.burpNull() + else () + case Some(v2) => writeAny(v2, out, cfg) + + case v: Either[?, ?] => + v match + case Left(_) => + if inTuple then out.burpNull() + else () + case Right(v2) => writeAny(v2, out, cfg) + + case v: Try[?] => + v match + case Failure(_) => + if inTuple then out.burpNull() + else () + case Success(v2) => writeAny(v2, out, cfg) + + case v: Tuple => + val varr = v.toArray + out.startArray() + varr.foreach(v2 => writeAny(v2, out, cfg, true)) + out.endArray() + + case v => + val rt = RType.of(v.getClass) + rt match + case t: ScalaClassRType[?] => + out.startObject() + t.fields.map(f => + val field = f.asInstanceOf[ScalaFieldInfo] + val m = v.getClass.getMethod(field.name) + m.setAccessible(true) + val fieldValue = m.invoke(v) + val fieldName = f.annotations.get("co.blocke.scalajack.Change").flatMap(_.get("name")).getOrElse(f.name) + _okToWrite(fieldName, fieldValue, out, cfg) + ) + out.endObject() + case _ => throw new JsonUnsupportedType("Class " + v.getClass.getName + " not supported for Any type") + + // Called by non-Any classes (in JsonCodecMaker) that have Any-typed fields + def isOkToWrite(prefix: Expr[Unit], value: Expr[Any], out: Expr[JsonOutput], cfg: SJConfig)(using Quotes): Expr[Unit] = + import quotes.reflect.* + '{ + _okToWrite($value, ${ Expr(cfg) }).map { v => + $prefix + AnyWriter.writeAny(v, $out, ${ Expr(cfg) }) + } + } + + // Called for Any-typed classes + private def _okToWrite(label: String, value: Any, out: JsonOutput, cfg: SJConfig): Unit = + _okToWrite(value, cfg).map { v => + out.label(label) + writeAny(v, out, cfg) + } + + private def _okToWrite(value: Any, cfg: SJConfig): Option[Any] = + value match + case None => if cfg.noneAsNull then Some(null) else None + case Failure(e) => + cfg.tryFailureHandling match + case TryPolicy.AS_NULL => Some(null) + case TryPolicy.ERR_MSG_STRING => Some("Try Failure with msg: " + e.getMessage()) + case TryPolicy.THROW_EXCEPTION => throw e + + case Left(v) => + cfg.eitherLeftHandling match + case EitherLeftPolicy.AS_VALUE => Some(v) + case EitherLeftPolicy.AS_NULL => Some("null") + case EitherLeftPolicy.ERR_MSG_STRING => Some("Left Error: " + v.toString) + case EitherLeftPolicy.THROW_EXCEPTION => throw new JsonEitherLeftError("Left Error: " + v.toString) + case Some(v) => _okToWrite(v, cfg) + case _ => Some(value) diff --git a/src/main/scala/co.blocke.scalajack/json/writing/JsonOutput.scala b/src/main/scala/co.blocke.scalajack/json/writing/JsonOutput.scala new file mode 100644 index 00000000..b1417626 --- /dev/null +++ b/src/main/scala/co.blocke.scalajack/json/writing/JsonOutput.scala @@ -0,0 +1,528 @@ +package co.blocke.scalajack +package json +package writing + +import java.time.format.DateTimeFormatter.* + +/** Wrapper around a (Fast)StringBuilder that offers support for primitive types, + * including quotes-wrapping those that need it for use in Map keys (aka stringified). + * Handles dangling commas/separators, plus mark & revert capability. It is reusable + * via clear() for speed -- one per thread, of course! + */ +case class JsonOutput(): + val internal: FastStringBuilder = new FastStringBuilder() + + private var comma: Boolean = false + private var savePoint: Int = 0 + private var saveComma: Boolean = false + + def result = internal.result + + def clear() = + internal.clear() + comma = false + savePoint = 0 + saveComma = false + this + + def mark() = + savePoint = internal.length + saveComma = comma + + def revert() = // delete everything after the set savePoint + internal.setLength(savePoint) + comma = saveComma + + inline def startObject(): Unit = + maybeComma() + internal.append('{') + comma = false + + inline def endObject(): Unit = + internal.append('}') + comma = true + + inline def startArray(): Unit = + maybeComma() + internal.append('[') + comma = false + + inline def endArray(): Unit = + internal.append(']') + comma = true + + inline def colon(): Unit = + internal.append(':') + comma = false + + inline def maybeComma(): Unit = + if comma then internal.append(',') + comma = false + + inline def burpNull(): Unit = + maybeComma() + internal.append("null") + comma = true + + // Problem: for unions, if left fails to write, comma is not reset to true for the attempt to write right side + inline def label(s: String): Unit = + maybeComma() + internal.append('"') + internal.append(s) + internal.append('"') + internal.append(':') + + // ----------------------- Primitive/Simple type support + + inline def value(v: scala.math.BigDecimal): Unit = + maybeComma() + if v == null then internal.append("null") + else internal.append(v) + comma = true + + // Note: data types that are not naturally quotes-wrapped have "Stringified" variants + // (saparate vs a param for speed), for use in Map/Json object keys. + inline def valueStringified(v: scala.math.BigDecimal): Unit = + maybeComma() + if v == null then throw new JsonNullKeyValue("Key values may not be null") + else + internal.append('"') + internal.append(v.toString) + internal.append('"') + comma = true + + inline def value(v: scala.math.BigInt): Unit = + maybeComma() + if v == null then internal.append("null") + else internal.append(v.toString) + comma = true + + inline def valueStringified(v: scala.math.BigInt): Unit = + maybeComma() + if v == null then throw new JsonNullKeyValue("Key values may not be null") + else + internal.append('"') + internal.append(v.toString) + internal.append('"') + comma = true + + inline def value(v: java.math.BigDecimal): Unit = + maybeComma() + if v == null then internal.append("null") + else internal.append(v.toString) + comma = true + + inline def valueStringified(v: java.math.BigDecimal): Unit = + maybeComma() + if v == null then throw new JsonNullKeyValue("Key values may not be null") + else + internal.append('"') + internal.append(v.toString) + internal.append('"') + comma = true + + inline def value(v: java.math.BigInteger): Unit = + maybeComma() + if v == null then internal.append("null") + else internal.append(v.toString) + comma = true + + inline def valueStringified(v: java.math.BigInteger): Unit = + maybeComma() + if v == null then throw new JsonNullKeyValue("Key values may not be null") + else + internal.append('"') + internal.append(v.toString) + internal.append('"') + comma = true + + inline def value(v: Boolean): Unit = + maybeComma() + internal.append(v) + comma = true + + inline def valueStringified(v: Boolean): Unit = + maybeComma() + internal.append('"') + internal.append(v.toString) + internal.append('"') + comma = true + + inline def value(v: Byte): Unit = + maybeComma() + internal.append(v.toInt) + comma = true + + inline def valueStringified(v: Byte): Unit = + maybeComma() + internal.append('"') + internal.append(v.toInt) + internal.append('"') + comma = true + + inline def value(v: Char): Unit = + maybeComma() + internal.append('"') + internal.append(v) + internal.append('"') + comma = true + + inline def value(v: Double): Unit = + maybeComma() + internal.append(v) + comma = true + + inline def valueStringified(v: Double): Unit = + maybeComma() + internal.append('"') + internal.append(v) + internal.append('"') + comma = true + + inline def value(v: Float): Unit = + maybeComma() + internal.append(v) + comma = true + + inline def valueStringified(v: Float): Unit = + maybeComma() + internal.append('"') + internal.append(v) + internal.append('"') + comma = true + + inline def value(v: Int): Unit = + maybeComma() + internal.append(v) + comma = true + + inline def valueSringified(v: Int): Unit = + maybeComma() + internal.append('"') + internal.append(v) + internal.append('"') + comma = true + + inline def value(v: Long): Unit = + maybeComma() + internal.append(v) + comma = true + + inline def valueStringified(v: Long): Unit = + maybeComma() + internal.append('"') + internal.append(v) + internal.append('"') + comma = true + + inline def value(v: Short): Unit = + maybeComma() + internal.append(v) + comma = true + + inline def valueStringified(v: Short): Unit = + maybeComma() + internal.append('"') + internal.append(v) + internal.append('"') + comma = true + + inline def value(v: String): Unit = + maybeComma() + if v == null then internal.append("null") + else + internal.append('"') + internal.append(v) + internal.append('"') + comma = true + + inline def valueEscaped(v: String): Unit = + maybeComma() + if v == null then internal.append("null") + else + internal.append('"') + internal.appendEscaped(v, 0, v.length) + internal.append('"') + comma = true + + inline def value(v: java.lang.Boolean): Unit = + maybeComma() + if v == null then internal.append("null") + else internal.append(v) + comma = true + + inline def valueStringified(v: java.lang.Boolean): Unit = + maybeComma() + if v == null then internal.append("null") + else + internal.append('"') + internal.append(v) + internal.append('"') + comma = true + + inline def value(v: java.lang.Byte): Unit = + maybeComma() + if v == null then internal.append("null") + else internal.append(v.toInt) + comma = true + + inline def valueStringified(v: java.lang.Byte): Unit = + maybeComma() + if v == null then internal.append("null") + else + internal.append('"') + internal.append(v.toInt) + internal.append('"') + comma = true + + inline def value(v: java.lang.Character): Unit = + maybeComma() + if v == null then internal.append("null") + else + internal.append('"') + internal.append(v) + internal.append('"') + comma = true + + inline def value(v: java.lang.Double): Unit = + maybeComma() + if v == null then internal.append("null") + else internal.append(v) + comma = true + + inline def valueStringified(v: java.lang.Double): Unit = + maybeComma() + if v == null then internal.append("null") + else + internal.append('"') + internal.append(v) + internal.append('"') + comma = true + + inline def value(v: java.lang.Float): Unit = + maybeComma() + if v == null then internal.append("null") + else internal.append(v) + comma = true + + inline def valueStringified(v: java.lang.Float): Unit = + maybeComma() + if v == null then internal.append("null") + else + internal.append('"') + internal.append(v) + internal.append('"') + comma = true + + inline def value(v: java.lang.Integer): Unit = + maybeComma() + if v == null then internal.append("null") + else internal.append(v) + comma = true + + inline def valueStringified(v: java.lang.Integer): Unit = + maybeComma() + if v == null then internal.append("null") + else + internal.append('"') + internal.append(v) + internal.append('"') + comma = true + + inline def value(v: java.lang.Long): Unit = + maybeComma() + if v == null then internal.append("null") + else internal.append(v) + comma = true + + inline def valueStringified(v: java.lang.Long): Unit = + maybeComma() + if v == null then internal.append("null") + else + internal.append('"') + internal.append(v) + internal.append('"') + comma = true + + inline def value(v: java.lang.Short): Unit = + maybeComma() + if v == null then internal.append("null") + else internal.append(v) + comma = true + + inline def valueStringified(v: java.lang.Short): Unit = + maybeComma() + if v == null then internal.append("null") + else + internal.append('"') + internal.append(v) + internal.append('"') + comma = true + + inline def value(v: java.lang.Number): Unit = + maybeComma() + if v == null then internal.append("null") + else internal.append(v) + comma = true + + inline def valueStringified(v: java.lang.Number): Unit = + maybeComma() + if v == null then internal.append("null") + else + internal.append('"') + internal.append(v) + internal.append('"') + comma = true + + inline def value(v: java.time.Duration): Unit = + maybeComma() + if v == null then internal.append("null") + else + internal.append('"') + internal.append(v.toString) + internal.append('"') + comma = true + + inline def value(v: java.time.Instant): Unit = + maybeComma() + if v == null then internal.append("null") + else + internal.append('"') + internal.append(v.toString) + internal.append('"') + comma = true + + inline def value(v: java.time.LocalDate): Unit = + maybeComma() + if v == null then internal.append("null") + else + internal.append('"') + internal.append(v.format(ISO_LOCAL_DATE)) + internal.append('"') + comma = true + + inline def value(v: java.time.LocalDateTime): Unit = + maybeComma() + if v == null then internal.append("null") + else + internal.append('"') + internal.append(v.format(ISO_LOCAL_DATE_TIME)) + internal.append('"') + comma = true + + inline def value(v: java.time.LocalTime): Unit = + maybeComma() + if v == null then internal.append("null") + else + internal.append('"') + internal.append(v.format(ISO_LOCAL_TIME)) + internal.append('"') + comma = true + + inline def value(v: java.time.MonthDay): Unit = + maybeComma() + if v == null then internal.append("null") + else + internal.append('"') + internal.append(v.toString) + internal.append('"') + comma = true + + inline def value(v: java.time.OffsetDateTime): Unit = + maybeComma() + if v == null then internal.append("null") + else + internal.append('"') + internal.append(v.format(ISO_OFFSET_DATE_TIME)) + internal.append('"') + comma = true + + inline def value(v: java.time.OffsetTime): Unit = + maybeComma() + if v == null then internal.append("null") + else + internal.append('"') + internal.append(v.format(ISO_OFFSET_TIME)) + internal.append('"') + comma = true + + inline def value(v: java.time.Period): Unit = + maybeComma() + if v == null then internal.append("null") + else + internal.append('"') + internal.append(v.toString) + internal.append('"') + comma = true + + inline def value(v: java.time.Year): Unit = + maybeComma() + if v == null then internal.append("null") + else + internal.append('"') + internal.append(v.toString) + internal.append('"') + comma = true + + inline def value(v: java.time.YearMonth): Unit = + maybeComma() + if v == null then internal.append("null") + else + internal.append('"') + internal.append(v.toString) + internal.append('"') + comma = true + + inline def value(v: java.time.ZonedDateTime): Unit = + maybeComma() + if v == null then internal.append("null") + else + internal.append('"') + internal.append(v.format(ISO_ZONED_DATE_TIME)) + internal.append('"') + comma = true + + inline def value(v: java.time.ZoneId): Unit = + maybeComma() + if v == null then internal.append("null") + else + internal.append('"') + internal.append(v.toString) + internal.append('"') + comma = true + + inline def value(v: java.time.ZoneOffset): Unit = + maybeComma() + if v == null then internal.append("null") + else + internal.append('"') + internal.append(v.toString) + internal.append('"') + comma = true + + inline def value(v: java.net.URL): Unit = + maybeComma() + if v == null then internal.append("null") + else + internal.append('"') + internal.append(v.toString) + internal.append('"') + comma = true + + inline def value(v: java.net.URI): Unit = + maybeComma() + if v == null then internal.append("null") + else + internal.append('"') + internal.append(v.toString) + internal.append('"') + comma = true + + inline def value(v: java.util.UUID): Unit = + maybeComma() + if v == null then internal.append("null") + else + internal.append('"') + internal.append(v.toString) + internal.append('"') + comma = true diff --git a/src/main/scala/co.blocke.scalajack/package.scala b/src/main/scala/co.blocke.scalajack/package.scala new file mode 100644 index 00000000..6b0e9423 --- /dev/null +++ b/src/main/scala/co.blocke.scalajack/package.scala @@ -0,0 +1,25 @@ +package co.blocke.scalajack + +inline def lastPart(n: String) = n.split('.').last.stripSuffix("$") +inline def allButLastPart(n: String) = + val l = n.lastIndexOf('.') + if l >= 0 then n.substring(0, l) + else n + +val random = new scala.util.Random() +def scramble(hash: Int): String = + val last5 = f"$hash%05d".takeRight(5) + val digits = (1 to 5).map(_ => random.nextInt(10)) + if digits(0) % 2 == 0 then s"${last5(0)}${digits(0)}${last5(1)}${digits(1)}${last5(2)}-${digits(2)}${last5(3)}${digits(3)}-${last5(4)}${digits(4)}A" + else s"${digits(0)}${last5(0)}${digits(1)}${last5(1)}${digits(2)}-${last5(2)}${digits(3)}${last5(3)}-${digits(4)}${last5(4)}B" + +def descramble(in: String, hash: Int): Boolean = + val last5 = f"$hash%05d".takeRight(5) + in.last match + case 'A' if in.length == 13 => "" + in(0) + in(2) + in(4) + in(7) + in(10) == last5 + case 'B' if in.length == 13 => "" + in(1) + in(3) + in(6) + in(8) + in(11) == last5 + case _ => false + +enum Language { + case Scala, Java +} diff --git a/src/main/scala/co.blocke.scalajack/run/Play.scalax b/src/main/scala/co.blocke.scalajack/run/Play.scalax new file mode 100644 index 00000000..6df3623e --- /dev/null +++ b/src/main/scala/co.blocke.scalajack/run/Play.scalax @@ -0,0 +1,15 @@ +package co.blocke.scalajack +package json +package run + +import ScalaJack.* + +case class Well(repo: scala.collection.mutable.Map[String, Int] = scala.collection.mutable.Map.empty[String, Int]) + +object RunMe extends App: + + import ScalaJack.* + + given sj: ScalaJack[Pet] = sjCodecOf[Pet] + + println("ok") diff --git a/src/main/scala/co.blocke.scalajack/run/Record.scalax b/src/main/scala/co.blocke.scalajack/run/Record.scalax new file mode 100644 index 00000000..10fe5db8 --- /dev/null +++ b/src/main/scala/co.blocke.scalajack/run/Record.scalax @@ -0,0 +1,185 @@ +package co.blocke.scalajack +package json +package run + +import neotype.* +import co.blocke.scalajack.json.schema.* + +@additionalProperties(value = "true") +@id(value = "abc123") +@title(value = "The Greatest") +@description(value = "nothing special") +case class Person( + name: String = "Greg", + age: Int = 57, + address: Address = Address("123 Main", "New York", "NY", "12345"), + email: Option[String] = Some("wow.gmail.com"), + phone_numbers: List[String] = List("a", "b"), + is_employed: Boolean = true +) + +case class Amorphous(thing: Any) + +case class Address( + street: String, + city: String, + state: String, + postal_code: String +) + +case class Friend( + name: String, + age: Int, + email: String +) + +case class Pet( + name: String, + species: String, + age: Int +) + +case class Record( + person: Person, + hobbies: List[String], + friends: List[Friend], + pets: List[Pet] +) + +case class Empl(name: String, age: Int) +// case class Foo(a: Empl, b: List[Empl]) +case class Blah(a: String, b: String | Option[Int]) +/* +case class ArrayHolder[T](a: Array[T]) + +// case class Foo(name: String, maybe: Option[Int], age: Int, expected: String = "nada", gotit: Option[Int] = Some(5)) +case class Foo(name: String, a: Animal, other: Option[Foo], color: Color, expected: String = "nada") +// case class Foo(name: String, age: Int, expected: String = "nada") + +enum Color: + case Red, Blue, Green + */ + +case class Foo(name: String, a: Animal, x: (String, Seq[Boolean])) +sealed abstract class Animal +@TypeHint(hintValue = "bow-wow") +case class Dog(name: String, numLegs: Int) extends Animal +@TypeHint(hintValue = "flippy") +case class Fish(name: String, @Change(name = "fresh") isFreshwater: Option[Boolean]) extends Animal + +type NonEmptyString = NonEmptyString.Type +given NonEmptyString: Newtype[String] with + override inline def validate(input: String): Boolean = + input.nonEmpty + +type NonEmptyList = NonEmptyList.Type +given NonEmptyList: Newtype[List[Int]] with + override inline def validate(input: List[Int]): Boolean = + input.nonEmpty + +type NonZeroInt = NonZeroInt.Type +object NonZeroInt extends Newtype[Int]: + override inline def validate(input: Int): Boolean = + input != 0 + +type XList = XList.Type +given XList: Newtype[List[String]] with + override inline def validate(input: List[String]): Boolean = + input(0) == "x" + +case class Validated(name: NonEmptyString, xspot: XList, nada: EmptyString) + +// case class Tag[X](a: X) +// given [A, B](using newType: Newtype.WithType[A, B], tag: Tag[A]): Tag[B] = +// newType.unsafeWrapF(tag) + +type EmptyString = EmptyString.Type +given EmptyString: Newtype[String] with + override inline def validate(input: String): Boolean = + input.isEmpty + +case class Person2(age: XList) + +case class Foom(a: schema.Schema) + +sealed trait Candy: + val isSweet: Boolean +case class MMs(isSweet: Boolean) extends Candy +case class Musk(isSweet: Boolean) extends Candy +case class Veggies(yuks: String) + +type Food = Candy | Veggies + +case class JJ(a: java.util.ArrayList[Int]) + +val jsData = + """{ + "person": { + "name": "John Doe", + "age": 30, + "address": { + "street": "123 Main Street", + "city": "Anytown", + "state": "CA", + "postal_code": "12345" + }, + "email": "john.doe@example.com", + "phone_numbers": [ + "555-555-5555", + "555-123-4567" + ], + "is_employed": true + }, + "hobbies": [ + "reading", + "swimming", + "traveling" + ], + "friends": [ + { + "name": "Jane Smith", + "age": 28, + "email": "jane.smith@example.com" + }, + { + "name": "Bob Johnson", + "age": 32, + "email": "bob.johnson@example.com" + } + ], + "pets": [ + { + "name": "Fido", + "species": "Dog", + "age": 5 + }, + { + "name": "Whiskers", + "species": "Cat", + "age": 3 + } + ] + }""" + +case class Yippy(a: (Int, List[String], Boolean) = (5, List("a", "b"), false), b: Boolean) +sealed trait Flavor +case object Vanilla extends Flavor +case object ChocolateX extends Flavor +case object Bourbon extends Flavor +case class FlavorHolder(f: Flavor) + +enum Colors: + case Red, Green, Blue + +object WeekDay extends Enumeration { + type WeekDay = Value + val Mon, Tue, Wed, Thu, Fri, Sat, Sun = Value +} +import WeekDay.* + +val record = Record( + Person("John Doe", 30, Address("123 Main Street", "Anytown", "CA", "12345"), Some("john.doe@example.com"), List("555-555-5555", "555-123-4567"), true), + List("reading", "swimming", "traveling"), + List(Friend("Jane Smith", 28, "jane.smith@example.com"), Friend("Bob Johnson", 32, "bob.johnson@example.com")), + List(Pet("Fido", "Dog", 5), Pet("Whiskers", "Cat", 3)) +) diff --git a/src/main/scala/co.blocke.scalajack/run/Sample.scalax b/src/main/scala/co.blocke.scalajack/run/Sample.scalax new file mode 100644 index 00000000..1a67dd21 --- /dev/null +++ b/src/main/scala/co.blocke.scalajack/run/Sample.scalax @@ -0,0 +1,47 @@ +package co.blocke.scalajack +package run + +import neotype.* + +// Enumeration sealed trait +sealed trait card extends Enumeration +case object CLUB extends card +case object HEART extends card +case object DIAMOND extends card +case object SPADE extends card + +sealed abstract class msg[X] +@TypeHint +case class Command[T](item: T) extends msg[T] +case class Query[T](item: T) extends msg[T] + +class Wrapper(val underlying: Int) extends AnyVal + +enum Colors: + case Red, Blue, Green + +trait Miss[E] { val x: E } +case class Foom[X](x: X) extends Miss[X] + +// case class Person[Y](name: String, age: Miss[Y], again: Option[Person[Y]]) + +case class Greg(hey: String, age: Int) + +case class Person[T](val name: String, val card: card, val msg: msg[T], meh: Wrapper): + var thingy: String = "wow" + + private var c: Int = 5 + def count: Int = c + def count_=(x: Int) = c = x + +// type NonEmptyString = NonEmptyString.Type +// given NonEmptyString: Newtype[String] with +// inline def validate(input: String): Boolean = +// input.nonEmpty + +// type MinorPerson = MinorPerson.Type +// given MinorPerson: Newtype[Person] with +// inline def validate(input: Person): Boolean = +// input.age < 18 + +// case class SampleNeo(name: NonEmptyString, label: String, unknown: MinorPerson) diff --git a/src/test/java/co/blocke/scalajack/json/SampleClass.java b/src/test/java/co/blocke/scalajack/json/SampleClass.java new file mode 100644 index 00000000..63189e1f --- /dev/null +++ b/src/test/java/co/blocke/scalajack/json/SampleClass.java @@ -0,0 +1,47 @@ +package co.blocke.scalajack.json; + +import co.blocke.scala_reflection.Ignore; + +public class SampleClass { + // Fields + private String name; + private int age; + private String address; + + // Constructors + public SampleClass(){} + + // public SampleClass(String name, int age, String address) { + // this.name = name; + // this.age = age; + // this.address = address; + // } + + // Getter methods + public String getName() { + return name; + } + + @Ignore + public int getAge() { + return age; + } + + public String getAddress() { + return address; + } + + // Setter methods (optional, depending on whether you want to allow modification) + public void setName(String name) { + this.name = name; + } + + public void setAge(int age) { + this.age = age; + } + + public void setAddress(String address) { + this.address = address; + } + +} \ No newline at end of file diff --git a/src/test/java/co/blocke/scalajack/json/collections/CarEnum.java b/src/test/java/co/blocke/scalajack/json/collections/CarEnum.java new file mode 100644 index 00000000..66cf40b6 --- /dev/null +++ b/src/test/java/co/blocke/scalajack/json/collections/CarEnum.java @@ -0,0 +1,7 @@ +package co.blocke.scalajack.json.collections; + +public enum CarEnum{ + TOYOTA, + VW, + PORSCHE +} \ No newline at end of file diff --git a/core/src/test/scala/co.blocke.scalajack/JsonDiff.scala b/src/test/scala/co.blocke.scalajack/JsonDiff.scala similarity index 80% rename from core/src/test/scala/co.blocke.scalajack/JsonDiff.scala rename to src/test/scala/co.blocke.scalajack/JsonDiff.scala index acafcefa..7412ceed 100644 --- a/core/src/test/scala/co.blocke.scalajack/JsonDiff.scala +++ b/src/test/scala/co.blocke.scalajack/JsonDiff.scala @@ -1,25 +1,21 @@ package co.blocke.scalajack package json -import org.json4s.JsonAST.{ JNothing, JObject, JValue } +import org.json4s.JsonAST.{JNothing, JObject, JValue} object JsonDiff { - def compare( - left: JValue, - right: JValue, - leftLabel: String = "left", - rightLabel: String = "right"): Seq[JsonDiff] = { + def compare(left: JValue, right: JValue, leftLabel: String = "left", rightLabel: String = "right"): Seq[JsonDiff] = (left, right) match { case (JObject(leftFields), JObject(rightFields)) => val allFieldNames = (leftFields.map(_._1) ++ rightFields.map(_._1)).distinct allFieldNames.sorted flatMap { fieldName => val leftFieldValue = leftFields - .collectFirst({ case (`fieldName`, fieldValue) => fieldValue }) + .collectFirst { case (`fieldName`, fieldValue) => fieldValue } .getOrElse(JNothing) val rightFieldValue = rightFields - .collectFirst({ case (`fieldName`, fieldValue) => fieldValue }) + .collectFirst { case (`fieldName`, fieldValue) => fieldValue } .getOrElse(JNothing) compare(leftFieldValue, rightFieldValue, leftLabel, rightLabel) } @@ -35,7 +31,7 @@ object JsonDiff { // } case _ => - if (left == right) { + if left == right then { Seq.empty } else { val outerLeft = left @@ -48,7 +44,6 @@ object JsonDiff { }) } } - } } diff --git a/src/test/scala/co.blocke.scalajack/JsonMatcher.scala b/src/test/scala/co.blocke.scalajack/JsonMatcher.scala new file mode 100644 index 00000000..9531db62 --- /dev/null +++ b/src/test/scala/co.blocke.scalajack/JsonMatcher.scala @@ -0,0 +1,28 @@ +package co.blocke.scalajack +package json + +import org.json4s.JsonAST.JValue +import org.json4s.native.JsonMethods.* + +import org.scalatest.* +import matchers.* + +trait JsonMatchers { + class JsonMatchesMatcher(expectedJson: String) extends Matcher[String]: + def apply(srcJson: String) = + val diffs = JsonDiff.compare( + parseJValue(srcJson), + parseJValue(expectedJson), + "expected", + "actual" + ) + MatchResult( + diffs.isEmpty, + s"""JSON values did not match""", + s"""JSON values matched""" + ) + + def matchJson(targetJson: String) = new JsonMatchesMatcher(targetJson) + implicit def parseJValue(string: String): JValue = parse(string) +} +object JsonMatchers extends JsonMatchers diff --git a/core/src/test/scala/co.blocke.scalajack/TestUtil.scala b/src/test/scala/co.blocke.scalajack/TestUtil.scala similarity index 62% rename from core/src/test/scala/co.blocke.scalajack/TestUtil.scala rename to src/test/scala/co.blocke.scalajack/TestUtil.scala index c8365bdf..b8a5d5d5 100644 --- a/core/src/test/scala/co.blocke.scalajack/TestUtil.scala +++ b/src/test/scala/co.blocke.scalajack/TestUtil.scala @@ -1,19 +1,21 @@ package co.blocke.scalajack -import munit.internal.console +// import munit.internal.console object TestUtil { - inline def describe(message: String, color: String = Console.MAGENTA): Unit = println(s"$color$message${Console.RESET}") - inline def pending = describe(" << Test Pending (below) >>", Console.YELLOW) + // inline def describe(message: String, color: String = Console.MAGENTA): Unit = TestingConsole.out.println(s"$color$message${Console.RESET}") + // inline def pending = describe(" << Test Pending (below) >>", Console.YELLOW) + + inline def colorString(str: String, color: String = Console.MAGENTA): String = + str.split("\n").map(s => s"$color$s${Console.RESET}").mkString("\n") def hexStringToByteArray(s: String): Array[Byte] = { val len = s.length val data = new Array[Byte](len / 2) var i = 0 - while ({ - i < len - }) { + while i < len + do { data(i / 2) = ((Character.digit(s.charAt(i), 16) << 4) + Character.digit( s.charAt(i + 1), 16 @@ -26,17 +28,16 @@ object TestUtil { // Utility to generate test code quickly def showException(label: String, fnStr: String, fn: () => Any) = - try { + try fn() - } catch { + catch { case x: IndexOutOfBoundsException => throw x case t: Throwable => - if (!t.getMessage.contains("\n")) - throw t + if !t.getMessage.contains("\n") then throw t val msg = "\"\"\"" + t.getMessage().replace("\n", "\n |") + "\"\"\"" println( label + " >> " + t.getClass.getName + "\n-----------------------\n" + s"val msg = $msg.stripMargin\nthe[${t.getClass.getName}] thrownBy $fnStr should have message msg\n" ) } -} \ No newline at end of file +} diff --git a/src/test/scala/co.blocke.scalajack/json/classes/ClassSpec.scala b/src/test/scala/co.blocke.scalajack/json/classes/ClassSpec.scala new file mode 100644 index 00000000..fdf1811d --- /dev/null +++ b/src/test/scala/co.blocke.scalajack/json/classes/ClassSpec.scala @@ -0,0 +1,172 @@ +package co.blocke.scalajack +package json +package classes + +import ScalaJack.* +import co.blocke.scala_reflection.* +import org.scalatest.funspec.AnyFunSpec +import org.scalatest.matchers.should.Matchers.* +import org.scalatest.* +import scala.util.* +import TestUtil.* + +import java.util.UUID + +class ClassSpec() extends AnyFunSpec with JsonMatchers: + opaque type phone = String + + describe(colorString("-------------------------------\n: Class Tests :\n-------------------------------", Console.YELLOW)) { + it("Simple case class must work (with field renaming)") { + val inst = Person("Bob", 34) + val sj = sjCodecOf[Person] + val js = sj.toJson(inst) + js should matchJson("""{"name":"Bob","duration":34}""") + sj.fromJson(js) shouldEqual (inst) + } + it("Inherited class must work") { + val inst = Child("Bob", 34, 3) + val sj = sjCodecOf[Child] + val js = sj.toJson(inst) + js should matchJson("""{"name":"Bob","age":34,"phase":3}""") + sj.fromJson(js) shouldEqual (inst) + } + it("Non-constructor fields of class must work") { + val inst = Parent(99, List("x", "y")) + inst.hidden_=(true) + inst.nope_=(false) + inst.foo = "we'll see" + val sj = sjCodecOf[Parent](SJConfig.writeNonConstructorFields()) + val js = sj.toJson(inst) + js should matchJson("""{"phase":99,"stuff":["x","y"],"foo":"we'll see","hidden":true}""") + val re = sj.fromJson(js) + re.phase shouldEqual (inst.phase) + re.stuff shouldEqual (inst.stuff) + re.foo shouldEqual (inst.foo) + re.hidden shouldEqual (inst.hidden) + } + it("Block non-constructor fields of class must work") { + val inst = Parent(99, List("x", "y")) + inst.hidden_=(true) + inst.nope_=(false) + val sj = sjCodecOf[Parent] + val js = sj.toJson(inst) + js should matchJson("""{"phase":99,"stuff":["x","y"]}""") + val re = sj.fromJson(js) + re.phase shouldEqual (inst.phase) + re.stuff shouldEqual (inst.stuff) + re.foo shouldEqual ("ok") + re.hidden shouldEqual (false) + } + it("Parameterized class must work") { + val num: phone = "123-456-7890" + val inst = Params(List(Person("Bob", 34), Person("Sarah", 28)), Some(num)) + val sj = sjCodecOf[Params[Person, phone]] + val js = sj.toJson(inst) + js should matchJson("""{"a":[{"name":"Bob","duration":34},{"name":"Sarah","duration":28}],"b":"123-456-7890"}""") + sj.fromJson(js) shouldEqual (inst) + } + it("Sealed abstract class with case objects and case classes must work") { + val inst = AbstractClassHolder(Start2, Fish2("Beta", false), Miami2(101.1)) + val sj = sjCodecOf[AbstractClassHolder] + val js = sj.toJson(inst) + js should matchJson("""{"a":"Start2","b":{"_hint":"Fish2","species":"Beta","freshwater":false},"c":{"_hint":"Miami2","temp":101.1}}""") + val re = sj.fromJson(js) + re.a shouldEqual (inst.a) + re.b shouldEqual (inst.b) + (re.c.asInstanceOf[Miami2].temp == inst.c.asInstanceOf[Miami2].temp) shouldEqual (true) + } + it("Sealed abstract class with modified type hint label must work") { + val inst = AbstractClassHolder(Start2, Fish2("Beta", false), Miami2(101.1)) + val sj = sjCodecOf[AbstractClassHolder](SJConfig.withTypeHintLabel("ref")) + val js = sj.toJson(inst) + js should matchJson("""{"a":"Start2","b":{"ref":"Fish2","species":"Beta","freshwater":false},"c":{"ref":"Miami2","temp":101.1}}""") + val re = sj.fromJson(js) + re.a shouldEqual (inst.a) + re.b shouldEqual (inst.b) + (re.c.asInstanceOf[Miami2].temp == inst.c.asInstanceOf[Miami2].temp) shouldEqual (true) + } + it("Sealed abstract class with type hint policy SCRAMBLE_CLASSNAME label must work") { + val inst = AbstractClassHolder(Start2, Fish2("Beta", false), Miami2(101.1)) + val sj = sjCodecOf[AbstractClassHolder](SJConfig.withTypeHintPolicy(TypeHintPolicy.SCRAMBLE_CLASSNAME)) + val js = sj.toJson(inst) + val diff = parseJValue(js).diff(parseJValue("""{"a":"Start2","b":{"_hint":"82949-049-49A","species":"Beta","freshwater":false},"c":{"_hint":"53150-867-73B","temp":101.1}}""")) + val diffMap = diff.changed.values.asInstanceOf[Map[String, Map[String, ?]]] + assert(diffMap("b").contains("_hint") && diffMap("c").contains("_hint") == true) // ie only the scrambled _hint values are different + val re = sj.fromJson(js) + re.a shouldEqual (inst.a) + re.b shouldEqual (inst.b) + (re.c.asInstanceOf[Miami2].temp == inst.c.asInstanceOf[Miami2].temp) shouldEqual (true) + } + it("Sealed abstract class with type hint policy USE_ANNOTATION label must work") { + val inst = AbstractClassHolder(Start2, Fish2("Beta", false), Miami2(101.1)) + val sj = sjCodecOf[AbstractClassHolder](SJConfig.withTypeHintPolicy(TypeHintPolicy.USE_ANNOTATION)) + val js = sj.toJson(inst) + js should matchJson("""{"a":"Start2","b":{"_hint":"flipper","species":"Beta","freshwater":false},"c":{"_hint":"vice","temp":101.1}}""") + val re = sj.fromJson(js) + re.a shouldEqual (inst.a) + re.b shouldEqual (inst.b) + (re.c.asInstanceOf[Miami2].temp == inst.c.asInstanceOf[Miami2].temp) shouldEqual (true) + } + it("Parameterized sealed abstract class must work") { + val inst = AbstractClassHolder2(Thing2(15L, "wow")) + val sj = sjCodecOf[AbstractClassHolder2[Long]] + val js = sj.toJson(inst) + js should matchJson("""{"a":{"_hint":"Thing2","t":15,"s":"wow"}}""") + val re = sj.fromJson(js) + re.a.asInstanceOf[Thing2[Long]].t shouldEqual (15L) + re.a.asInstanceOf[Thing2[Long]].s shouldEqual ("wow") + } + it("Top-level abstract class must work") { + val inst: AThing[Long] = Thing2(99L, "ok") + val sj = sjCodecOf[AThing[Long]] + val js = sj.toJson(inst) + js should matchJson("""{"_hint":"Thing2","t":99,"s":"ok"}""") + val re = sj.fromJson(js) + re.asInstanceOf[Thing2[Long]].t shouldEqual (99L) + re.asInstanceOf[Thing2[Long]].s shouldEqual ("ok") + } + it("Self-referencing class must work (bonus: parameterized self-referencing class)") { + val inst = Empl("abc123", 5, Empl("xyz11", -1, null, Nil), List(Empl("tru777", 0, null, Nil), Empl("pop9", 9, null, Nil))) + val sj = sjCodecOf[Empl[Int]] + val js = sj.toJson(inst) + js should matchJson("""{"id":"abc123","data":5,"boss":{"id":"xyz11","data":-1,"boss":null,"coworkers":[]},"coworkers":[{"id":"tru777","data":0,"boss":null,"coworkers":[]},{"id":"pop9","data":9,"boss":null,"coworkers":[]}]}""") + sj.fromJson(js) shouldEqual (inst) + } + it("Java classes must work") { + val inst = new SampleClass() + inst.setName("John Doe") + inst.setAge(45) + inst.setAddress("123 Main St") + val sj = sjCodecOf[SampleClass] + val js = sj.toJson(inst) + js should matchJson("""{"address":"123 Main St","name":"John Doe"}""") + val re = sj.fromJson(js) + re.getName() shouldEqual ("John Doe") + re.getAge() shouldEqual (0) + re.getAddress() shouldEqual ("123 Main St") + } + it("Java class value is null") { + val inst: SampleClass = null + val sj = sjCodecOf[SampleClass] + val js = sj.toJson(inst) + js shouldEqual ("""null""") + sj.fromJson(js) shouldEqual (null) + } + it("Abstract class value is null") { + val inst = AbstractClassHolder(null, null, null) + val sj = sjCodecOf[AbstractClassHolder] + val js = sj.toJson(inst) + js should matchJson("""{"a":null,"b":null,"c":null}""") + val re = sj.fromJson(js) + re.a shouldEqual (null) + re.b shouldEqual (null) + re.c shouldEqual (null) + } + it("Scala case class value is null") { + val inst: Person = null + val sj = sjCodecOf[Person] + val js = sj.toJson(inst) + js shouldEqual ("""null""") + sj.fromJson(js) shouldEqual (inst) + } + } diff --git a/src/test/scala/co.blocke.scalajack/json/classes/Model.scala b/src/test/scala/co.blocke.scalajack/json/classes/Model.scala new file mode 100644 index 00000000..8401e133 --- /dev/null +++ b/src/test/scala/co.blocke.scalajack/json/classes/Model.scala @@ -0,0 +1,90 @@ +package co.blocke.scalajack +package json +package classes + +import co.blocke.scala_reflection.Ignore +import dotty.tools.repl.Command +import java.net.NoRouteToHostException + +case class Person(name: String, @Change(name = "duration") age: Int) + +class Parent(val phase: Int, var stuff: List[String]): + private var _hidden: Boolean = false + def hidden: Boolean = _hidden + def hidden_=(h: Boolean) = _hidden = h + + private var _nope: Boolean = false // should not generate due to @Ignore + @Ignore def nope: Boolean = _nope + def nope_=(h: Boolean) = _nope = h + + var foo: String = "ok" + @Ignore var noFoo: String = "not ok" + +case class Child(name: String, age: Int, override val phase: Int) extends Parent(phase, Nil) + +case class Params[X, Y](a: List[X], b: Option[Y]) + +sealed trait Command +case object Start extends Command +case object Stop extends Command + +sealed trait Animal +@TypeHint(hintValue = "bowow") +case class Dog(name: String, numLegs: Int) extends Animal +@TypeHint(hintValue = "flipper") +case class Fish(species: String, freshwater: Boolean) extends Animal + +sealed trait City +class Dallas(val pop: Int) extends City +@TypeHint(hintValue = "vice") +class Miami(val temp: Double) extends City + +sealed trait Route +class CityRoute(val numStreets: Int) extends Route +// Testing indirection. In real-world scenario all your sealed trait's classes +// must be defined in one file. Implementation classes like CityRouteImpl could +// be in other files so the sealed trait's file doesn't grow huge. + +case class TraitHolder(a: Command, b: Animal, c: City, d: Route) + +sealed abstract class Command2 +case object Start2 extends Command2 +case object Stop2 extends Command2 + +sealed abstract class Animal2 +@TypeHint(hintValue = "bowow") +case class Dog2(name: String, numLegs: Int) extends Animal2 +@TypeHint(hintValue = "flipper") +case class Fish2(species: String, freshwater: Boolean) extends Animal2 + +sealed abstract class City2 +class Dallas2(val pop: Int) extends City2 +@TypeHint(hintValue = "vice") +class Miami2(val temp: Double) extends City2 + +sealed abstract class AThing[T] +class Thing1[T](val t: T) extends AThing[T] +class Thing2[T](val t: T, val s: String) extends AThing[T] + +case class AbstractClassHolder(a: Command2, b: Animal2, c: City2) +case class AbstractClassHolder2[P](a: AThing[P]) + +case class Empl[T](id: String, data: T, boss: Empl[T], coworkers: List[Empl[T]]) + +object VehicleClass extends Enumeration { + type VehicleClass = Value + val Land, Air, Sea = Value +} +import VehicleClass.* + +sealed trait Vehicle { val kind: VehicleClass } +case class Car(passengers: Int) extends Vehicle { val kind: Land.type = Land } + +sealed trait Hobby[X, Y] { val thing1: X; val thing2: Y } +sealed trait Artist[W, Z] { val instrument: W; val effort: Z } +sealed trait PersonX[X, Y] { val who: X; val org: Y } + +case class Sports[A, B](thing1: A, thing2: B) extends Hobby[A, B] +case class Painter[A, B](instrument: A, effort: B) extends Artist[A, B] +case class Employee[A, B, C, D](who: Artist[C, Hobby[D, A]], org: B) extends PersonX[Artist[C, Hobby[D, A]], B] +type ComplexPerson = PersonX[Artist[Int, Hobby[Double, Char]], Vehicle] diff --git a/src/test/scala/co.blocke.scalajack/json/classes/Model2.scala b/src/test/scala/co.blocke.scalajack/json/classes/Model2.scala new file mode 100644 index 00000000..706643f3 --- /dev/null +++ b/src/test/scala/co.blocke.scalajack/json/classes/Model2.scala @@ -0,0 +1,5 @@ +package co.blocke.scalajack +package json +package classes + +case class CityRouteImpl(override val numStreets: Int) extends CityRoute(numStreets) diff --git a/src/test/scala/co.blocke.scalajack/json/classes/TraitSpec.scala b/src/test/scala/co.blocke.scalajack/json/classes/TraitSpec.scala new file mode 100644 index 00000000..3b4fd5f3 --- /dev/null +++ b/src/test/scala/co.blocke.scalajack/json/classes/TraitSpec.scala @@ -0,0 +1,72 @@ +package co.blocke.scalajack +package json +package classes + +import ScalaJack.* +import co.blocke.scala_reflection.* +import org.scalatest.funspec.AnyFunSpec +import org.scalatest.matchers.should.Matchers.* +import org.scalatest.* +import scala.util.* +import TestUtil.* + +import java.util.UUID + +class TraitSpec() extends AnyFunSpec with JsonMatchers: + opaque type phone = String + + describe(colorString("-------------------------------\n: Trait Tests :\n-------------------------------", Console.YELLOW)) { + it("Sealed trait with case objects and case classes must work") { + val inst = TraitHolder(Start, Fish("Beta", false), Miami(101.1), CityRouteImpl(99)) + val sj = sjCodecOf[TraitHolder] + val js = sj.toJson(inst) + js should matchJson("""{"a":"Start","b":{"_hint":"Fish","species":"Beta","freshwater":false},"c":{"_hint":"Miami","temp":101.1},"d":{"_hint":"CityRoute","numStreets":99}}""") + val re = sj.fromJson(js) + re.a shouldEqual (inst.a) + re.b shouldEqual (inst.b) + (re.c.asInstanceOf[Miami].temp == inst.c.asInstanceOf[Miami].temp) shouldEqual (true) + (re.d.asInstanceOf[CityRoute].numStreets == inst.d.asInstanceOf[CityRoute].numStreets) shouldEqual (true) + } + it("Sealed trait with modified type hint label must work") { + val inst = TraitHolder(Start, Fish("Beta", false), Miami(101.1), CityRouteImpl(25)) + val sj = sjCodecOf[TraitHolder](SJConfig.withTypeHintLabel("ref")) + val js = sj.toJson(inst) + js should matchJson("""{"a":"Start","b":{"ref":"Fish","species":"Beta","freshwater":false},"c":{"ref":"Miami","temp":101.1},"d":{"ref":"CityRoute","numStreets":25}}""") + val re = sj.fromJson(js) + re.a shouldEqual (inst.a) + re.b shouldEqual (inst.b) + (re.c.asInstanceOf[Miami].temp == inst.c.asInstanceOf[Miami].temp) shouldEqual (true) + (re.d.asInstanceOf[CityRoute].numStreets == inst.d.asInstanceOf[CityRoute].numStreets) shouldEqual (true) + } + it("Sealed trait with type hint policy SCRAMBLE_CLASSNAME label must work") { + val inst = TraitHolder(Start, Fish("Beta", false), Miami(101.1), CityRouteImpl(25)) + val sj = sjCodecOf[TraitHolder](SJConfig.withTypeHintPolicy(TypeHintPolicy.SCRAMBLE_CLASSNAME)) + val js = sj.toJson(inst) + val diff = parseJValue(js).diff(parseJValue("""{"a":"Start","b":{"_hint":"86999-847-46A","species":"Beta","freshwater":false},"c":{"_hint":"13652-857-33B","temp":101.1},"d":{"_hint":"51470-503-54B","numStreets":25}}""")) + val diffMap = diff.changed.values.asInstanceOf[Map[String, Map[String, ?]]] + assert(diffMap("b").contains("_hint") && diffMap("c").contains("_hint") && diffMap("d").contains("_hint") == true) // ie only the scrambled _hint values are different + val re = sj.fromJson(js) + re.a shouldEqual (inst.a) + re.b shouldEqual (inst.b) + (re.c.asInstanceOf[Miami].temp == inst.c.asInstanceOf[Miami].temp) shouldEqual (true) + (re.d.asInstanceOf[CityRoute].numStreets == inst.d.asInstanceOf[CityRoute].numStreets) shouldEqual (true) + } + it("Sealed trait with type hint policy USE_ANNOTATION label must work") { + val inst = TraitHolder(Start, Fish("Beta", false), Miami(101.1), CityRouteImpl(25)) + val sj = sjCodecOf[TraitHolder](SJConfig.withTypeHintPolicy(TypeHintPolicy.USE_ANNOTATION)) + val js = sj.toJson(inst) + js should matchJson("""{"a":"Start","b":{"_hint":"flipper","species":"Beta","freshwater":false},"c":{"_hint":"vice","temp":101.1},"d":{"_hint":"CityRoute","numStreets":25}}""") + val re = sj.fromJson(js) + re.a shouldEqual (inst.a) + re.b shouldEqual (inst.b) + (re.c.asInstanceOf[Miami].temp == inst.c.asInstanceOf[Miami].temp) shouldEqual (true) + (re.d.asInstanceOf[CityRoute].numStreets == inst.d.asInstanceOf[CityRoute].numStreets) shouldEqual (true) + } + it("Complex trait relationships must work") { + val inst: ComplexPerson = Employee(Painter(5, Sports(1.2, 'Z')), Car(4)) + val sj = sjCodecOf[ComplexPerson] + val js = sj.toJson(inst) + js should matchJson("""{"_hint":"Employee","who":{"_hint":"Painter","instrument":5,"effort":{"_hint":"Sports","thing1":1.2,"thing2":"Z"}},"org":{"_hint":"Car","passengers":4}}""") + sj.fromJson(js) shouldEqual (inst) + } + } diff --git a/src/test/scala/co.blocke.scalajack/json/collections/JavaCollSpec.scala b/src/test/scala/co.blocke.scalajack/json/collections/JavaCollSpec.scala new file mode 100644 index 00000000..96a75593 --- /dev/null +++ b/src/test/scala/co.blocke.scalajack/json/collections/JavaCollSpec.scala @@ -0,0 +1,275 @@ +package co.blocke.scalajack +package json +package collections + +import ScalaJack.* +import co.blocke.scala_reflection.* +import org.scalatest.funspec.AnyFunSpec +import org.scalatest.matchers.should.Matchers.* +import org.scalatest.* +import TestUtil.* + +import java.util.{ArrayList, Arrays, HashSet} + +class JavaCollSpec() extends AnyFunSpec with JsonMatchers: + + describe(colorString("-------------------------------\n: Java Collection Tests :\n-------------------------------", Console.YELLOW)) { + describe(colorString("+++ Basic functions (Set) +++")) { + it("Set is null must work") { + val inst = JSetHolder[Int](null) + val sj = sjCodecOf[JSetHolder[Int]] + val js = sj.toJson(inst) + js should matchJson("""{"a":null}""") + sj.fromJson(js) shouldEqual (inst) + } + it("Set of numeric must work") { + val inst = JSetHolder[Int](HashSet(Arrays.asList(1, 2, 3))) + val sj = sjCodecOf[JSetHolder[Int]] + val js = sj.toJson(inst) + js should matchJson("""{"a":[1,2,3]}""") + sj.fromJson(js) shouldEqual (inst) + } + it("Set of string must work") { + val inst = JSetHolder[String](HashSet(Arrays.asList("a", "b", "c"))) + val sj = sjCodecOf[JSetHolder[String]] + val js = sj.toJson(inst) + js should matchJson("""{"a":["a","b","c"]}""") + sj.fromJson(js) shouldEqual (inst) + } + it("Set of boolean must work") { + val inst = JSetHolder[Boolean](HashSet(Arrays.asList(true, false, true))) + val sj = sjCodecOf[JSetHolder[Boolean]] + val js = sj.toJson(inst) + js should matchJson("""{"a":[false,true]}""") + sj.fromJson(js) shouldEqual (inst) + } + it("Set of Set (nested) must work") { + import java.util.TreeSet as JTreeSet + import java.util.Comparator + val setOfSets: java.util.Set[java.util.Set[Int]] = new JTreeSet[java.util.Set[Int]](new Comparator[java.util.Set[Int]] { + override def compare(o1: java.util.Set[Int], o2: java.util.Set[Int]): Int = -1 + }) + val a: java.util.Set[Int] = new JTreeSet() + a.add(1) + a.add(2) + val b: java.util.Set[Int] = new JTreeSet() + b.add(3) + b.add(4) + setOfSets.add(b) + setOfSets.add(a) + val inst = JSetHolder[java.util.Set[Int]](setOfSets) + val sj = sjCodecOf[JSetHolder[java.util.Set[Int]]] + val js = sj.toJson(inst) + js should (matchJson("""{"a":[[1,2],[3,4]]}""") or matchJson("""{"a":[[3,4],[1,2]]}""")) + sj.fromJson(js) shouldEqual (inst) + } + it("Set of either must work") { + val inst = JSetHolder[Either[Int, Boolean]](HashSet(Arrays.asList(Right(true), Left(15), Right(false)))) + val sj = sjCodecOf[JSetHolder[Either[Int, Boolean]]](SJConfig.withEitherLeftHandling(EitherLeftPolicy.AS_VALUE)) + val js = sj.toJson(inst) + js should matchJson("""{"a":[true,15,false]}""") + sj.fromJson(js) shouldEqual (inst) + } + it("Set of union must work") { + val inst = JSetHolder[Int | Boolean](HashSet(Arrays.asList(true, 15, false))) + val sj = sjCodecOf[JSetHolder[Int | Boolean]] + val js = sj.toJson(inst) + js should matchJson("""{"a":[false,true,15]}""") + sj.fromJson(js) shouldEqual (inst) + } + it("Set of option must work") { + val inst = JSetHolder[Option[Int]](HashSet(Arrays.asList(Some(1), None, Some(3)))) + val sj = sjCodecOf[JSetHolder[Option[Int]]] + val js = sj.toJson(inst) + js should matchJson("""{"a":[1,3]}""") + sj.fromJson(js) shouldEqual (JSetHolder[Option[Int]](HashSet(Arrays.asList(Some(1), Some(3))))) + } + it("Set of map must work") { + val inst = JSetHolder[Map[String, Int]](HashSet(Arrays.asList(Map("a" -> 1, "b" -> 2), Map("c" -> 3, "d" -> 4)))) + val sj = sjCodecOf[JSetHolder[Map[String, Int]]] + val js = sj.toJson(inst) + js should matchJson("""{"a":[{"a":1,"b":2},{"c":3,"d":4}]}""") + sj.fromJson(js) shouldEqual (inst) + } + it("Set of class must work") { + val inst = JSetHolder[Person](HashSet(Arrays.asList(Person("Bob", 35), Person("Sally", 54)))) + val sj = sjCodecOf[JSetHolder[Person]] + val js = sj.toJson(inst) + js should matchJson("""{"a":[{"name":"Bob","age",35},{"name":"Sally","age",54}]}""") + sj.fromJson(js) shouldEqual (inst) + } + } + + describe(colorString("+++ Basic functions (ArrayList) +++")) { + it("ArrayList is null must work") { + val inst = ArrayListHolder[Int](null) + val sj = sjCodecOf[ArrayListHolder[Int]] + val js = sj.toJson(inst) + js should matchJson("""{"a":null}""") + sj.fromJson(js) shouldEqual (inst) + } + it("ArrayList of numeric must work") { + val inst = ArrayListHolder[Int](ArrayList[Int](Arrays.asList(1, 2, 3))) + val sj = sjCodecOf[ArrayListHolder[Int]] + val js = sj.toJson(inst) + js should matchJson("""{"a":[1,2,3]}""") + sj.fromJson(js) shouldEqual (inst) + } + it("ArrayList of string must work") { + val inst = ArrayListHolder[String](ArrayList[String](Arrays.asList("a", "b", "c"))) + val sj = sjCodecOf[ArrayListHolder[String]] + val js = sj.toJson(inst) + js should matchJson("""{"a":["a","b","c"]}""") + sj.fromJson(js) shouldEqual (inst) + } + it("ArrayList of boolean must work") { + val inst = ArrayListHolder[Boolean](ArrayList[Boolean](Arrays.asList(true, false, true))) + val sj = sjCodecOf[ArrayListHolder[Boolean]] + val js = sj.toJson(inst) + js should matchJson("""{"a":[true,false,true]}""") + sj.fromJson(js) shouldEqual (inst) + } + it("ArrayList of ArrayList (nested) must work") { + val inst = ArrayListHolder[ArrayList[Int]](ArrayList[ArrayList[Int]](Arrays.asList(ArrayList(Arrays.asList(1, 2)), ArrayList(Arrays.asList(3, 4))))) + val sj = sjCodecOf[ArrayListHolder[ArrayList[Int]]] + val js = sj.toJson(inst) + js should matchJson("""{"a":[[1,2],[3,4]]}""") + sj.fromJson(js) shouldEqual (inst) + } + it("ArrayList of either must work") { + val inst = ArrayListHolder[Either[Int, Boolean]](ArrayList[Either[Int, Boolean]](Arrays.asList(Right(true), Left(15), Right(false)))) + val sj = sjCodecOf[ArrayListHolder[Either[Int, Boolean]]](SJConfig.withEitherLeftHandling(EitherLeftPolicy.AS_VALUE)) + val js = sj.toJson(inst) + js should matchJson("""{"a":[true,15,false]}""") + sj.fromJson(js) shouldEqual (inst) + } + it("ArrayList of union must work") { + val inst = ArrayListHolder[Int | Boolean](ArrayList[Int | Boolean](Arrays.asList(true, 15, false))) + val sj = sjCodecOf[ArrayListHolder[Int | Boolean]] + val js = sj.toJson(inst) + js should matchJson("""{"a":[true,15,false]}""") + sj.fromJson(js) shouldEqual (inst) + } + it("ArrayList of option must work") { + val inst = ArrayListHolder[Option[Int]](ArrayList[Option[Int]](Arrays.asList(Some(1), None, Some(3)))) + val sj = sjCodecOf[ArrayListHolder[Option[Int]]] + val js = sj.toJson(inst) + js should matchJson("""{"a":[1,3]}""") + sj.fromJson(js) shouldEqual (ArrayListHolder[Option[Int]](ArrayList[Option[Int]](Arrays.asList(Some(1), Some(3))))) + } + it("ArrayList of map must work") { + val inst = ArrayListHolder[Map[String, Int]](ArrayList[Map[String, Int]](Arrays.asList(Map("a" -> 1, "b" -> 2), Map("c" -> 3, "d" -> 4)))) + val sj = sjCodecOf[ArrayListHolder[Map[String, Int]]] + val js = sj.toJson(inst) + js should matchJson("""{"a":[{"a":1,"b":2},{"c":3,"d":4}]}""") + sj.fromJson(js) shouldEqual (inst) + } + it("ArrayList of class must work") { + val inst = ArrayListHolder[Person](ArrayList[Person](Arrays.asList(Person("Bob", 35), Person("Sally", 54)))) + val sj = sjCodecOf[ArrayListHolder[Person]] + val js = sj.toJson(inst) + js should matchJson("""{"a":[{"name":"Bob","age",35},{"name":"Sally","age",54}]}""") + sj.fromJson(js) shouldEqual (inst) + } + } + + describe(colorString("+++ Coersions (special cases, traits, etc) +++")) { + it("ArrayBlockingQueue") { + val q = new java.util.concurrent.ArrayBlockingQueue[Int](2, true, Arrays.asList(1, 2)) + val inst = Holder[java.util.concurrent.ArrayBlockingQueue[Int]](q) + val sj = sjCodecOf[Holder[java.util.concurrent.ArrayBlockingQueue[Int]]] + val js = sj.toJson(inst) + js should matchJson("""{"a":[1,2]}""") + val x = sj.fromJson(js) + x.a.getClass.getName shouldEqual (inst.a.getClass.getName) + x.a should contain(1) + x.a should contain(2) + x.a.size shouldEqual (2) + } + it("TreeSet") { + val ts = new java.util.TreeSet[Int](Arrays.asList(1, 2, 3)) + val inst = Holder(ts) + val sj = sjCodecOf[Holder[java.util.TreeSet[Int]]] + val js = sj.toJson(inst) + js should matchJson("""{"a":[1,2,3]}""") + sj.fromJson(js) shouldEqual (inst) + } + it("Stack") { + val ts = new java.util.Stack[String]() + ts.push("x") + ts.push("y") + ts.push("z") + val inst = Holder(ts) + val sj = sjCodecOf[Holder[java.util.Stack[String]]] + val js = sj.toJson(inst) + js should matchJson("""{"a":["x","y","z"]}""") + sj.fromJson(js) shouldEqual (inst) + } + it("List") { + val ts: java.util.List[Int] = new java.util.ArrayList[Int](Arrays.asList(1, 2, 3)) + val inst = Holder(ts) + val sj = sjCodecOf[Holder[java.util.List[Int]]] + val js = sj.toJson(inst) + js should matchJson("""{"a":[1,2,3]}""") + sj.fromJson(js) shouldEqual (inst) + } + it("Iterable") { + val ts: java.lang.Iterable[Int] = new java.util.ArrayList[Int](Arrays.asList(1, 2, 3)) + val inst = Holder(ts) + val sj = sjCodecOf[Holder[java.lang.Iterable[Int]]] + val js = sj.toJson(inst) + js should matchJson("""{"a":[1,2,3]}""") + sj.fromJson(js) shouldEqual (inst) + } + it("Queue") { + val ts: java.util.Queue[Int] = new java.util.LinkedList[Int](Arrays.asList(1, 2, 3)) + val inst = Holder(ts) + val sj = sjCodecOf[Holder[java.util.Queue[Int]]] + val js = sj.toJson(inst) + js should matchJson("""{"a":[1,2,3]}""") + sj.fromJson(js) shouldEqual (inst) + } + it("Deque") { + val ts: java.util.Deque[Int] = new java.util.LinkedList[Int](Arrays.asList(1, 2, 3)) + val inst = Holder(ts) + val sj = sjCodecOf[Holder[java.util.Deque[Int]]] + val js = sj.toJson(inst) + js should matchJson("""{"a":[1,2,3]}""") + sj.fromJson(js) shouldEqual (inst) + } + it("BlockingQueue") { + val ts: java.util.concurrent.BlockingQueue[Int] = new java.util.concurrent.LinkedBlockingQueue[Int](Arrays.asList(1, 2, 3)) + val inst = Holder(ts) + val sj = sjCodecOf[Holder[java.util.concurrent.BlockingQueue[Int]]] + val js = sj.toJson(inst) + js should matchJson("""{"a":[1,2,3]}""") + val x = sj.fromJson(js) + x.a.getClass.getName shouldEqual (inst.a.getClass.getName) + x.a should contain(1) + x.a should contain(2) + x.a should contain(3) + x.a.size shouldEqual (3) + } + it("TransferQueue") { + val ts: java.util.concurrent.TransferQueue[Int] = new java.util.concurrent.LinkedTransferQueue[Int](Arrays.asList(1, 2, 3)) + val inst = Holder(ts) + val sj = sjCodecOf[Holder[java.util.concurrent.TransferQueue[Int]]] + val js = sj.toJson(inst) + js should matchJson("""{"a":[1,2,3]}""") + val x = sj.fromJson(js) + x.a.getClass.getName shouldEqual (inst.a.getClass.getName) + x.a should contain(1) + x.a should contain(2) + x.a should contain(3) + x.a.size shouldEqual (3) + } + it("Vector (generic Collection example)") { + val ts = new java.util.Vector[Int](Arrays.asList(1, 2, 3)) + val inst = Holder(ts) + val sj = sjCodecOf[Holder[java.util.Vector[Int]]] + val js = sj.toJson(inst) + js should matchJson("""{"a":[1,2,3]}""") + sj.fromJson(js) shouldEqual (inst) + } + } + } diff --git a/src/test/scala/co.blocke.scalajack/json/collections/JavaMapSpec.scala b/src/test/scala/co.blocke.scalajack/json/collections/JavaMapSpec.scala new file mode 100644 index 00000000..ada78bbc --- /dev/null +++ b/src/test/scala/co.blocke.scalajack/json/collections/JavaMapSpec.scala @@ -0,0 +1,229 @@ +package co.blocke.scalajack +package json +package collections + +import ScalaJack.* +import co.blocke.scala_reflection.* +import org.scalatest.funspec.AnyFunSpec +import org.scalatest.matchers.should.Matchers.* +import org.scalatest.* +import TestUtil.* + +import java.util.UUID + +class JavaMapSpec() extends AnyFunSpec with JsonMatchers: + + describe(colorString("-------------------------------\n: Java Map Tests :\n-------------------------------", Console.YELLOW)) { + describe(colorString("+++ General Map Interface Tests +++")) { + it("Map is null must work") { + val inst = JMapHolder[Int, Int](null) + val sj = sjCodecOf[JMapHolder[Int, Int]] + val js = sj.toJson(inst) + js should matchJson("""{"a":null}""") + sj.fromJson(js) shouldEqual (inst) + } + it("Map key of string must work") { + val m: java.util.Map[String, Int] = new java.util.HashMap[String, Int]() + m.put("x", 1) + m.put("y", 2) + val inst = JMapHolder[String, Int](new java.util.HashMap(m)) + val sj = sjCodecOf[JMapHolder[String, Int]] + val js = sj.toJson(inst) + js should matchJson("""{"a":{"x":1,"y":2}}""") + sj.fromJson(js) shouldEqual (inst) + sj.fromJson(js) shouldEqual (inst) + } + it("Map key of long must work") { + val m: java.util.Map[Long, Int] = new java.util.HashMap[Long, Int]() + m.put(15L, 1) + m.put(25L, 2) + val inst = JMapHolder[Long, Int](new java.util.HashMap(m)) + val sj = sjCodecOf[JMapHolder[Long, Int]] + val js = sj.toJson(inst) + js should matchJson("""{"a":{"15":1,"25":2}}""") + sj.fromJson(js) shouldEqual (inst) + } + it("Map key of boolean must work") { + val m: java.util.Map[Boolean, Int] = new java.util.HashMap[Boolean, Int]() + m.put(true, 1) + m.put(false, 2) + val inst = JMapHolder[Boolean, Int](new java.util.HashMap(m)) + val sj = sjCodecOf[JMapHolder[Boolean, Int]] + val js = sj.toJson(inst) + js should matchJson("""{"a":{"true":1,"false":2}}""") + sj.fromJson(js) shouldEqual (inst) + } + it("Map key of uuid must work") { + val m: java.util.Map[UUID, String] = new java.util.HashMap[UUID, String]() + m.put(UUID.fromString("1b9ab03f-26a3-4ec5-a8dd-d5122ff86b03"), "x") + m.put(UUID.fromString("09abdeb1-8b07-4683-8f97-1f5621696008"), "y") + val inst = JMapHolder[UUID, String](new java.util.HashMap(m)) + val sj = sjCodecOf[JMapHolder[UUID, String]] + val js = sj.toJson(inst) + js should matchJson("""{"a":{"1b9ab03f-26a3-4ec5-a8dd-d5122ff86b03":"x","09abdeb1-8b07-4683-8f97-1f5621696008":"y"}}""") + sj.fromJson(js) shouldEqual (inst) + } + it("Map key of value class must work") { + val m: java.util.Map[Distance, String] = new java.util.HashMap[Distance, String]() + m.put(Distance(1.23), "w") + m.put(Distance(4.56), "y") + val inst = JMapHolder[Distance, String](new java.util.HashMap(m)) + val sj = sjCodecOf[JMapHolder[Distance, String]] + val js = sj.toJson(inst) + js should matchJson("""{"a":{"1.23":"w","4.56":"y"}}""") + sj.fromJson(js) shouldEqual (inst) + } + it("Map value of string must work") { + val m: java.util.Map[String, String] = new java.util.HashMap[String, String]() + m.put("w", "x") + m.put("y", "z") + val inst = JMapHolder[String, String](new java.util.HashMap(m)) + val sj = sjCodecOf[JMapHolder[String, String]] + val js = sj.toJson(inst) + js should matchJson("""{"a":{"w":"x","y":"z"}}""") + sj.fromJson(js) shouldEqual (inst) + } + it("Map value of long must work") { + val m: java.util.Map[String, Long] = new java.util.HashMap[String, Long]() + m.put("w", 3L) + m.put("y", 4L) + val inst = JMapHolder[String, Long](new java.util.HashMap(m)) + val sj = sjCodecOf[JMapHolder[String, Long]] + val js = sj.toJson(inst) + js should matchJson("""{"a":{"w":3,"y":4}}""") + sj.fromJson(js) shouldEqual (inst) + } + it("Map value of boolean must work") { + val m: java.util.Map[String, Boolean] = new java.util.HashMap[String, Boolean]() + m.put("w", true) + m.put("y", false) + val inst = JMapHolder[String, Boolean](new java.util.HashMap(m)) + val sj = sjCodecOf[JMapHolder[String, Boolean]] + val js = sj.toJson(inst) + js should matchJson("""{"a":{"w":true,"y":false}}""") + sj.fromJson(js) shouldEqual (inst) + } + it("Map value of uuid must work") { + val m: java.util.Map[String, UUID] = new java.util.HashMap[String, UUID]() + m.put("x", UUID.fromString("1b9ab03f-26a3-4ec5-a8dd-d5122ff86b03")) + m.put("y", UUID.fromString("09abdeb1-8b07-4683-8f97-1f5621696008")) + val inst = JMapHolder[String, UUID](new java.util.HashMap(m)) + val sj = sjCodecOf[JMapHolder[String, UUID]] + val js = sj.toJson(inst) + js should matchJson("""{"a":{"x":"1b9ab03f-26a3-4ec5-a8dd-d5122ff86b03","y":"09abdeb1-8b07-4683-8f97-1f5621696008"}}""") + sj.fromJson(js) shouldEqual (inst) + } + it("Map value of Seq must work") { + val m: java.util.Map[String, List[Int]] = new java.util.HashMap[String, List[Int]]() + m.put("w", List(1, 2)) + m.put("y", List(3, 4)) + val inst = JMapHolder[String, List[Int]](new java.util.HashMap(m)) + val sj = sjCodecOf[JMapHolder[String, List[Int]]] + val js = sj.toJson(inst) + js should matchJson("""{"a":{"w":[1,2],"y":[3,4]}}""") + sj.fromJson(js) shouldEqual (inst) + } + it("Map value of Map (nested) must work") { + val m: java.util.Map[String, Map[String, Int]] = new java.util.HashMap[String, Map[String, Int]]() + m.put("w", Map("r" -> 3, "t" -> 4)) + m.put("y", Map("s" -> 7, "q" -> 9)) + val inst = JMapHolder[String, Map[String, Int]](new java.util.HashMap(m)) + val sj = sjCodecOf[JMapHolder[String, Map[String, Int]]] + val js = sj.toJson(inst) + js should matchJson("""{"a":{"w":{"r":3,"t":4},"y":{"s":7,"q":9}}}""") + sj.fromJson(js) shouldEqual (inst) + } + it("Map value of class must work") { + val m: java.util.Map[String, Person] = new java.util.HashMap[String, Person]() + m.put("w", Person("Bob", 34)) + m.put("y", Person("Sally", 25)) + val inst = JMapHolder[String, Person](new java.util.HashMap(m)) + val sj = sjCodecOf[JMapHolder[String, Person]] + val js = sj.toJson(inst) + js should matchJson("""{"a":{"w":{"name":"Bob","age":34},"y":{"name":"Sally","age":25}}}""") + sj.fromJson(js) shouldEqual (inst) + } + it("Map value of union type must work") { + val m: java.util.Map[String, Int | List[String]] = new java.util.HashMap[String, Int | List[String]]() + m.put("w", 3) + m.put("y", List("wow", "blah")) + val inst = JMapHolder[String, Int | List[String]](new java.util.HashMap(m)) + val sj = sjCodecOf[JMapHolder[String, Int | List[String]]] + val js = sj.toJson(inst) + js should matchJson("""{"a":{"w":3,"y":["wow","blah"]}}""") + sj.fromJson(js) shouldEqual (inst) + } + it("Map value of value class must work") { + val m: java.util.Map[String, Distance] = new java.util.HashMap[String, Distance]() + m.put("w", new Distance(1.23)) + m.put("y", Distance(4.56)) + val inst = JMapHolder[String, Distance](new java.util.HashMap(m)) + val sj = sjCodecOf[JMapHolder[String, Distance]] + val js = sj.toJson(inst) + js should matchJson("""{"a":{"w":1.23,"y":4.56}}""") + sj.fromJson(js) shouldEqual (inst) + } + } + describe(colorString("+++ Special/Specific Map Tests +++")) { + it("NavigableMap must work") { + val m: java.util.NavigableMap[String, Int] = new java.util.TreeMap[String, Int]() + m.put("x", 1) + m.put("y", 2) + val inst = Holder[java.util.NavigableMap[String, Int]](m) + val sj = sjCodecOf[Holder[java.util.NavigableMap[String, Int]]] + val js = sj.toJson(inst) + js should matchJson("""{"a":{"x":1,"y":2}}""") + sj.fromJson(js) shouldEqual (inst) + } + it("SortedMap must work") { + val m: java.util.SortedMap[String, Int] = new java.util.TreeMap[String, Int]() + m.put("x", 1) + m.put("y", 2) + val inst = Holder[java.util.SortedMap[String, Int]](m) + val sj = sjCodecOf[Holder[java.util.SortedMap[String, Int]]] + val js = sj.toJson(inst) + js should matchJson("""{"a":{"x":1,"y":2}}""") + sj.fromJson(js) shouldEqual (inst) + } + it("TreeMap must work") { + val m = new java.util.TreeMap[String, Int]() + m.put("x", 1) + m.put("y", 2) + val inst = Holder[java.util.TreeMap[String, Int]](m) + val sj = sjCodecOf[Holder[java.util.TreeMap[String, Int]]] + val js = sj.toJson(inst) + js should matchJson("""{"a":{"x":1,"y":2}}""") + sj.fromJson(js) shouldEqual (inst) + } + it("ConcurrentMap must work") { + val m = new java.util.concurrent.ConcurrentHashMap[String, Int]() + m.put("x", 1) + m.put("y", 2) + val inst = Holder[java.util.concurrent.ConcurrentMap[String, Int]](m) + val sj = sjCodecOf[Holder[java.util.concurrent.ConcurrentMap[String, Int]]] + val js = sj.toJson(inst) + js should matchJson("""{"a":{"x":1,"y":2}}""") + sj.fromJson(js) shouldEqual (inst) + } + it("ConcurrentNavigableMap must work") { + val m = new java.util.concurrent.ConcurrentSkipListMap[String, Int]() + m.put("x", 1) + m.put("y", 2) + val inst = Holder[java.util.concurrent.ConcurrentNavigableMap[String, Int]](m) + val sj = sjCodecOf[Holder[java.util.concurrent.ConcurrentNavigableMap[String, Int]]] + val js = sj.toJson(inst) + js should matchJson("""{"a":{"x":1,"y":2}}""") + sj.fromJson(js) shouldEqual (inst) + } + it("Specific must work (eg HashMap)") { + val m = new java.util.HashMap[String, Int]() + m.put("x", 1) + m.put("y", 2) + val inst = Holder[java.util.HashMap[String, Int]](m) + val sj = sjCodecOf[Holder[java.util.HashMap[String, Int]]] + val js = sj.toJson(inst) + js should matchJson("""{"a":{"x":1,"y":2}}""") + sj.fromJson(js) shouldEqual (inst) + } + } + } diff --git a/src/test/scala/co.blocke.scalajack/json/collections/MapSpec.scala b/src/test/scala/co.blocke.scalajack/json/collections/MapSpec.scala new file mode 100644 index 00000000..3e1ad9da --- /dev/null +++ b/src/test/scala/co.blocke.scalajack/json/collections/MapSpec.scala @@ -0,0 +1,288 @@ +package co.blocke.scalajack +package json +package collections + +import ScalaJack.* +import co.blocke.scala_reflection.* +import org.scalatest.funspec.AnyFunSpec +import org.scalatest.matchers.should.Matchers.* +import org.scalatest.* +import TestUtil.* + +import java.util.UUID + +class MapSpec() extends AnyFunSpec with JsonMatchers: + + describe(colorString("-------------------------------\n: Map Tests :\n-------------------------------", Console.YELLOW)) { + describe(colorString("+++ Positive Tests +++")) { + it("Map is null must work") { + val inst = MapHolder[Int, Int](null) + val sj = sjCodecOf[MapHolder[Int, Int]] + val js = sj.toJson(inst) + js should matchJson("""{"a":null}""") + sj.fromJson(js) shouldEqual (inst) + } + it("Map key of string must work") { + val inst = MapHolder[String, Int](Map("x" -> 1, "y" -> 2)) + val sj = sjCodecOf[MapHolder[String, Int]] + val js = sj.toJson(inst) + js should matchJson("""{"a":{"x":1,"y":2}}""") + sj.fromJson(js) shouldEqual (inst) + } + it("Map key of long must work") { + val inst = MapHolder[Long, Int](Map(15L -> 1, 25L -> 2)) + val sj = sjCodecOf[MapHolder[Long, Int]] + val js = sj.toJson(inst) + js should matchJson("""{"a":{"15":1,"25":2}}""") + sj.fromJson(js) shouldEqual (inst) + } + it("Map key of boolean must work") { + val inst = MapHolder[Boolean, Int](Map(true -> 1, false -> 2)) + val sj = sjCodecOf[MapHolder[Boolean, Int]] + val js = sj.toJson(inst) + js should matchJson("""{"a":{"true":1,"false":2}}""") + sj.fromJson(js) shouldEqual (inst) + } + it("Map key of uuid must work") { + val inst = MapHolder[UUID, String](Map(UUID.fromString("1b9ab03f-26a3-4ec5-a8dd-d5122ff86b03") -> "x", UUID.fromString("09abdeb1-8b07-4683-8f97-1f5621696008") -> "y")) + val sj = sjCodecOf[MapHolder[UUID, String]] + val js = sj.toJson(inst) + js should matchJson("""{"a":{"1b9ab03f-26a3-4ec5-a8dd-d5122ff86b03":"x","09abdeb1-8b07-4683-8f97-1f5621696008":"y"}}""") + sj.fromJson(js) shouldEqual (inst) + } + it("Map value of string must work") { + val inst = MapHolder[String, String](Map("w" -> "x", "y" -> "z")) + val sj = sjCodecOf[MapHolder[String, String]] + val js = sj.toJson(inst) + js should matchJson("""{"a":{"w":"x","y":"z"}}""") + sj.fromJson(js) shouldEqual (inst) + } + it("Map value of long must work") { + val inst = MapHolder[String, Long](Map("w" -> 3L, "y" -> 4L)) + val sj = sjCodecOf[MapHolder[String, Long]] + val js = sj.toJson(inst) + js should matchJson("""{"a":{"w":3,"y":4}}""") + sj.fromJson(js) shouldEqual (inst) + } + it("Map value of boolean must work") { + val inst = MapHolder[String, Boolean](Map("w" -> true, "y" -> false)) + val sj = sjCodecOf[MapHolder[String, Boolean]] + val js = sj.toJson(inst) + js should matchJson("""{"a":{"w":true,"y":false}}""") + sj.fromJson(js) shouldEqual (inst) + } + it("Map key and value of opaque alias type must work") { + val inst = MapHolder[OnOff, Counter](Map(true.asInstanceOf[OnOff] -> 1.asInstanceOf[Counter], false.asInstanceOf[OnOff] -> 0.asInstanceOf[Counter])) + val sj = sjCodecOf[MapHolder[OnOff, Counter]] + val js = sj.toJson(inst) + js should matchJson("""{"a":{"true":1,"false":0}}""") + sj.fromJson(js) shouldEqual (inst) + } + it("Map value of uuid must work") { + val inst = MapHolder[String, UUID](Map("x" -> UUID.fromString("1b9ab03f-26a3-4ec5-a8dd-d5122ff86b03"), "y" -> UUID.fromString("09abdeb1-8b07-4683-8f97-1f5621696008"))) + val sj = sjCodecOf[MapHolder[String, UUID]] + val js = sj.toJson(inst) + js should matchJson("""{"a":{"x":"1b9ab03f-26a3-4ec5-a8dd-d5122ff86b03","y":"09abdeb1-8b07-4683-8f97-1f5621696008"}}""") + sj.fromJson(js) shouldEqual (inst) + } + it("Map value of Seq must work") { + val inst = MapHolder[String, List[Int]](Map("w" -> List(1, 2), "y" -> List(3, 4))) + val sj = sjCodecOf[MapHolder[String, List[Int]]] + val js = sj.toJson(inst) + js should matchJson("""{"a":{"w":[1,2],"y":[3,4]}}""") + sj.fromJson(js) shouldEqual (inst) + } + it("Map value of Map (nested) must work") { + val inst = MapHolder[String, Map[String, Int]](Map("w" -> Map("r" -> 3, "t" -> 4), "y" -> Map("s" -> 7, "q" -> 9))) + val sj = sjCodecOf[MapHolder[String, Map[String, Int]]] + val js = sj.toJson(inst) + js should matchJson("""{"a":{"w":{"r":3,"t":4},"y":{"s":7,"q":9}}}""") + sj.fromJson(js) shouldEqual (inst) + } + it("Map value of union type must work") { + val inst = MapHolder[String, Int | List[String]](Map("w" -> 3, "y" -> List("wow", "blah"))) + val sj = sjCodecOf[MapHolder[String, Int | List[String]]] + val js = sj.toJson(inst) + js should matchJson("""{"a":{"w":3,"y":["wow","blah"]}}""") + sj.fromJson(js) shouldEqual (inst) + } + it("Map value of class must work") { + val inst = MapHolder[String, Person](Map("w" -> Person("Bob", 34), "y" -> Person("Sally", 25))) + val sj = sjCodecOf[MapHolder[String, Person]] + val js = sj.toJson(inst) + js should matchJson("""{"a":{"w":{"name":"Bob","age":34},"y":{"name":"Sally","age":25}}}""") + sj.fromJson(js) shouldEqual (inst) + } + it("Map key of value class must work") { + val inst = MapHolder[Distance, String](Map(new Distance(1.23) -> "x", Distance(4.56) -> "y")) + val sj = sjCodecOf[MapHolder[Distance, String]] + val js = sj.toJson(inst) + js should matchJson("""{"a":{"1.23":"x","4.56":"y"}}""") + sj.fromJson(js) shouldEqual (inst) + } + it("Map value of value class must work") { + val inst = MapHolder[String, Distance](Map("w" -> new Distance(1.23), "y" -> Distance(4.56))) + val sj = sjCodecOf[MapHolder[String, Distance]] + val js = sj.toJson(inst) + js should matchJson("""{"a":{"w":1.23,"y":4.56}}""") + sj.fromJson(js) shouldEqual (inst) + } + it("Mutable Map value must work - Map") { + val inst = MMapHolder[String, Distance](scala.collection.mutable.Map("w" -> new Distance(1.23), "y" -> Distance(4.56))) + val sj = sjCodecOf[MMapHolder[String, Distance]] + val js = sj.toJson(inst) + js should matchJson("""{"a":{"w":1.23,"y":4.56}}""") + sj.fromJson(js) shouldEqual (inst) + } + it("Mutable Map value must work - HashMap") { + val inst = MMapHolder2[String, Distance](scala.collection.mutable.HashMap("w" -> new Distance(1.23), "y" -> Distance(4.56))) + val sj = sjCodecOf[MMapHolder2[String, Distance]] + val js = sj.toJson(inst) + js should matchJson("""{"a":{"w":1.23,"y":4.56}}""") + sj.fromJson(js) shouldEqual (inst) + } + it("Mutable Map value must work - SeqMap (examplar for all other mutable Maps)") { + val inst = MMapHolder3[String, Distance](scala.collection.mutable.SeqMap("w" -> new Distance(1.23), "y" -> Distance(4.56))) + val sj = sjCodecOf[MMapHolder3[String, Distance]] + val js = sj.toJson(inst) + js should matchJson("""{"a":{"w":1.23,"y":4.56}}""") + sj.fromJson(js) shouldEqual (inst) + } + it("Map value must work - HashMap") { + val inst = MapHolder2[String, Distance](scala.collection.immutable.HashMap("w" -> new Distance(1.23), "y" -> Distance(4.56))) + val sj = sjCodecOf[MapHolder2[String, Distance]] + val js = sj.toJson(inst) + js should matchJson("""{"a":{"w":1.23,"y":4.56}}""") + sj.fromJson(js) shouldEqual (inst) + } + it("Map value must work - SeqMap") { + val inst = MapHolder3[String, Distance](scala.collection.immutable.SeqMap("w" -> new Distance(1.23), "y" -> Distance(4.56))) + val sj = sjCodecOf[MapHolder3[String, Distance]] + val js = sj.toJson(inst) + js should matchJson("""{"a":{"w":1.23,"y":4.56}}""") + sj.fromJson(js) shouldEqual (inst) + } + it("Map value must work - VectorMap (examplar for all other immutable Maps)") { + val inst = MapHolder4[String, Distance](scala.collection.immutable.TreeMap("w" -> new Distance(1.23), "y" -> Distance(4.56))) + val sj = sjCodecOf[MapHolder4[String, Distance]] + val js = sj.toJson(inst) + js should matchJson("""{"a":{"w":1.23,"y":4.56}}""") + sj.fromJson(js) shouldEqual (inst) + } + it("Map keys of remaining types must work (test coverage addition)") { + val inst = MapHolder[java.lang.Number, Int](Map(java.lang.Integer.valueOf(5).asInstanceOf[java.lang.Number] -> 12)) + val sj = sjCodecOf[MapHolder[java.lang.Number, Int]] + val js = sj.toJson(inst) + js should matchJson("""{"a":{"5":12}}""") + sj.fromJson(js) shouldEqual (inst) + + val inst2 = MapHolder[java.lang.Short, Int](Map(java.lang.Short.valueOf("5") -> 12)) + val sj2 = sjCodecOf[MapHolder[java.lang.Short, Int]] + val js2 = sj2.toJson(inst2) + js2 should matchJson("""{"a":{"5":12}}""") + sj2.fromJson(js2) shouldEqual (inst2) + + val inst3 = MapHolder[java.lang.Long, Int](Map(java.lang.Long.valueOf(5) -> 12)) + val sj3 = sjCodecOf[MapHolder[java.lang.Long, Int]] + val js3 = sj3.toJson(inst3) + js3 should matchJson("""{"a":{"5":12}}""") + sj3.fromJson(js3) shouldEqual (inst3) + + val inst4 = MapHolder[java.lang.Integer, Int](Map(java.lang.Integer.valueOf(5) -> 12)) + val sj4 = sjCodecOf[MapHolder[java.lang.Integer, Int]] + val js4 = sj4.toJson(inst4) + js4 should matchJson("""{"a":{"5",12}}""") + sj4.fromJson(js4) shouldEqual (inst4) + + val inst5 = MapHolder[java.lang.Float, Int](Map(java.lang.Float.valueOf(5) -> 12)) + val sj5 = sjCodecOf[MapHolder[java.lang.Float, Int]] + val js5 = sj5.toJson(inst5) + js5 should matchJson("""{"a":{"5.0":12}}""") + sj5.fromJson(js5) shouldEqual (inst5) + + val inst6 = MapHolder[java.lang.Double, Int](Map(java.lang.Double.valueOf(5) -> 12)) + val sj6 = sjCodecOf[MapHolder[java.lang.Double, Int]] + val js6 = sj6.toJson(inst6) + js6 should matchJson("""{"a":{"5.0":12}}""") + sj6.fromJson(js6) shouldEqual (inst6) + + val inst7 = MapHolder[java.lang.Byte, Int](Map(java.lang.Byte.valueOf("5") -> 12)) + val sj7 = sjCodecOf[MapHolder[java.lang.Byte, Int]] + val js7 = sj7.toJson(inst7) + js7 should matchJson("""{"a":{"5":12}}""") + sj7.fromJson(js7) shouldEqual (inst7) + + val inst8 = MapHolder[java.math.BigDecimal, Int](Map(java.math.BigDecimal.valueOf(5) -> 12)) + val sj8 = sjCodecOf[MapHolder[java.math.BigDecimal, Int]] + val js8 = sj8.toJson(inst8) + js8 should matchJson("""{"a":{"5":12}}""") + sj8.fromJson(js8) shouldEqual (inst8) + + val inst9 = MapHolder[java.math.BigInteger, Int](Map(java.math.BigInteger.valueOf(5) -> 12)) + val sj9 = sjCodecOf[MapHolder[java.math.BigInteger, Int]] + val js9 = sj9.toJson(inst9) + js9 should matchJson("""{"a":{"5":12}}""") + sj9.fromJson(js9) shouldEqual (inst9) + + val inst10 = MapHolder[java.lang.Boolean, Int](Map(java.lang.Boolean.valueOf(true) -> 12)) + val sj10 = sjCodecOf[MapHolder[java.lang.Boolean, Int]] + val js10 = sj10.toJson(inst10) + js10 should matchJson("""{"a":{"true":12}}""") + sj10.fromJson(js10) shouldEqual (inst10) + + val inst11 = MapHolder[Short, Int](Map(5.toShort -> 12)) + val sj11 = sjCodecOf[MapHolder[Short, Int]] + val js11 = sj11.toJson(inst11) + js11 should matchJson("""{"a":{"5":12}}""") + sj11.fromJson(js11) shouldEqual (inst11) + + val inst12 = MapHolder[Byte, Int](Map(5.toByte -> 12)) + val sj12 = sjCodecOf[MapHolder[Byte, Int]] + val js12 = sj12.toJson(inst12) + js12 should matchJson("""{"a":{"5":12}}""") + sj12.fromJson(js12) shouldEqual (inst12) + + val inst13 = MapHolder[Float, Int](Map(5.0.toFloat -> 12)) + val sj13 = sjCodecOf[MapHolder[Float, Int]] + val js13 = sj13.toJson(inst13) + js13 should matchJson("""{"a":{"5.0":12}}""") + sj13.fromJson(js13) shouldEqual (inst13) + + val inst14 = MapHolder[scala.math.BigDecimal, Int](Map(scala.math.BigDecimal(5) -> 12)) + val sj14 = sjCodecOf[MapHolder[scala.math.BigDecimal, Int]] + val js14 = sj14.toJson(inst14) + js14 should matchJson("""{"a":{"5":12}}""") + sj14.fromJson(js14) shouldEqual (inst14) + + val inst15 = MapHolder[scala.math.BigInt, Int](Map(scala.math.BigInt(5) -> 12)) + val sj15 = sjCodecOf[MapHolder[scala.math.BigInt, Int]] + val js15 = sj15.toJson(inst15) + js15 should matchJson("""{"a":{"5":12}}""") + sj15.fromJson(js15) shouldEqual (inst15) + + val inst16 = MapHolder[OnOff, OnOff](Map(true.asInstanceOf[OnOff] -> false.asInstanceOf[OnOff])) + val sj16 = sjCodecOf[MapHolder[OnOff, OnOff]] + val js16 = sj16.toJson(inst16) + js16 should matchJson("""{"a":{"true":false}}""") + sj16.fromJson(js16) shouldEqual (inst16) + + val inst17 = MapHolder[Boolean, OnOff](Map(true -> false.asInstanceOf[OnOff])) + val sj17 = sjCodecOf[MapHolder[Boolean, OnOff]] + val js17 = sj17.toJson(inst17) + js17 should matchJson("""{"a":{"true":false}}""") + sj17.fromJson(js17) shouldEqual (inst17) + + val inst18 = MapHolder[OnOff, Boolean](Map(true.asInstanceOf[OnOff] -> false)) + val sj18 = sjCodecOf[MapHolder[OnOff, Boolean]] + val js18 = sj18.toJson(inst18) + js18 should matchJson("""{"a":{"true":false}}""") + sj18.fromJson(js18) shouldEqual (inst18) + + val now = java.time.Instant.now() + val inst19 = MapHolder[java.time.Instant, Boolean](Map(now -> false)) + val sj19 = sjCodecOf[MapHolder[java.time.Instant, Boolean]] + val js19 = sj19.toJson(inst19) + sj19.fromJson(js19) shouldEqual (inst19) + } + } + } diff --git a/src/test/scala/co.blocke.scalajack/json/collections/Model.scala b/src/test/scala/co.blocke.scalajack/json/collections/Model.scala new file mode 100644 index 00000000..9ffc6e79 --- /dev/null +++ b/src/test/scala/co.blocke.scalajack/json/collections/Model.scala @@ -0,0 +1,46 @@ +package co.blocke.scalajack +package json +package collections + +import java.util.{ArrayList, Map as JMap, Set as JSet} + +case class Person(name: String, age: Int) + +opaque type OnOff = Boolean +opaque type Counter = Short + +case class SeqHolder[T](a: Seq[T]) +case class SetHolder[T](a: Set[T]) +case class MSeqHolder[T](a: scala.collection.mutable.Seq[T]) +case class MSetHolder[T](a: scala.collection.mutable.Set[T]) +case class VectorHolder[T](a: Vector[T]) +case class IndexedSeqHolder[T](a: IndexedSeq[T]) +case class IterableHolder[T](a: Iterable[T]) +case class ArrayHolder[T](a: Array[T]) +case class Holder[T](a: T) + +case class MapHolder[T, V](a: Map[T, V]) +case class MapHolder2[T, V](a: scala.collection.immutable.HashMap[T, V]) // specific +case class MapHolder3[T, V](a: scala.collection.immutable.SeqMap[T, V]) // specific +case class MapHolder4[T, V](a: scala.collection.immutable.TreeMap[T, V]) // open coersion +case class MMapHolder[T, V](a: scala.collection.mutable.Map[T, V]) // specific +case class MMapHolder2[T, V](a: scala.collection.mutable.HashMap[T, V]) // specific +case class MMapHolder3[T, V](a: scala.collection.mutable.SeqMap[T, V]) // open coersion +case class JMapHolder[T, V](a: JMap[T, V]) + +class Distance(val meters: Double) extends AnyVal + +case class TupleHolder[A, B, C](a: (A, B, C)) +case class TupleOneHolder[A](a: Tuple1[A]) + +case class ArrayListHolder[T](a: ArrayList[T]) +case class JSetHolder[T](a: JSet[T]) + +enum Color: + case Red, Green, Blue + +object Permissions extends Enumeration { + type Permissions = Value + val READ, WRITE, EXEC, NONE = Value +} +import Permissions.* diff --git a/src/test/scala/co.blocke.scalajack/json/collections/SeqSetArraySpec.scala b/src/test/scala/co.blocke.scalajack/json/collections/SeqSetArraySpec.scala new file mode 100644 index 00000000..a405b245 --- /dev/null +++ b/src/test/scala/co.blocke.scalajack/json/collections/SeqSetArraySpec.scala @@ -0,0 +1,268 @@ +package co.blocke.scalajack +package json +package collections + +import ScalaJack.* +import co.blocke.scala_reflection.* +import org.scalatest.funspec.AnyFunSpec +import org.scalatest.matchers.should.Matchers.* +import org.scalatest.* +import TestUtil.* + +import scala.collection.immutable.HashSet + +class SeqSetArraySpec() extends AnyFunSpec with JsonMatchers: + + describe(colorString("-------------------------------\n: Seq, Set, and Array Tests :\n-------------------------------", Console.YELLOW)) { + describe(colorString("+++ Positive Tests +++")) { + it("Seq is null must work") { + val inst = SeqHolder[Int](null) + val sj = sjCodecOf[SeqHolder[Int]] + val js = sj.toJson(inst) + js should matchJson("""{"a":null}""") + sj.fromJson(js) shouldEqual (inst) + } + it("Seq of numeric must work") { + val inst = SeqHolder[Int](List(1, 2, 3)) + val sj = sjCodecOf[SeqHolder[Int]] + val js = sj.toJson(inst) + js should matchJson("""{"a":[1,2,3]}""") + sj.fromJson(js) shouldEqual (inst) + } + it("Seq of string must work") { + val inst = SeqHolder[String](List("a", "b", "c")) + val sj = sjCodecOf[SeqHolder[String]] + val js = sj.toJson(inst) + js should matchJson("""{"a":["a","b","c"]}""") + sj.fromJson(js) shouldEqual (inst) + } + it("Mutable Seq of string must work") { + val inst = MSeqHolder[String](scala.collection.mutable.ListBuffer("a", "b", "c")) + val sj = sjCodecOf[MSeqHolder[String]] + val js = sj.toJson(inst) + js should matchJson("""{"a":["a","b","c"]}""") + sj.fromJson(js) shouldEqual (inst) + } + it("Seq of boolean must work") { + val inst = SeqHolder[Boolean](List(true, false, true)) + val sj = sjCodecOf[SeqHolder[Boolean]] + val js = sj.toJson(inst) + js should matchJson("""{"a":[true,false,true]}""") + sj.fromJson(js) shouldEqual (inst) + } + it("Seq of seq (nested) must work") { + val inst = SeqHolder[List[Int]](List(List(1, 2), List(3, 4))) + val sj = sjCodecOf[SeqHolder[List[Int]]] + val js = sj.toJson(inst) + js should matchJson("""{"a":[[1,2],[3,4]]}""") + sj.fromJson(js) shouldEqual (inst) + } + it("Seq of either must work") { + val inst = SeqHolder[Either[Int, Boolean]](List(Right(true), Left(15), Right(false))) + val sj = sjCodecOf[SeqHolder[Either[Int, Boolean]]](SJConfig.withEitherLeftHandling(EitherLeftPolicy.AS_VALUE)) + val js = sj.toJson(inst) + js should matchJson("""{"a":[true,15,false]}""") + sj.fromJson(js) shouldEqual (inst) + } + it("Seq of union must work") { + val inst = SeqHolder[Int | Boolean](List(true, 15, false)) + val sj = sjCodecOf[SeqHolder[Int | Boolean]] + val js = sj.toJson(inst) + js should matchJson("""{"a":[true,15,false]}""") + sj.fromJson(js) shouldEqual (inst) + } + it("Seq of option must work") { + val inst = SeqHolder[Option[Int]](List(Some(1), None, Some(3))) + val sj = sjCodecOf[SeqHolder[Option[Int]]] + val js = sj.toJson(inst) + js should matchJson("""{"a":[1,3]}""") + sj.fromJson(js) shouldEqual (SeqHolder[Option[Int]](List(Some(1), Some(3)))) + } + it("Seq of map must work") { + val inst = SeqHolder[Map[String, Int]](List(Map("a" -> 1, "b" -> 2), Map("c" -> 3, "d" -> 4))) + val sj = sjCodecOf[SeqHolder[Map[String, Int]]] + val js = sj.toJson(inst) + js should matchJson("""{"a":[{"a":1,"b":2},{"c":3,"d":4}]}""") + sj.fromJson(js) shouldEqual (inst) + } + it("Seq of class must work") { + val inst = SeqHolder[Person](List(Person("Bob", 35), Person("Sally", 54))) + val sj = sjCodecOf[SeqHolder[Person]] + val js = sj.toJson(inst) + js should matchJson("""{"a":[{"name":"Bob","age",35},{"name":"Sally","age",54}]}""") + sj.fromJson(js) shouldEqual (inst) + } + + it("Set is null must work") { + val inst = SetHolder[Int](null) + val sj = sjCodecOf[SetHolder[Int]] + val js = sj.toJson(inst) + js should matchJson("""{"a":null}""") + sj.fromJson(js) shouldEqual (inst) + } + it("Set of numeric must work") { + val inst = SetHolder[Int](HashSet(1, 2, 3)) + val sj = sjCodecOf[SetHolder[Int]] + val js = sj.toJson(inst) + js should matchJson("""{"a":[1,2,3]}""") + sj.fromJson(js) shouldEqual (inst) + } + it("Set of string must work") { + val inst = SetHolder[String](HashSet("a", "b", "c")) + val sj = sjCodecOf[SetHolder[String]] + val js = sj.toJson(inst) + js should matchJson("""{"a":["a","b","c"]}""") + sj.fromJson(js) shouldEqual (inst) + } + it("Mutable Set of string must work") { + val inst = MSetHolder[String](scala.collection.mutable.HashSet("a", "b", "c")) + val sj = sjCodecOf[MSetHolder[String]] + val js = sj.toJson(inst) + js should matchJson("""{"a":["a","b","c"]}""") + sj.fromJson(js) shouldEqual (inst) + } + it("Set of boolean must work") { + val inst = SetHolder[Boolean](HashSet(true, false, true)) + val sj = sjCodecOf[SetHolder[Boolean]] + val js = sj.toJson(inst) + js should matchJson("""{"a":[false,true]}""") + sj.fromJson(js) shouldEqual (inst) + } + it("Set of Set (nested) must work") { + val inst = SetHolder[HashSet[Int]](HashSet(HashSet(1, 2), HashSet(3, 4))) + val sj = sjCodecOf[SetHolder[HashSet[Int]]] + val js = sj.toJson(inst) + js should matchJson("""{"a":[[3,4],[1,2]]}""") + sj.fromJson(js) shouldEqual (inst) + } + it("Set of either must work") { + val inst = SetHolder[Either[Int, Boolean]](HashSet(Right(true), Left(15), Right(false))) + val sj = sjCodecOf[SetHolder[Either[Int, Boolean]]](SJConfig.withEitherLeftHandling(EitherLeftPolicy.AS_VALUE)) + val js = sj.toJson(inst) + js should matchJson("""{"a":[15,true,false]}""") + sj.fromJson(js) shouldEqual (inst) + } + it("Set of union must work") { + val inst = SetHolder[Int | Boolean](HashSet(true, 15, false)) + val sj = sjCodecOf[SetHolder[Int | Boolean]] + val js = sj.toJson(inst) + js should matchJson("""{"a":[false,true,15]}""") + sj.fromJson(js) shouldEqual (inst) + } + it("Set of option must work") { + val inst = SetHolder[Option[Int]](HashSet(Some(1), None, Some(3))) + val sj = sjCodecOf[SetHolder[Option[Int]]] + val js = sj.toJson(inst) + js should matchJson("""{"a":[3,1]}""") + sj.fromJson(js) shouldEqual (SetHolder[Option[Int]](HashSet(Some(1), Some(3)))) + } + it("Set of map must work") { + val inst = SetHolder[Map[String, Int]](HashSet(Map("a" -> 1, "b" -> 2), Map("c" -> 3, "d" -> 4))) + val sj = sjCodecOf[SetHolder[Map[String, Int]]] + val js = sj.toJson(inst) + js should matchJson("""{"a":[{"c":3,"d":4},{"a":1,"b":2}]}""") + sj.fromJson(js) shouldEqual (inst) + } + it("Set of class must work") { + val inst = SetHolder[Person](HashSet(Person("Bob", 35), Person("Sally", 54))) + val sj = sjCodecOf[SetHolder[Person]] + val js = sj.toJson(inst) + js should matchJson("""{"a":[{"name":"Bob","age",35},{"name":"Sally","age",54}]}""") + sj.fromJson(js) shouldEqual (inst) + } + + it("Array is null must work") { + val inst = ArrayHolder[Int](null) + val sj = sjCodecOf[ArrayHolder[Int]] + val js = sj.toJson(inst) + js should matchJson("""{"a":null}""") + sj.fromJson(js) shouldEqual (inst) + } + it("Array of numeric must work") { + val inst = ArrayHolder[Int](Array(1, 2, 3)) + val sj = sjCodecOf[ArrayHolder[Int]] + val js = sj.toJson(inst) + js should matchJson("""{"a":[1,2,3]}""") + sj.fromJson(js).a.toList shouldEqual (inst.a.toList) + } + it("Array of string must work") { + val inst = ArrayHolder[String](Array("a", "b", "c")) + val sj = sjCodecOf[ArrayHolder[String]] + val js = sj.toJson(inst) + js should matchJson("""{"a":["a","b","c"]}""") + sj.fromJson(js).a.toList shouldEqual (inst.a.toList) + } + it("Array of boolean must work") { + val inst = ArrayHolder[Boolean](Array(true, false, true)) + val sj = sjCodecOf[ArrayHolder[Boolean]] + val js = sj.toJson(inst) + js should matchJson("""{"a":[true,false,true]}""") + sj.fromJson(js).a.toList shouldEqual (inst.a.toList) + } + it("Array of Array (nested) must work") { + val inst = ArrayHolder[Array[Int]](Array(Array(1, 2), Array(3, 4))) + val sj = sjCodecOf[ArrayHolder[Array[Int]]] + val js = sj.toJson(inst) + js should matchJson("""{"a":[[1,2],[3,4]]}""") + sj.fromJson(js).a.map(_.toList).toList shouldEqual (inst.a.map(_.toList).toList) + } + it("Array of either must work") { + val inst = ArrayHolder[Either[Int, Boolean]](Array(Right(true), Left(15), Right(false))) + val sj = sjCodecOf[ArrayHolder[Either[Int, Boolean]]](SJConfig.withEitherLeftHandling(EitherLeftPolicy.AS_VALUE)) + val js = sj.toJson(inst) + js should matchJson("""{"a":[true,15,false]}""") + sj.fromJson(js).a.toList shouldEqual (inst.a.toList) + } + it("Array of union must work") { + val inst = ArrayHolder[Int | Boolean](Array(true, 15, false)) + val sj = sjCodecOf[ArrayHolder[Int | Boolean]] + val js = sj.toJson(inst) + js should matchJson("""{"a":[true,15,false]}""") + sj.fromJson(js).a.toList shouldEqual (inst.a.toList) + } + it("Array of option must work") { + val inst = ArrayHolder[Option[Int]](Array(Some(1), None, Some(3))) + val sj = sjCodecOf[ArrayHolder[Option[Int]]] + val js = sj.toJson(inst) + js should matchJson("""{"a":[1,3]}""") + sj.fromJson(js).a.toList shouldEqual (ArrayHolder[Option[Int]](Array(Some(1), Some(3))).a.toList) + } + it("Array of map must work") { + val inst = ArrayHolder[Map[String, Int]](Array(Map("a" -> 1, "b" -> 2), Map("c" -> 3, "d" -> 4))) + val sj = sjCodecOf[ArrayHolder[Map[String, Int]]] + val js = sj.toJson(inst) + js should matchJson("""{"a":[{"a":1,"b":2},{"c":3,"d":4}]}""") + sj.fromJson(js).a.toList shouldEqual (inst.a.toList) + } + it("Array of class must work") { + val inst = ArrayHolder[Person](Array(Person("Bob", 35), Person("Sally", 54))) + val sj = sjCodecOf[ArrayHolder[Person]] + val js = sj.toJson(inst) + js should matchJson("""{"a":[{"name":"Bob","age",35},{"name":"Sally","age",54}]}""") + sj.fromJson(js).a.toList shouldEqual (inst.a.toList) + } + + it("Vector of class must work") { + val inst = VectorHolder[Person](Vector(Person("Bob", 35), Person("Sally", 54))) + val sj = sjCodecOf[VectorHolder[Person]] + val js = sj.toJson(inst) + js should matchJson("""{"a":[{"name":"Bob","age",35},{"name":"Sally","age",54}]}""") + sj.fromJson(js).a.toList shouldEqual (inst.a.toList) + } + it("IndexedSeq of class must work") { + val inst = IndexedSeqHolder[Person](IndexedSeq(Person("Bob", 35), Person("Sally", 54))) + val sj = sjCodecOf[IndexedSeqHolder[Person]] + val js = sj.toJson(inst) + js should matchJson("""{"a":[{"name":"Bob","age",35},{"name":"Sally","age",54}]}""") + sj.fromJson(js).a.toList shouldEqual (inst.a.toList) + } + it("Iterable of class must work") { + val inst = IterableHolder[Person](Seq(Person("Bob", 35), Person("Sally", 54))) + val sj = sjCodecOf[IterableHolder[Person]] + val js = sj.toJson(inst) + js should matchJson("""{"a":[{"name":"Bob","age",35},{"name":"Sally","age",54}]}""") + sj.fromJson(js).a.toList shouldEqual (inst.a.toList) + } + + } + } diff --git a/src/test/scala/co.blocke.scalajack/json/collections/TupleSpec.scala b/src/test/scala/co.blocke.scalajack/json/collections/TupleSpec.scala new file mode 100644 index 00000000..bbc2947e --- /dev/null +++ b/src/test/scala/co.blocke.scalajack/json/collections/TupleSpec.scala @@ -0,0 +1,68 @@ +package co.blocke.scalajack +package json +package collections + +import ScalaJack.* +import co.blocke.scala_reflection.* +import org.scalatest.funspec.AnyFunSpec +import org.scalatest.matchers.should.Matchers.* +import org.scalatest.* +import TestUtil.* + +import java.util.UUID + +class TupleSpec() extends AnyFunSpec with JsonMatchers: + + describe(colorString("-------------------------------\n: Tuple Tests :\n-------------------------------", Console.YELLOW)) { + describe(colorString("+++ Positive Tests +++")) { + it("Tuple is null must work") { + val inst = TupleHolder[Int, String, Boolean](null) + val sj = sjCodecOf[TupleHolder[Int, String, Boolean]] + val js = sj.toJson(inst) + js should matchJson("""{"a":null}""") + sj.fromJson(js) shouldEqual inst + } + it("Tuple of simple types must work") { + val inst = TupleHolder[Int, String, Boolean]((15, "wow", true)) + val sj = sjCodecOf[TupleHolder[Int, String, Boolean]] + val js = sj.toJson(inst) + js should matchJson("""{"a":[15,"wow",true]}""") + sj.fromJson(js) shouldEqual inst + } + it("Tuple of collecitons (including another tuple) must work") { + val inst = TupleHolder[Seq[Int], Map[String, Long], (Double, Char, Boolean)]((List(1, 2), Map("a" -> 3L, "b" -> 4L), (1.23d, 'X', true))) + val sj = sjCodecOf[TupleHolder[Seq[Int], Map[String, Long], (Double, Char, Boolean)]] + val js = sj.toJson(inst) + js should matchJson("""{"a":[[1,2],{"a":3,"b":4},[1.23,"X",true]]}""") + sj.fromJson(js) shouldEqual inst + } + it("Tuple of one element must work") { + val inst = TupleOneHolder[Int](Tuple1(15)) + val sj = sjCodecOf[TupleOneHolder[Int]] + val js = sj.toJson(inst) + js should matchJson("""{"a":[15]}""") + sj.fromJson(js) shouldEqual inst + } + } + + describe(colorString("--- Negative Tests ---")) { + it("Wrong number of elements in a tuple") { + val js = """{"a":[15,"wow",true,12.34]}""" + val msg = + """Expected ']' here at position [19] + |{"a":[15,"wow",true,12.34]} + |-------------------^""".stripMargin + val ex = intercept[JsonParseError](sjCodecOf[TupleHolder[Int, String, Boolean]].fromJson(js)) + ex.show shouldEqual msg + } + it("Wrong type of elements in tuple") { + val js = """{"a":[15,true,true]}""" + val msg = + """Expected a String value but got 't' at position [9] + |{"a":[15,true,true]} + |---------^""".stripMargin + val ex = intercept[JsonParseError](sjCodecOf[TupleHolder[Int, String, Boolean]].fromJson(js)) + ex.show shouldEqual msg + } + } + } diff --git a/src/test/scala/co.blocke.scalajack/json/misc/AliasSpec.scala b/src/test/scala/co.blocke.scalajack/json/misc/AliasSpec.scala new file mode 100644 index 00000000..58a2b469 --- /dev/null +++ b/src/test/scala/co.blocke.scalajack/json/misc/AliasSpec.scala @@ -0,0 +1,59 @@ +package co.blocke.scalajack +package json +package misc + +import ScalaJack.* +import co.blocke.scala_reflection.* +import org.scalatest.funspec.AnyFunSpec +import org.scalatest.matchers.should.Matchers.* +import org.scalatest.* +import scala.util.* +import TestUtil.* + +import java.util.UUID + +class AliasSpec() extends AnyFunSpec with JsonMatchers: + opaque type Count = Int + opaque type CountX = Option[Int] + type CountY = String + type CountZ = Option[String] + + describe(colorString("-------------------------------\n: Alias Tests :\n-------------------------------", Console.YELLOW)) { + describe(colorString("+++ Positive Tests +++")) { + it("Type aliases (opaque types) must be dereferenced") { + val inst = AliasHolder[Count](5, List(1, 2, 3), Map(1 -> "wow"), Map("wow" -> 2)) + val sj = sjCodecOf[AliasHolder[Count]] + val js = sj.toJson(inst) + js should matchJson("""{"a":5,"b":[1,2,3],"c":{"1":"wow"},"d":{"wow":2}}""") + sj.fromJson(js) shouldEqual (inst) + } + it("Type aliases (opaque types) must be dereferenced (with Option)") { + val inst = AliasHolder2[CountX](Some(5), List(Some(1), None, Some(3)), Map("wow" -> None)) + val sj = sjCodecOf[AliasHolder2[CountX]] + val js = sj.toJson(inst) + js should matchJson("""{"a":5,"b":[1,3],"c":{}}""") + sj.fromJson(js) shouldEqual (AliasHolder2[CountX](Some(5), List(Some(1), Some(3)), Map.empty[String, CountX])) + } + it("Type aliases (opaque types) must be dereferenced (with Option, noneAsNull)") { + val inst = AliasHolder2[CountX](Some(5), List(Some(1), None, Some(3)), Map("wow" -> None)) + val sj = sjCodecOf[AliasHolder2[CountX]](SJConfig.withNoneAsNull()) + val js = sj.toJson(inst) + js should matchJson("""{"a":5,"b":[1,null,3],"c":{"wow":null}}""") + sj.fromJson(js) shouldEqual (inst) + } + it("Type aliases (non-opaque types) must be dereferenced") { + val inst = AliasHolder[CountY]("q", List("r", "s", "t"), Map("u" -> "wow"), Map("wow" -> "v")) + val sj = sjCodecOf[AliasHolder[CountY]] + val js = sj.toJson(inst) + js should matchJson("""{"a":"q","b":["r","s","t"],"c":{"u":"wow"},"d":{"wow":"v"}}""") + sj.fromJson(js) shouldEqual (inst) + } + it("Type aliases (non-opaque types) must be dereferenced (with Option)") { + val inst = AliasHolder2[CountZ](Some("q"), List(Some("r"), None, Some("t")), Map("wow" -> None)) + val sj = sjCodecOf[AliasHolder2[CountZ]] + val js = sj.toJson(inst) + js should matchJson("""{"a":"q","b":["r","t"],"c":{}}""") + sj.fromJson(js) shouldEqual (AliasHolder2[CountZ](Some("q"), List(Some("r"), Some("t")), Map.empty[String, CountZ])) + } + } + } diff --git a/src/test/scala/co.blocke.scalajack/json/misc/EnumSpec.scala b/src/test/scala/co.blocke.scalajack/json/misc/EnumSpec.scala new file mode 100644 index 00000000..4d2207f3 --- /dev/null +++ b/src/test/scala/co.blocke.scalajack/json/misc/EnumSpec.scala @@ -0,0 +1,113 @@ +package co.blocke.scalajack +package json +package misc + +import ScalaJack.* +import co.blocke.scalajack.json.collections.CarEnum +import co.blocke.scala_reflection.* +import org.scalatest.funspec.AnyFunSpec +import org.scalatest.matchers.should.Matchers.* +import org.scalatest.* +import scala.util.* +import TestUtil.* + +class EnumSpec() extends AnyFunSpec with JsonMatchers: + + describe(colorString("-------------------------------\n: Enum Tests :\n-------------------------------", Console.YELLOW)) { + describe(colorString("+++ Positive Tests +++")) { + it("Enum as Map key and value must work") { + val inst = MapHolder[Color, Color](Map(Color.Red -> Color.Blue, Color.Green -> Color.Red)) + val sj = sjCodecOf[MapHolder[Color, Color]] + val js = sj.toJson(inst) + js should matchJson("""{"a":{"Red":"Blue","Green":"Red"}}""") + sj.fromJson(js) shouldEqual (inst) + } + it("Enum as Map key and value must work (using id)") { + val inst = MapHolder[Color, Color](Map(Color.Red -> Color.Blue, Color.Green -> Color.Red)) + val sj = sjCodecOf[MapHolder[Color, Color]](SJConfig.withEnumsAsIds(Nil)) + val js = sj.toJson(inst) + js should matchJson("""{"a":{"0":1,"2":0}}""") + sj.fromJson(js) shouldEqual (inst) + } + it("Enumeration (Scale 2) as Map key and value must work") { + import Permissions.* + val inst = MapHolder[Permissions, Permissions](Map(Permissions.READ -> Permissions.WRITE, Permissions.EXEC -> Permissions.NONE)) + val sj = sjCodecOf[MapHolder[Permissions, Permissions]] + val js = sj.toJson(inst) + js should matchJson("""{"a":{"READ":"WRITE","EXEC":"NONE"}}""") + sj.fromJson(js) shouldEqual (inst) + } + it("Enumeration (Scale 2) as Map key and value must work (using id)") { + import Permissions.* + val inst = MapHolder[Permissions, Permissions](Map(Permissions.READ -> Permissions.WRITE, Permissions.EXEC -> Permissions.NONE)) + val sj = sjCodecOf[MapHolder[Permissions, Permissions]](SJConfig.withEnumsAsIds(Nil)) + val js = sj.toJson(inst) + js should matchJson("""{"a":{"0":1,"2":3}}""") + sj.fromJson(js) shouldEqual (inst) + } + it("Enumeration (Scala 2) ordinal mutation") { + import SizeWithType.* + val inst = SampleEnum(Size.Small, Size.Medium, Size.Large, null, Size.Medium, Little) + val sj = sjCodecOf[SampleEnum] + val js = sj.toJson(inst) + js shouldEqual ("""{"e1":"Small","e2":"Medium","e3":"Large","e4":null,"e5":"Medium","e6":"Little"}""") + // mutate e5 into an ordinal... + val js2 = js.replaceAll(""""e5":"Medium"""", """"e5":1""") + sj.fromJson(js2) shouldEqual (inst) + } + it("Java Enumeration as Map key and value must work") { + val inst = MapHolder[CarEnum, CarEnum](Map(CarEnum.VW -> CarEnum.PORSCHE, CarEnum.PORSCHE -> CarEnum.TOYOTA)) + val sj = sjCodecOf[MapHolder[CarEnum, CarEnum]] + val js = sj.toJson(inst) + js should matchJson("""{"a":{"VW":"PORSCHE","PORSCHE":"TOYOTA"}}""") + sj.fromJson(js) shouldEqual (inst) + } + it("Java Enumeration as Map key and value must work (using id)") { + val inst = MapHolder[CarEnum, CarEnum](Map(CarEnum.VW -> CarEnum.PORSCHE, CarEnum.PORSCHE -> CarEnum.TOYOTA)) + val sj = sjCodecOf[MapHolder[CarEnum, CarEnum]](SJConfig.withEnumsAsIds(Nil)) + val js = sj.toJson(inst) + val targetJs = RType.of[CarEnum] match + case t: co.blocke.scala_reflection.rtypes.JavaEnumRType[?] => + val valMap = t.values.zipWithIndex.toMap + s"""{"a":{"${valMap("VW")}":${valMap("PORSCHE")},"${valMap("PORSCHE")}":${valMap("TOYOTA")}}}""" + js shouldEqual (targetJs) + sj.fromJson(js) shouldEqual (inst) + } + it("Enum/Enumeration mix of enum as value must work") { + import Permissions.* + val inst = MapHolder[Color, Permissions](Map(Color.Red -> Permissions.WRITE, Color.Blue -> Permissions.NONE)) + val sj = sjCodecOf[MapHolder[Color, Permissions]](SJConfig.withEnumsAsIds(List("co.blocke.scalajack.json.misc.Color"))) + val js = sj.toJson(inst) + js should matchJson("""{"a":{"0":"WRITE","1":"NONE"}}""") + sj.fromJson(js) shouldEqual (inst) + } + it("Sealed trait enumeration w/case objects must work") { + val inst = FlavorHolder(Chocolate, null, Map(Bourbon -> "a"), Map("a" -> Vanilla)) + val sj = sjCodecOf[FlavorHolder] + val js = sj.toJson(inst) + js should matchJson("""{"f":"Chocolate","f2":null,"f3":{"Bourbon":"a"},"f4":{"a":"Vanilla"}}""") + sj.fromJson(js) shouldEqual (inst) + } + it("Sealed trait enumeration w/case class must work") { + val inst = VehicleHolder(Car(4, "Red"), null, Map("a" -> Truck(18))) + val sj = sjCodecOf[VehicleHolder] + val js = sj.toJson(inst) + js should matchJson("""{"f":{"_hint":"Car","numberOfWheels":4,"color":"Red"},"f2":null,"f4":{"a":{"_hint":"Truck","numberOfWheels":18}}}""") + sj.fromJson(js) shouldEqual (inst) + } + } + describe(colorString("--- Negative Tests ---")) { + it("Enum must break - bad value") { + val sj = sjCodecOf[MapHolder[Color, Color]] + val js = """{"a":{"Red":"Bogus","Green":"Red"}}""" + val ex = intercept[java.lang.IllegalArgumentException](sj.fromJson(js)) + ex.getMessage() shouldEqual ("enum co.blocke.scalajack.json.misc.Color has no case with name: Bogus") + } + it("Enum must break(using id) - bad value") { + val sj = sjCodecOf[MapHolder[Color, Color]](SJConfig.withEnumsAsIds(Nil)) + val js = """{"a":{"0":1,"9":0}}""" + val ex = intercept[java.util.NoSuchElementException](sj.fromJson(js)) + ex.getMessage() shouldEqual ("enum co.blocke.scalajack.json.misc.Color has no case with ordinal: 9") + } + } + } diff --git a/src/test/scala/co.blocke.scalajack/json/misc/LRSpec.scala b/src/test/scala/co.blocke.scalajack/json/misc/LRSpec.scala new file mode 100644 index 00000000..7be3f3dd --- /dev/null +++ b/src/test/scala/co.blocke.scalajack/json/misc/LRSpec.scala @@ -0,0 +1,143 @@ +package co.blocke.scalajack +package json +package misc + +import ScalaJack.* +import co.blocke.scala_reflection.* +import org.scalatest.funspec.AnyFunSpec +import org.scalatest.matchers.should.Matchers.* +import org.scalatest.* +import scala.util.* +import TestUtil.* + +import java.util.UUID + +class LRSpec() extends AnyFunSpec with JsonMatchers: + + describe(colorString("-------------------------------\n: Either Tests :\n-------------------------------", Console.YELLOW)) { + describe(colorString("""/// Right Tests ///""")) { + it("Complex Either/Option must work (non-None)") { + val inst = ComplexEither[Int](Some(Right(Some(3)))) + val sj = sjCodecOf[ComplexEither[Int]] + val js = sj.toJson(inst) + js should matchJson("""{"a":3}""") + sj.fromJson(js) shouldEqual (inst) + } + it("Complex Either/Option must work (None -> null for Either)") { + val inst = ComplexEither[Int](Some(Right(None))) + val sj = sjCodecOf[ComplexEither[Int]] + val js = sj.toJson(inst) + js should matchJson("""{}""") + sj.fromJson(js) shouldEqual (ComplexEither(None)) + } + it("Complex Either/Option must work (NoneAsNull)") { + val inst = ComplexEither[Int](Some(Right(None))) + val sj = sjCodecOf[ComplexEither[Int]](SJConfig.withNoneAsNull()) + val js = sj.toJson(inst) + js should matchJson("""{"a":null}""") // same output result regardless of noneAsNull setting + sj.fromJson(js) shouldEqual (ComplexEither(None)) // None here because value existed, but was null with NoneAsNull + } + } + describe(colorString("""\\\ Left Tests \\\""")) { + it("Complex Either/Option must work (Left-default: AS_VALUE)") { + val inst = ComplexEither[Int](Some(Left("msg"))) + val sj = sjCodecOf[ComplexEither[Int]] + val js = sjCodecOf[ComplexEither[Int]].toJson(inst) + js should matchJson("""{"a":"msg"}""") + sj.fromJson(js) shouldEqual inst + } + it("Complex Either/Option must work (Left-AS_NULL)") { + val inst = ComplexEither[Int](Some(Left("err"))) + val sj = sjCodecOf[ComplexEither[Int]](SJConfig.withEitherLeftHandling(EitherLeftPolicy.AS_NULL)) + val js = sj.toJson(inst) + js should matchJson("""{"a":null}""") + sj.fromJson(js) shouldEqual (ComplexEither(null)) + } + it("Complex Either/Option must work (Left-AS_NULL, Option nullAsNull)") { + val inst = ComplexEither[Int](Some(Left("err"))) + val sj = sjCodecOf[ComplexEither[Int]](SJConfig.withEitherLeftHandling(EitherLeftPolicy.AS_NULL).withNoneAsNull()) + val js = sj.toJson(inst) + js should matchJson("""{"a":null}""") + sj.fromJson(js) shouldEqual (ComplexEither(None)) + } + it("Either with AS_VALUE (default) left policy must work") { + val inst = EitherHolder[Int](Left(5), Right(3)) + val sj = sjCodecOf[EitherHolder[Int]] + val js = sj.toJson(inst) + js should matchJson("""{"a":5,"b":3}""") + sj.fromJson(js) shouldEqual (inst) + } + it("Either with AS_NULL left policy must work") { + val inst = EitherHolder[Int](Left(5), Right(3)) + val sj = sjCodecOf[EitherHolder[Int]](SJConfig.withEitherLeftHandling(EitherLeftPolicy.AS_NULL)) + val js = sj.toJson(inst) + js should matchJson("""{"a":null,"b":3}""") + sj.fromJson(js) shouldEqual (EitherHolder(null, Right(3))) + } + it("Either with ERR_MSG_STRING left policy must work") { + val inst = EitherHolder[Int](Left(5), Right(3)) + val sj = sjCodecOf[EitherHolder[Int]](SJConfig.withEitherLeftHandling(EitherLeftPolicy.ERR_MSG_STRING)) + val js = sj.toJson(inst) + js should matchJson("""{"a":"Left Error: 5","b":3}""") + sj.fromJson(js) shouldEqual (EitherHolder(Right("Left Error: 5"), Right(3))) + // WARNING! Here a Left(err_msg) was "promoted" to a Right(String) upon read, because Right was of type + // String, and "Left Error: 5" is a valid string. Use with extreme caution. Best to consider this a 1-way + // trip for debugging purposes only. You have been warned. + } + it("Either with THROW_EXCEPTION left policy must work") { + val inst = EitherHolder[Int](Left(5), Right(3)) + val caught = + intercept[JsonEitherLeftError] { + sjCodecOf[EitherHolder[Int]](SJConfig.withEitherLeftHandling(EitherLeftPolicy.THROW_EXCEPTION)).toJson(inst) + } + assert(caught.getMessage == "Left Error: 5") + } + } + } + + describe(colorString("----------------------------------\n: Union Tests :\n----------------------------------", Console.YELLOW)) { + it("LR (union) must work with Option (non-None)") { + val inst = LRUnionHolder[Option[Int], String](List(Some(5), "x"), ("y", Some(10))) + val sj = sjCodecOf[LRUnionHolder[Option[Int], String]] + val js = sj.toJson(inst) + js should matchJson("""{"a":[5,"x"],"b":["y",10]}""") + sj.fromJson(js) shouldEqual inst + } + it("LR (union) must work with Option (None)") { + val inst = LRUnionHolder[Option[Int], String](List(None, "x"), ("y", None)) + val sj = sjCodecOf[LRUnionHolder[Option[Int], String]] + val js = sj.toJson(inst) + js should matchJson("""{"a":["x"],"b":["y",null]}""") + sj.fromJson(js) shouldEqual LRUnionHolder[Option[Int], String](List("x"), ("y", None)) + } + it("LR (union) must work with Try of Option (non-None)") { + val inst = LRUnionHolder[Try[Option[Int]], String](List(Success(Some(5)), "x"), ("y", Success(Some(10)))) + val sj = sjCodecOf[LRUnionHolder[Try[Option[Int]], String]] + val js = sj.toJson(inst) + js should matchJson("""{"a":[5,"x"],"b":["y",10]}""") + sj.fromJson(js) shouldEqual (inst) + } + it("LR (union) must work with Try of Option (Success(None))") { + val inst = LRUnionHolder[Try[Option[Int]], String](List(Success(None), "x"), ("y", Success(None))) + val sj = sjCodecOf[LRUnionHolder[Try[Option[Int]], String]] + val js = sj.toJson(inst) + sj.fromJson(js) shouldEqual (LRUnionHolder(List("x"), ("y", null))) // None's get swallowed + } + it("LR (union) must work with Try of Option (Failure)") { + val inst = LRUnionHolder[Try[Option[Int]], String](List(Failure(new Exception("boom")), "x"), ("y", Failure(new Exception("boom2")))) + val sj = sjCodecOf[LRUnionHolder[Try[Option[Int]], String]] + val js = sj.toJson(inst) + js should matchJson("""{"a":[null,"x"],"b":["y",null]}""") + sj.fromJson(js) shouldEqual (LRUnionHolder(List(null, "x"), ("y", null))) // Left's come back as null + } + } + + // describe(colorString("----------------------------------\n: Intersection Tests :\n----------------------------------", Console.YELLOW)) { + // it("LR (intersection) must work") { + // val inst = LRUnionHolder[Option[Int], String](List(Some(5), "x"), ("y", Some(10))) + // val sj = sjCodecOf[LRUnionHolder[Option[Int], String]] + // val js = sj.toJson(inst) + // js should matchJson("""{"a":[5,"x"],"b":["y",10]}""") + // sj.fromJson(js) shouldEqual inst + // } + // } diff --git a/src/test/scala/co.blocke.scalajack/json/misc/MiscSpec.scala b/src/test/scala/co.blocke.scalajack/json/misc/MiscSpec.scala new file mode 100644 index 00000000..31d7380a --- /dev/null +++ b/src/test/scala/co.blocke.scalajack/json/misc/MiscSpec.scala @@ -0,0 +1,92 @@ +package co.blocke.scalajack +package json +package misc + +import ScalaJack.* +import co.blocke.scala_reflection.* +import org.scalatest.funspec.AnyFunSpec +import org.scalatest.matchers.should.Matchers.* +import org.scalatest.* +import scala.util.* +import TestUtil.* + +import java.util.UUID + +class MiscSpec() extends AnyFunSpec with JsonMatchers: + + describe(colorString("-------------------------------\n: Misc Tests :\n-------------------------------", Console.YELLOW)) { + it("String escaping must work (proves escape can be turned off)") { + val inst = StringHolder("""This is a "strange" test +on another level.""") + val sj1 = sjCodecOf[StringHolder] + val js1 = sj1.toJson(inst) + val sj2 = sjCodecOf[StringHolder](SJConfig.suppressEscapedStrings()) + val js2 = sj2.toJson(inst) + js1 should equal("""{"a":"This is a \"strange\" test\non another level."}""") + js2 should equal("""{"a":"This is a "strange" test +on another level."}""") + sj1.fromJson(js1) shouldEqual (inst) + val msg = + """Expected ',' or '}' but found 's' at position [17] + |{"a":"This is a "strange" test~on another level."} + |-----------------^""".stripMargin + val ex = intercept[JsonParseError](sj2.fromJson(js2)) + ex.show shouldEqual msg + } + it("NeoType integration must work") { + val inst = Validated(NonEmptyString("Mike"), XList(List("x", "y", "z")), List(EmptyString(""), EmptyString(""), EmptyString(""))) + val sj = sjCodecOf[Validated] + val js = sj.toJson(inst) + js should equal("""{"name":"Mike","xspot":["x","y","z"],"nada":["","",""]}""") + sj.fromJson(js) shouldEqual (inst) + } + it("NeoType validation must work (test failure)") { + val sj = sjCodecOf[Validated] + val js = """{"name":"","xspot":["x","y","z"],"nada":["","",""]}""" + val msg = + """NeoType validation for NonEmptyString failed at position [9] + |{"name":"","xspot":["x","y","z"],"nada":["","",""]} + |---------^""".stripMargin + val ex = intercept[JsonParseError](sj.fromJson(js)) + ex.show shouldEqual msg + } + it("Any type must work (non-exhaustive test)") { + val inst = AnyHolder( + Some(List(1, 2, 3)), + None, + TryHolder(Success(-5)), + Success(99), + Failure(new Exception("oops")), + Map("a" -> 1, "b" -> 2), + Right(3), + Left("nope"), + (Some('a'), None, Some('b')) + ) + val sj = sjCodecOf[AnyHolder] + val js = sj.toJson(inst) + js should equal("""{"maybe":[1,2,3],"itried":{"a":-5},"itried2":99,"ifailed":null,"anymap":{"a":1,"b":2},"whichOneR":3,"whichOneL":"nope","bunch":["a",null,"b"]}""") + sj.fromJson(js) shouldEqual (AnyHolder(List(1, 2, 3), null, Map("a" -> -5), 99, null, Map("a" -> 1, "b" -> 2), 3, "nope", List("a", null, "b"))) + } + it("Any type must work (none as null)") { + val inst = AnyHolder( + Some(List(1, 2, 3)), + None, + TryHolder(Success(-5)), + Success(99), + Failure(new Exception("oops")), + Map("a" -> 1, "b" -> 2), + Right(3), + Left("nope"), + (Some('a'), None, Some('b')) + ) + val sj = sjCodecOf[AnyHolder]( + SJConfig + .withNoneAsNull() + .withEitherLeftHandling(EitherLeftPolicy.ERR_MSG_STRING) + .withTryFailureHandling(TryPolicy.ERR_MSG_STRING) + ) + val js = sj.toJson(inst) + js should equal("""{"maybe":[1,2,3],"maybeNot":null,"itried":{"a":-5},"itried2":99,"ifailed":"Try Failure with msg: oops","anymap":{"a":1,"b":2},"whichOneR":3,"whichOneL":"Left Error: nope","bunch":["a",null,"b"]}""") + sj.fromJson(js) shouldEqual (AnyHolder(List(1, 2, 3), null, Map("a" -> -5), 99, "Try Failure with msg: oops", Map("a" -> 1, "b" -> 2), 3, "Left Error: nope", List("a", null, "b"))) + } + } diff --git a/src/test/scala/co.blocke.scalajack/json/misc/Model.scala b/src/test/scala/co.blocke.scalajack/json/misc/Model.scala new file mode 100644 index 00000000..4e27fa5b --- /dev/null +++ b/src/test/scala/co.blocke.scalajack/json/misc/Model.scala @@ -0,0 +1,138 @@ +package co.blocke.scalajack +package json +package misc + +import java.util.Optional +import scala.util.* +import neotype.* + +case class Person(name: String, age: Int) + +case class OptionHolder[T]( + a: Option[T], // straight Option + b: (Option[T], String), // tuple w/Option + c: List[Option[T]], // Seq of Option + d: Map[Int, Option[T]], // Map of Option + e: T | Option[T], // Union of Option (R) + f: Option[T] | T, // Union of Option (L) + g: Option[Option[T]], // Nested Option + h: Option[Person], // Option of Class + i: Either[T, Option[T]], // Either of Option (R) + j: Either[Option[T], T] // Either of Option (L) +) + +case class OptionalHolder[T]( + a: Optional[T], // straight Optional + b: (Optional[T], String), // tuple w/Optional + c: List[Optional[T]], // Seq of Optional + d: Map[Int, Optional[T]], // Map of Optional + e: T | Optional[T], // Union of Optional (R) + f: Optional[T] | T, // Union of Optional (L) + g: Optional[Optional[T]], // Nested Optional + h: Optional[Person], // Optional of Class + i: Either[T, Optional[T]], // Either of Optional (R) + j: Either[Optional[T], T] // Either of Optional (L) +) + +case class SimpleOptionHolder[T](a: Option[T]) +case class TryHolder[T](a: Try[T]) +case class TryHolder2[T](a: Seq[Try[T]], b: (Try[T], Try[T])) + +case class LRUnionHolder[T, U](a: Seq[T | U], b: (T | U, T | U)) +case class LRUnionHolder2[T, U](a: Seq[Boolean | Int], b: (T | U, T | U)) +case class EitherHolder[T](a: Either[T, String], b: Either[String, T]) + +case class ComplexEither[T](a: Option[Either[String, Option[T]]]) +case class EitherRecipe[T](a: Either[Boolean, Either[Option[T], String]]) +case class EitherRecipeJ[T](a: Either[Boolean, Either[Optional[T], String]]) + +case class AliasHolder[T](a: T, b: List[T], c: Map[T, String], d: Map[String, T]) +case class AliasHolder2[T](a: T, b: List[T], c: Map[String, T]) + +case class StringHolder(a: String) + +case class MapHolder[T, V](a: Map[T, V]) + +type NonEmptyString = NonEmptyString.Type +given NonEmptyString: Newtype[String] with + override inline def validate(input: String): Boolean = + input.nonEmpty + +type XList = XList.Type +given XList: Newtype[List[String]] with + override inline def validate(input: List[String]): Boolean = + input.nonEmpty && input(0) == "x" + +type EmptyString = EmptyString.Type +given EmptyString: Newtype[String] with + override inline def validate(input: String): Boolean = + input.isEmpty + +case class Validated(name: NonEmptyString, xspot: XList, nada: List[EmptyString]) + +case class AnyHolder( + maybe: Any, // Option[List[String]] <- Some + maybeNot: Any, // None + itried: Any, // TryHolder[Int] <- class test + itried2: Any, // Try[Int] (Success) + ifailed: Any, // Try[Int] (Failure) + anymap: Any, // Map[String,Int] + whichOneR: Any, // Either[String,Int] <- right + whichOneL: Any, // Either[String,Int] <- left + bunch: Any // (Some('a'),None,Some('b')) +) + +object Size extends Enumeration { + val Small, Medium, Large = Value +} +object SizeWithType extends Enumeration { + type SizeWithType = Value + val Little, Grand = Value +} +import SizeWithType.* + +object Permissions extends Enumeration { + type Permissions = Value + val READ, WRITE, EXEC, NONE = Value +} + +// case class SampleEnum(e1: Enumeration#Value, e2: Enumeration#Value, e3: Enumeration#Value, e4: Enumeration#Value, e5: Enumeration#Value, e6: SizeWithType) +case class SampleEnum(e1: Size.Value, e2: Size.Value, e3: Size.Value, e4: Size.Value, e5: Size.Value, e6: SizeWithType) + +enum Color { + case Red, Blue, Green +} +case class TVColors(color1: Color, color2: Color) + +sealed trait Flavor +case object Vanilla extends Flavor +case object Chocolate extends Flavor +case object Bourbon extends Flavor +case class FlavorHolder(f: Flavor, f2: Flavor, f3: Map[Flavor, String], f4: Map[String, Flavor]) + +sealed trait Vehicle +case class Truck(numberOfWheels: Int) extends Vehicle +case class Car(numberOfWheels: Int, color: String) extends Vehicle +case class Plane(numberOfEngines: Int) extends Vehicle +case class VehicleHolder(f: Vehicle, f2: Vehicle, f4: Map[String, Vehicle]) + +/* + +TODO: FUTURE + +To implement Intersection types we must be able to support non-sealed traits. +To do that, we need a full runtime mirror of everything else in ScalaJack that +happens at compile-time (everything, basically). Not sure if/when that will +happen, so for now... no Intersection type support. + +trait Scissors: + val isSharp: Boolean + def cut: Unit +trait Needle: + val bobbins: Int + def sew: Unit +case class DressFixer(isSharp: Boolean, bobbins: Int) extends Scissors, Needle: + override def cut = println("Cut") + override def sew = println("Sew") +case class LRInterHolder[T, U](a: Seq[T | U], b: (T | U, T | U)) + */ diff --git a/src/test/scala/co.blocke.scalajack/json/misc/OptionSpec.scala b/src/test/scala/co.blocke.scalajack/json/misc/OptionSpec.scala new file mode 100644 index 00000000..4d0171b9 --- /dev/null +++ b/src/test/scala/co.blocke.scalajack/json/misc/OptionSpec.scala @@ -0,0 +1,187 @@ +package co.blocke.scalajack +package json +package misc + +import ScalaJack.* +import co.blocke.scala_reflection.* +import org.scalatest.funspec.AnyFunSpec +import org.scalatest.matchers.should.Matchers.* +import org.scalatest.* +import scala.util.* +import java.util.Optional +import TestUtil.* + +import java.util.UUID + +class OptionSpec() extends AnyFunSpec with JsonMatchers: + + describe(colorString("-------------------------------\n: Option Tests :\n-------------------------------", Console.YELLOW)) { + describe(colorString("+++ Scala Option +++")) { + it("Non-empty Options must work") { + val inst = OptionHolder[Int]( + Some(5), // straight Option + (Some(1), "ok"), // tuple w/Option + List(Some(1), None, Some(3)), // Seq of Option + Map(1 -> Some(2), 3 -> Some(4)), // Map of Option + Some(99), // Union of Option (R) + Some(100), // Union of Option (L) + Some(Some(0)), // Nested Option + Some(Person("BoB", 34)), // Option of class + Right(Some(15)), // Either of Option (R) + Left(Some(-3)) // Either of Option (L) + ) + val sj = sjCodecOf[OptionHolder[Int]] + val js = sj.toJson(inst) + js should matchJson("""{"a":5,"b":[1,"ok"],"c":[1,3],"d":{"1":2,"3":4},"e":99,"f":100,"g":0,"h":{"name":"BoB","age":34},"i":15,"j":-3}""") + // Some fields get changed/"promoted" when read back in per policy, as Either reads "best-fit" starting with Right value first + sj.fromJson(js) shouldEqual (inst.copy(c = List(Some(1), Some(3))).copy(e = 99).copy(j = Right(-3))) + } + it("Empty Options must work (default)") { + val inst = OptionHolder[Int]( + None, // straight Option + (None, "ok"), // tuple w/Option + List(None, None, None), // Seq of Option + Map(1 -> None, 3 -> None), // Map of Option + None, // Union of Option (R) + None, // Union of Option (L) + Some(None), // Nested Option + None, // Option of class + Right(None), // Either of Option (R) + Left(None) // Either of Option (L) + ) + val sj = sjCodecOf[OptionHolder[Int]] + val js = sj.toJson(inst) + js should matchJson("""{"b":[null,"ok"],"c":[],"d":{}}""") + sj.fromJson(js) shouldEqual (inst.copy(c = List(), d = Map(), g = None)) + } + it("Empty Options must work (config noneAsNull = true)") { + val inst = OptionHolder[Int]( + None, // straight Option + (None, "ok"), // tuple w/Option + List(None, None, None), // Seq of Option + Map(1 -> None, 3 -> None), // Map of Option + None, // Union of Option (R) + None, // Union of Option (L) + Some(None), // Nested Option + None, // Option of class + Right(None), // Either of Option (R) + Left(None) // Either of Option (L) + ) + val sj = sjCodecOf[OptionHolder[Int]]( + SJConfig.withNoneAsNull() + ) + val js = sj.toJson(inst) + js should matchJson("""{"a":null,"b":[null,"ok"],"c":[null,null,null],"d":{"1":null,"3":null},"e":null,"f":null,"g":null,"h":null,"i":null,"j":null}""") + sj.fromJson(js) shouldEqual (inst.copy(g = None, i = null, j = null)) + } + it("Either recipe should work (non-None)") { + val inst = EitherRecipe[Int](Right(Left(Some(5)))) + val sj = sjCodecOf[EitherRecipe[Int]] + val js = sj.toJson(inst) + js should matchJson("""{"a":5}""") + sj.fromJson(js) shouldEqual (inst) + } + it("Either recipe should work (None)") { + val inst = EitherRecipe[Int](Right(Left(None))) + val sj = sjCodecOf[EitherRecipe[Int]] + val js = sj.toJson(inst) + js should matchJson("""{}""") + sj.fromJson(js) shouldEqual (inst) + } + it("Either recipe should work (None as null)") { + val inst = EitherRecipe[Int](Right(Left(None))) + val sj = sjCodecOf[EitherRecipe[Int]](SJConfig.withNoneAsNull()) + val js = sj.toJson(inst) + js should matchJson("""{"a":null}""") + sj.fromJson(js) shouldEqual (EitherRecipe[Int](null)) + } + } + + describe(colorString("+++ Java Optional +++")) { + it("Non-empty Options must work") { + val inst = OptionalHolder[Int]( + Optional.of(5), // straight Optional + (Optional.of(1), "ok"), // tuple w/Optional + List(Optional.of(1), Optional.empty, Optional.of(3)), // Seq of Optional + Map(1 -> Optional.of(2), 3 -> Optional.of(4)), // Map of Optional + Optional.of(99), // Union of Optional (R) + Optional.of(100), // Union of Optional (L) + Optional.of(Optional.of(0)), // Nested Optional + Optional.of(Person("BoB", 34)), // Optional of class + Right(Optional.of(15)), // Either of Optional (R) + Left(Optional.of(-3)) // Either of Optional (L) + ) + val sj = sjCodecOf[OptionalHolder[Int]] + val js = sj.toJson(inst) + js should matchJson("""{"a":5,"b":[1,"ok"],"c":[1,3],"d":{"1":2,"3":4},"e":99,"f":100,"g":0,"h":{"name":"BoB","age":34},"i":15,"j":-3}""") + // Some fields get changed/"promoted" when read back in per policy, as Either reads "best-fit" starting with Right value first + sj.fromJson(js) shouldEqual (inst.copy(c = List(Optional.of(1), Optional.of(3))).copy(e = 99).copy(j = Right(-3))) + } + it("Empty Options must work (default)") { + val inst = OptionalHolder[Int]( + Optional.empty, // straight Option + (Optional.empty, "ok"), // tuple w/Option + List(Optional.empty, Optional.empty, Optional.empty), // Seq of Option + Map(1 -> Optional.empty, 3 -> Optional.empty), // Map of Option + Optional.empty, // Union of Option (R) + Optional.empty, // Union of Option (L) + Optional.of(Optional.empty), // Nested Option + Optional.empty, // Option of class + Right(Optional.empty), // Either of Option (R) + Left(Optional.empty) // Either of Option (L) + ) + val sj = sjCodecOf[OptionalHolder[Int]] + val js = sj.toJson(inst) + js should matchJson("""{"b":[null,"ok"],"c":[],"d":{}}""") + sj.fromJson(js) shouldEqual (inst.copy(c = List(), d = Map(), g = Optional.empty)) + } + it("Empty Options must work (config noneAsNull = true)") { + val inst = OptionalHolder[Int]( + Optional.empty, // straight Option + (Optional.empty, "ok"), // tuple w/Option + List(Optional.empty, Optional.empty, Optional.empty), // Seq of Option + Map(1 -> Optional.empty, 3 -> Optional.empty), // Map of Option + Optional.empty, // Union of Option (R) + Optional.empty, // Union of Option (L) + Optional.of(Optional.empty), // Nested Option + Optional.empty, // Option of class + Right(Optional.empty), // Either of Option (R) + Left(Optional.empty) // Either of Option (L) + ) + val sj = sjCodecOf[OptionalHolder[Int]]( + SJConfig.withNoneAsNull() + ) + val js = sj.toJson(inst) + js should matchJson("""{"a":null,"b":[null,"ok"],"c":[null,null,null],"d":{"1":null,"3":null},"e":null,"f":null,"g":null,"h":null,"i":null,"j":null}""") + sj.fromJson(js) shouldEqual (inst.copy(g = Optional.empty, i = null, j = null)) + } + it("Either recipe should work (non-None)") { + val inst = EitherRecipeJ[Int](Right(Left(Optional.of(5)))) + val sj = sjCodecOf[EitherRecipeJ[Int]] + val js = sj.toJson(inst) + js should matchJson("""{"a":5}""") + sj.fromJson(js) shouldEqual (inst) + } + it("Either recipe should work (None)") { + val inst = EitherRecipeJ[Int](Right(Left(Optional.empty))) + val sj = sjCodecOf[EitherRecipeJ[Int]] + val js = sj.toJson(inst) + js should matchJson("""{}""") + sj.fromJson(js) shouldEqual (inst) + } + it("Either recipe should work (None as null)") { + val inst = EitherRecipeJ[Int](Right(Left(Optional.empty))) + val sj = sjCodecOf[EitherRecipeJ[Int]](SJConfig.withNoneAsNull()) + val js = sj.toJson(inst) + js should matchJson("""{"a":null}""") + sj.fromJson(js) shouldEqual (EitherRecipeJ[Int](null)) + } + it("Option of Either should work") { + val inst = SimpleOptionHolder[Either[Boolean, Int]](Some(Right(5))) + val sj = sjCodecOf[SimpleOptionHolder[Either[Boolean, Int]]] + val js = sj.toJson(inst) + js should matchJson("""{"a":5}""") + sj.fromJson(js) shouldEqual (inst) + } + } + } diff --git a/src/test/scala/co.blocke.scalajack/json/misc/TrySpec.scala b/src/test/scala/co.blocke.scalajack/json/misc/TrySpec.scala new file mode 100644 index 00000000..c3f59374 --- /dev/null +++ b/src/test/scala/co.blocke.scalajack/json/misc/TrySpec.scala @@ -0,0 +1,96 @@ +package co.blocke.scalajack +package json +package misc + +import ScalaJack.* +import co.blocke.scala_reflection.* +import org.scalatest.funspec.AnyFunSpec +import org.scalatest.matchers.should.Matchers.* +import org.scalatest.* +import scala.util.* +import java.util.Optional +import TestUtil.* + +import java.util.UUID + +class TrySpec() extends AnyFunSpec with JsonMatchers: + + describe(colorString("-------------------------------\n: Try Tests :\n-------------------------------", Console.YELLOW)) { + describe(colorString("+++ Positive Tests +++")) { + it("Try must work (Success)") { + val inst = TryHolder(Success(15)) + val sj = sjCodecOf[TryHolder[Int]] + val js = sj.toJson(inst) + js should matchJson("""{"a":15}""") + sj.fromJson(js) shouldEqual (inst) + } + it("Try of Option (non-None) must work (Success)") { + val inst = TryHolder[Option[Int]](Success(Some(15))) + val sj = sjCodecOf[TryHolder[Option[Int]]] + val js = sj.toJson(inst) + js should matchJson("""{"a":15}""") + sj.fromJson(js) shouldEqual (inst) + } + it("Try of Option (None) must work (Success)") { + val inst = TryHolder[Option[Int]](Success(None)) + val sj = sjCodecOf[TryHolder[Option[Int]]] + val js = sj.toJson(inst) + js should matchJson("""{}""") + sj.fromJson(js) shouldEqual (inst) + } + it("Try of Optional (non-None) must work (Success)") { + val inst = TryHolder[Optional[Int]](Success(Optional.of(15))) + val sj = sjCodecOf[TryHolder[Optional[Int]]] + val js = sj.toJson(inst) + js should matchJson("""{"a":15}""") + sj.fromJson(js) shouldEqual (inst) + } + it("Try of Optional (None) must work (Success)") { + val inst = TryHolder[Optional[Int]](Success(Optional.empty)) + val sj = sjCodecOf[TryHolder[Optional[Int]]] + val js = sj.toJson(inst) + js should matchJson("""{}""") + sj.fromJson(js) shouldEqual (inst) + } + it("Try of Either must work (Success)") { + val inst = TryHolder[Either[Boolean, Int]](Success(Right(5))) + val sj = sjCodecOf[TryHolder[Either[Boolean, Int]]] + val js = sj.toJson(inst) + js should matchJson("""{"a":5}""") + sj.fromJson(js) shouldEqual (inst) + } + it("Try w/policy AS_NULL must work (Failure)") { + val inst = TryHolder[Int](Failure(new Exception("boom"))) + val sj = sjCodecOf[TryHolder[Int]](SJConfig.withTryFailureHandling(TryPolicy.AS_NULL)) + val js = sj.toJson(inst) + js should matchJson("""{"a":null}""") + sj.fromJson(js) shouldEqual (TryHolder[Int](null)) + } + it("Try w/policy ERR_MSG_STRING must work (Failure)") { + val inst = TryHolder[Int](Failure(new Exception("boom"))) + val sj = sjCodecOf[TryHolder[Int]](SJConfig.withTryFailureHandling(TryPolicy.ERR_MSG_STRING)) + val js = sj.toJson(inst) + js should matchJson("""{"a":"Try Failure with msg: boom"}""") + val msg = """Unsuccessful attempt to read Try type with failure: Non-numeric character found when integer value expected at position 5 at position [5] + |{"a":"Try Failure with msg: boom"} + |-----^""".stripMargin + val err = intercept[JsonParseError](sj.fromJson(js)) + err.show shouldEqual msg + } + it("Try w/policy ATHROW_EXCEPTIONS_NULL must work (Failure)") { + val inst = TryHolder[Int](Failure(new Exception("boom"))) + val caught = + intercept[java.lang.Exception] { + sjCodecOf[TryHolder[Int]](SJConfig.withTryFailureHandling(TryPolicy.THROW_EXCEPTION)).toJson(inst) + } + assert(caught.getMessage == "boom") + } + it("Seq and Tuple of Try must work for AS_NULL (Failure)") { + val inst = TryHolder2[Int](List(Success(1), Failure(new Exception("boom")), Success(3)), (Failure(new Exception("boom")), Success(0))) + val sj = sjCodecOf[TryHolder2[Int]](SJConfig.withTryFailureHandling(TryPolicy.AS_NULL)) + val js = sj.toJson(inst) + js should matchJson("""{"a":[1,null,3],"b":[null,0]}""") + sj.fromJson(js) shouldEqual (TryHolder2[Int](List(Success(1), null, Success(3)), (null, Success(0)))) + } + } + } diff --git a/src/test/scala/co.blocke.scalajack/json/primitives/JavaPrimSpec.scala b/src/test/scala/co.blocke.scalajack/json/primitives/JavaPrimSpec.scala new file mode 100644 index 00000000..9a2d6c2a --- /dev/null +++ b/src/test/scala/co.blocke.scalajack/json/primitives/JavaPrimSpec.scala @@ -0,0 +1,288 @@ +package co.blocke.scalajack +package json +package primitives + +import ScalaJack.* +import co.blocke.scala_reflection.* +import org.scalatest.funspec.AnyFunSpec +import org.scalatest.matchers.should.Matchers.* +import org.scalatest.* +import TestUtil.* + +import java.lang.{Boolean as JBoolean, Byte as JByte, Double as JDouble, Float as JFloat, Integer as JInt, Long as JLong, Short as JShort} +import java.math.{BigDecimal as JBigDecimal, BigInteger as JBigInteger} + +class JavaPrimSpec() extends AnyFunSpec with JsonMatchers: + + describe(colorString("--------------------------\n: Java Primitive Tests :\n--------------------------", Console.YELLOW)) { + describe(colorString("+++ Positive Tests +++")) { + it("BigDecimal must work") { + val inst = SampleJBigDecimal( + JBigDecimal.ZERO, + JBigDecimal.ONE, + JBigDecimal.TEN, + new JBigDecimal( + "0.1499999999999999944488848768742172978818416595458984375" + ), + null + ) + val js = sjCodecOf[SampleJBigDecimal].toJson(inst) + js should matchJson("""{"bd1":0,"bd2":1,"bd3":10,"bd4":0.1499999999999999944488848768742172978818416595458984375,"bd5":null}""") + sjCodecOf[SampleJBigDecimal].fromJson(js) shouldEqual inst + } + + it("BigInteger must work") { + val inst = SampleJBigInteger( + JBigInteger.ZERO, + JBigInteger.ONE, + JBigInteger.TEN, + new JBigInteger("-90182736451928374653345"), + new JBigInteger("90182736451928374653345"), + new JBigInteger("0"), + null + ) + val js = sjCodecOf[SampleJBigInteger].toJson(inst) + js should matchJson("""{"bi1":0,"bi2":1,"bi3":10,"bi4":-90182736451928374653345,"bi5":90182736451928374653345,"bi6":0,"bi7":null}""") + sjCodecOf[SampleJBigInteger].fromJson(js) shouldEqual inst + } + + it("Boolean must work") { + val inst = SampleJBoolean(JBoolean.TRUE, JBoolean.FALSE, true, false, null) + val js = sjCodecOf[SampleJBoolean].toJson(inst) + js should matchJson("""{"bool1":true,"bool2":false,"bool3":true,"bool4":false,"bool5":null}""") + sjCodecOf[SampleJBoolean].fromJson(js) shouldEqual inst + } + + it("Byte must work") { + val inst = SampleJByte( + JByte.MAX_VALUE, + JByte.MIN_VALUE, + 0.asInstanceOf[Byte], + 64.asInstanceOf[Byte], + null + ) + val js = sjCodecOf[SampleJByte].toJson(inst) + js should matchJson("""{"b1":127,"b2":-128,"b3":0,"b4":64,"b5":null}""") + sjCodecOf[SampleJByte].fromJson(js) shouldEqual inst + } + + it("Character must work") { + val inst = SampleJChar('Z', '\u20A0', null) + val js = sjCodecOf[SampleJChar].toJson(inst) + js should matchJson("""{"c1":"Z","c2":"\""" + """u20a0","c3":null}""") + sjCodecOf[SampleJChar].fromJson(js) shouldEqual inst + } + + it("Double must work") { + val inst = SampleJDouble( + JDouble.MAX_VALUE, + JDouble.MIN_VALUE, + 0.0, + -123.4567, + null + ) + val js = sjCodecOf[SampleJDouble].toJson(inst) + js should matchJson("""{"d1":1.7976931348623157E308,"d2":4.9E-324,"d3":0.0,"d4":-123.4567,"d5":null}""") + sjCodecOf[SampleJDouble].fromJson(js) shouldEqual inst + } + + it("Float must work") { + val inst = SampleJFloat( + JFloat.MAX_VALUE, + JFloat.MIN_VALUE, + 0.0f, + -123.4567f, + null + ) + val js = sjCodecOf[SampleJFloat].toJson(inst) + js should matchJson("""{"f1":3.4028235E38,"f2":1.4E-45,"f3":0.0,"f4":-123.4567,"f5":null}""") + sjCodecOf[SampleJFloat].fromJson(js) shouldEqual inst + } + + it("Integer must work") { + val inst = SampleJInt(JInt.MAX_VALUE, JInt.MIN_VALUE, 0, 123, null) + val js = sjCodecOf[SampleJInt].toJson(inst) + js should matchJson("""{"i1":2147483647,"i2":-2147483648,"i3":0,"i4":123,"i5":null}""") + sjCodecOf[SampleJInt].fromJson(js) shouldEqual inst + } + + it("Long must work") { + val inst = SampleJLong(JLong.MAX_VALUE, JLong.MIN_VALUE, 0L, 123L, null) + val js = sjCodecOf[SampleJLong].toJson(inst) + js should matchJson("""{"l1":9223372036854775807,"l2":-9223372036854775808,"l3":0,"l4":123,"l5":null}""") + sjCodecOf[SampleJLong].fromJson(js) shouldEqual inst + } + + it("Number must work") { + val inst = SampleJNumber( + JByte.valueOf("-128"), + JByte.valueOf("127"), + JShort.valueOf("-32768"), + JShort.valueOf("32767"), + JInt.valueOf("-2147483648"), + JInt.valueOf("2147483647"), + JLong.valueOf("-9223372036854775808"), + JLong.valueOf("9223372036854755807"), + null, // new JBigInteger("9923372036854755810"), + JByte.valueOf("0"), + JFloat.valueOf("3.4e-038"), + JFloat.valueOf("3.4e+038"), + JDouble.valueOf("1.7e-308"), + JDouble.valueOf("1.7e+308"), + null, // new JBigDecimal("1.8e+308"), + JFloat.valueOf("0.0"), + null + ) + val js = sjCodecOf[SampleJNumber].toJson(inst) + js should matchJson( + """{"n1":-128,"n2":127,"n3":-32768,"n4":32767,"n5":-2147483648,"n6":2147483647,"n7":-9223372036854775808,"n8":9223372036854755807,"n9":null,"n10":0,"n11":3.4E-38,"n12":3.4E38,"n13":1.7E-308,"n14":1.7E308,"n15":null,"n16":0.0,"n17":null}""" + ) + sjCodecOf[SampleJNumber].fromJson(js) shouldEqual inst + } + + it("Short must work") { + val inst = SampleJShort( + JShort.MAX_VALUE, + JShort.MIN_VALUE, + 0.asInstanceOf[Short], + 123.asInstanceOf[Short], + null + ) + val js = sjCodecOf[SampleJShort].toJson(inst) + js should matchJson("""{"s1":32767,"s2":-32768,"s3":0,"s4":123,"s5":null}""") + sjCodecOf[SampleJShort].fromJson(js) shouldEqual inst + } + } + + // -------------------------------------------------------- + + describe(colorString("--- Negative Tests ---")) { + it("BigDecimal must break") { + val js = + """{"bd1":0,"bd2":1,"bd3":10,"bd4":"0.1499999999999999944488848768742172978818416595458984375","bd5":null}""" + val msg = + """Expected a numerical value or null here at position [32] + |{"bd1":0,"bd2":1,"bd3":10,"bd4":"0.149999999999999994448884876874217297881841... + |--------------------------------^""".stripMargin + val ex = intercept[JsonParseError](sjCodecOf[SampleJBigDecimal].fromJson(js)) + ex.show shouldEqual msg + } + + it("BigInt must break") { + val js = + """{"bi1":"0","bi2":1,"bi3":10,"bi4":-90182736451928374653345,"bi5":90182736451928374653345,"bi6":0,"bi7":null}""" + val msg = + """Expected a numerical value or null here at position [7] + |{"bi1":"0","bi2":1,"bi3":10,"bi4":-90182736451928374653345,"bi5":901827364519... + |-------^""".stripMargin + val ex = intercept[JsonParseError](sjCodecOf[SampleJBigInteger].fromJson(js)) + ex.show shouldEqual msg + } + + it("Boolean must break") { + val js = """{"bool1":true,"bool2":false,"bool3":true,"bool4":"false","bool5":null}""" + val msg = + """Expected 'true', 'false', or null here at position [49] + |{"bool1":true,"bool2":false,"bool3":true,"bool4":"false","bool5":null} + |-------------------------------------------------^""".stripMargin + val ex = intercept[JsonParseError](sjCodecOf[SampleJBoolean].fromJson(js)) + ex.show shouldEqual msg + } + + it("Byte must break") { + val js = """{"b1":127,"b2":-128,"b3":false,"b4":64,"b5":null}""" + val msg = """Expected a numerical value or null here at position [25] + |{"b1":127,"b2":-128,"b3":false,"b4":64,"b5":null} + |-------------------------^""".stripMargin + val ex = intercept[JsonParseError](sjCodecOf[SampleJByte].fromJson(js)) + ex.show shouldEqual msg + } + + it("Char must break") { + val sj = sjCodecOf[SampleJChar] + val js = """{"c1":"Z","c2":3,"c3":null}""" + val msg = """Expected a String value but got '3' at position [15] + |{"c1":"Z","c2":3,"c3":null} + |---------------^""".stripMargin + val ex = intercept[JsonParseError](sjCodecOf[SampleJChar].fromJson(js)) + ex.show shouldEqual msg + val js2 = """{"c1":"Z","c2":"","c3":null}""" + val msg2 = """Character value expected but empty string found in json at position [15] + |{"c1":"Z","c2":"","c3":null} + |---------------^""".stripMargin + val ex2 = intercept[JsonParseError](sjCodecOf[SampleJChar].fromJson(js2)) + ex2.show shouldEqual msg2 + } + + it("Double must break") { + val js = + """{"d1":1.7976931348623157E308,"d2":4.9E-324,"d3":"0.0","d4":-123.4567,"d5":null}""" + val msg = + """Expected a numerical value or null here at position [48] + |{"d1":1.7976931348623157E308,"d2":4.9E-324,"d3":"0.0","d4":-123.4567,"d5":null} + |------------------------------------------------^""".stripMargin + val ex = intercept[JsonParseError](sjCodecOf[SampleJDouble].fromJson(js)) + ex.show shouldEqual msg + } + + it("Float must break") { + val js = + """{"f1":3.4028235E38,"f2":"1.4E-45","f3":0.0,"f4":-123.4567,"f5":null}""" + val msg = + """Expected a numerical value or null here at position [24] + |{"f1":3.4028235E38,"f2":"1.4E-45","f3":0.0,"f4":-123.4567,"f5":null} + |------------------------^""".stripMargin + val ex = intercept[JsonParseError](sjCodecOf[SampleJFloat].fromJson(js)) + ex.show shouldEqual msg + } + + it("Int must break") { + val js = """{"i1":2147483647,"i2":-2147483648,"i3":false,"i4":123,"i5":null}""" + val msg = + """Expected a numerical value or null here at position [39] + |{"i1":2147483647,"i2":-2147483648,"i3":false,"i4":123,"i5":null} + |---------------------------------------^""".stripMargin + val ex = intercept[JsonParseError](sjCodecOf[SampleJInt].fromJson(js)) + ex.show shouldEqual msg + val js2 = """{"i1":2147483647,"i2":-2147483648,"i3":0.3,"i4":123,"i5":null}""" + the[java.lang.NumberFormatException] thrownBy sjCodecOf[SampleJInt].fromJson(js2) should have message "For input string: \"0.3\"" + } + + it("Long must break") { + val js = + """{"l1":9223372036854775807,"l2":-9223372036854775808,"l3":"0","l4":123,"l5":null}""" + val msg = + """Expected a numerical value or null here at position [57] + |...23372036854775807,"l2":-9223372036854775808,"l3":"0","l4":123,"l5":null} + |----------------------------------------------------^""".stripMargin + val ex = intercept[JsonParseError](sjCodecOf[SampleJLong].fromJson(js)) + ex.show shouldEqual msg + val js2 = + """{"l1":9223372036854775807,"l2":-9223372036854775808,"l3":0.3,"l4":123,"l5":null}""" + the[java.lang.NumberFormatException] thrownBy sjCodecOf[SampleJLong].fromJson(js2) should have message "For input string: \"0.3\"" + } + + it("Number must break") { + val js = + """{"n1":-128,"n2":127,"n3":"-32768","n4":32767,"n5":-2147483648,"n6":2147483647,"n7":-9223372036854775808,"n8":9223372036854755807,"n9":9923372036854755810,"n10":0,"n11":3.4E-38,"n12":3.4E38,"n13":1.7E-308,"n14":1.7E308,"n15":1.8E+308,"n16":0.0,"n17":null}""" + val msg = + """Expected a numerical value or null here at position [25] + |{"n1":-128,"n2":127,"n3":"-32768","n4":32767,"n5":-2147483648,"n6":2147483647... + |-------------------------^""".stripMargin + val ex = intercept[JsonParseError](sjCodecOf[SampleJNumber].fromJson(js)) + ex.show shouldEqual msg + } + + it("Short must break") { + val sj = sjCodecOf[SampleJByte] + val js = """{"s1":false,"s2":-32768,"s3":0,"s4":123,"s5":null}""" + val msg = """Expected a numerical value or null here at position [6] + |{"s1":false,"s2":-32768,"s3":0,"s4":123,"s5":null} + |------^""".stripMargin + val ex = intercept[JsonParseError](sjCodecOf[SampleJShort].fromJson(js)) + ex.show shouldEqual msg + val js2 = """{"s1":2.3,"s2":-32768,"s3":0,"s4":123,"s5":null}""" + the[java.lang.NumberFormatException] thrownBy sjCodecOf[SampleJShort].fromJson(js2) should have message "For input string: \"2.3\"" + } + } + } diff --git a/core/src/test/scala/co.blocke.scalajack/yaml/primitives/Model.scala b/src/test/scala/co.blocke.scalajack/json/primitives/Model.scala similarity index 60% rename from core/src/test/scala/co.blocke.scalajack/yaml/primitives/Model.scala rename to src/test/scala/co.blocke.scalajack/json/primitives/Model.scala index 4e72e00b..db85415f 100644 --- a/core/src/test/scala/co.blocke.scalajack/yaml/primitives/Model.scala +++ b/src/test/scala/co.blocke.scalajack/json/primitives/Model.scala @@ -1,35 +1,20 @@ package co.blocke.scalajack -package yaml.primitives +package json.primitives import java.util.UUID -import java.lang.{ - Boolean => JBoolean, - Byte => JByte, - Character => JChar, - Double => JDouble, - Float => JFloat, - Integer => JInt, - Long => JLong, - Number => JNumber, - Short => JShort -} -import java.math.{BigDecimal => JBigDecimal, BigInteger => JBigInteger} -import java.time._ +import java.net.{URI, URL} +import java.lang.{Boolean as JBoolean, Byte as JByte, Character as JChar, Double as JDouble, Float as JFloat, Integer as JInt, Long as JLong, Number as JNumber, Short as JShort} +import java.math.{BigDecimal as JBigDecimal, BigInteger as JBigInteger} +import java.time.* +import scala.math.* // === Scala case class SampleBigDecimal(bd1: BigDecimal, bd2: BigDecimal, bd3: BigDecimal, bd4: BigDecimal, bd5: BigDecimal, bd6: BigDecimal) case class SampleBigInt(bi1: BigInt, bi2: BigInt, bi3: BigInt, bi4: BigInt) -case class SampleBinary(b1: Array[Byte], b2: Array[Byte]) case class SampleBoolean(bool1: Boolean, bool2: Boolean) case class SampleByte(b1: Byte, b2: Byte, b3: Byte, b4: Byte) case class SampleChar(c1: Char, c2: Char, c3: Char) case class SampleDouble(d1: Double, d2: Double, d3: Double, d4: Double) - -object Size extends Enumeration { - val Small, Medium, Large = Value -} -case class SampleEnum(e1: Size.Value, e2: Size.Value, e3: Size.Value, e4: Size.Value, e5: Size.Value) - case class SampleFloat(f1: Float, f2: Float, f3: Float, f4: Float) case class SampleInt(i1: Int, i2: Int, i3: Int, i4: Int) case class SampleLong(l1: Long, l2: Long, l3: Long, l4: Long) @@ -46,25 +31,10 @@ case class SampleJDouble(d1: JDouble, d2: JDouble, d3: JDouble, d4: JDouble, d5: case class SampleJFloat(f1: JFloat, f2: JFloat, f3: JFloat, f4: JFloat, f5: JFloat) case class SampleJInt(i1: JInt, i2: JInt, i3: JInt, i4: JInt, i5: JInt) case class SampleJLong(l1: JLong, l2: JLong, l3: JLong, l4: JLong, l5: JLong) -case class SampleJNumber(n1: JNumber, - n2: JNumber, - n3: JNumber, - n4: JNumber, - n5: JNumber, - n6: JNumber, - n7: JNumber, - n8: JNumber, - n9: JNumber, - n10: JNumber, - n11: JNumber, - n12: JNumber, - n13: JNumber, - n14: JNumber, - n15: JNumber, - n16: JNumber, - n17: JNumber) +case class SampleJNumber(n1: JNumber, n2: JNumber, n3: JNumber, n4: JNumber, n5: JNumber, n6: JNumber, n7: JNumber, n8: JNumber, n9: JNumber, n10: JNumber, n11: JNumber, n12: JNumber, n13: JNumber, n14: JNumber, n15: JNumber, n16: JNumber, n17: JNumber) case class SampleJShort(s1: JShort, s2: JShort, s3: JShort, s4: JShort, s5: JShort) case class SampleUUID(u1: UUID, u2: UUID) +case class SampleNet(u1: URL, u2: URL, u3: URI, u4: URI) // === Java Time case class SampleDuration(d1: Duration, d2: Duration, d3: Duration) @@ -72,26 +42,44 @@ case class SampleInstant(i1: Instant, i2: Instant, i3: Instant, i4: Instant, i5: case class SampleLocalDateTime(d1: LocalDateTime, d2: LocalDateTime, d3: LocalDateTime, d4: LocalDateTime) case class SampleLocalDate(d1: LocalDate, d2: LocalDate, d3: LocalDate, d4: LocalDate) case class SampleLocalTime(d1: LocalTime, d2: LocalTime, d3: LocalTime, d4: LocalTime, d5: LocalTime, d6: LocalTime) +case class SampleMonthDay(m1: MonthDay, m2: MonthDay) case class SampleOffsetDateTime(o1: OffsetDateTime, o2: OffsetDateTime, o3: OffsetDateTime, o4: OffsetDateTime) case class SampleOffsetTime(o1: OffsetTime, o2: OffsetTime, o3: OffsetTime, o4: OffsetTime) case class SamplePeriod(p1: Period, p2: Period, p3: Period) +case class SampleYear(y1: Year, y2: Year, y3: Year, y4: Year) +case class SampleYearMonth(y1: YearMonth, y2: YearMonth) case class SampleZonedDateTime(o1: ZonedDateTime, o2: ZonedDateTime) +case class SampleZoneId(z1: ZoneId, z2: ZoneId) +case class SampleZoneOffset(z1: ZoneOffset, z2: ZoneOffset) +// TODO: Missing Year, MonthYear, ZoneId, ZoneOffset, others? // === Any primitives case class AnyShell(a: Any) +object Size extends Enumeration { + val Small, Medium, Large = Value +} + +enum Color { + case Red, Blue, Green +} + // === Value Classes -case class VCBigDecimal(vc: BigDecimal) extends AnyVal -case class VCBigInt(vc: BigInt) extends AnyVal -case class VCBoolean(vc: Boolean) extends AnyVal -case class VCByte(vc: Byte) extends AnyVal -case class VCChar(vc: Char) extends AnyVal -case class VCDouble(vc: Double) extends AnyVal +case class VCBigDecimal(vc: BigDecimal) extends AnyVal +case class VCBigInt(vc: BigInt) extends AnyVal +case class VCBoolean(vc: Boolean) extends AnyVal +case class VCByte(vc: Byte) extends AnyVal +case class VCChar(vc: Char) extends AnyVal +case class VCDouble(vc: Double) extends AnyVal +case class VCEnum(vc: Color) extends AnyVal case class VCEnumeration(vc: Size.Value) extends AnyVal -case class VCFloat(vc: Float) extends AnyVal -case class VCInt(vc: Int) extends AnyVal -case class VCLong(vc: Long) extends AnyVal -case class VCShort(vc: Short) extends AnyVal -case class VCString(vc: String) extends AnyVal -case class VCUUID(vc: UUID) extends AnyVal -case class VCNumber(vc: Number) extends AnyVal \ No newline at end of file +case class VCFloat(vc: Float) extends AnyVal +case class VCInt(vc: Int) extends AnyVal +case class VCLong(vc: Long) extends AnyVal +case class VCShort(vc: Short) extends AnyVal +case class VCString(vc: String) extends AnyVal +case class VCUUID(vc: UUID) extends AnyVal +case class VCNumber(vc: Number) extends AnyVal + +// === Permissives test +case class Holder[T](value: T) diff --git a/src/test/scala/co.blocke.scalajack/json/primitives/ScalaPrimSpec.scala b/src/test/scala/co.blocke.scalajack/json/primitives/ScalaPrimSpec.scala new file mode 100644 index 00000000..66ae8799 --- /dev/null +++ b/src/test/scala/co.blocke.scalajack/json/primitives/ScalaPrimSpec.scala @@ -0,0 +1,336 @@ +package co.blocke.scalajack +package json +package primitives + +import ScalaJack.* +import co.blocke.scala_reflection.* +import scala.math.BigDecimal +import java.util.UUID +import TestUtil.* +import org.scalatest.funspec.AnyFunSpec +import org.scalatest.matchers.should.Matchers.* +import org.scalatest.* + +class ScalaPrimSpec() extends AnyFunSpec with JsonMatchers: + + describe(colorString("---------------------------\n: Scala Primitive Tests :\n---------------------------", Console.YELLOW)) { + describe(colorString("+++ Positive Tests +++")) { + it("BigDecimal must work") { + val inst = SampleBigDecimal( + BigDecimal(123L), + BigDecimal(1.23), + BigDecimal(0), + BigDecimal("123.456"), + BigDecimal( + "0.1499999999999999944488848768742172978818416595458984375" + ), + null + ) + + val sj = sjCodecOf[SampleBigDecimal] + val js = sj.toJson(inst) + js should matchJson("""{"bd1":123,"bd2":1.23,"bd3":0,"bd4":123.456,"bd5":0.1499999999999999944488848768742172978818416595458984375,"bd6":null}""") + sj.fromJson(js) shouldEqual inst + } + + it("BigInt must work") { + val inst = SampleBigInt( + BigInt("-90182736451928374653345"), + BigInt("90182736451928374653345"), + BigInt(0), + null + ) + val sj = sjCodecOf[SampleBigInt] + val js = sj.toJson(inst) + js should matchJson("""{"bi1":-90182736451928374653345,"bi2":90182736451928374653345,"bi3":0,"bi4":null}""") + sj.fromJson(js) shouldEqual inst + } + + it("Boolean must work (not nullable)") { + val inst = SampleBoolean(bool1 = true, bool2 = false) + val sj = sjCodecOf[SampleBoolean] + val js = sj.toJson(inst) + js should matchJson("""{"bool1":true,"bool2":false}""") + sj.fromJson(js) shouldEqual inst + } + + it("Byte must work (not nullable)") { + val inst = SampleByte(Byte.MaxValue, Byte.MinValue, 0, 64) + val sj = sjCodecOf[SampleByte] + val js = sj.toJson(inst) + js should matchJson("""{"b1":127,"b2":-128,"b3":0,"b4":64}""") + sj.fromJson(js) shouldEqual inst + } + + it("Char must work (not nullable)") { + val inst = SampleChar(Char.MaxValue, 'Z', '\u20A0') + val sj = sjCodecOf[SampleChar] + val js = sj.toJson(inst) + js should matchJson("""{"c1":"\""" + """uffff","c2":"Z","c3":"\""" + """u20a0"}""") + sj.fromJson(js) shouldEqual inst + } + + it("Double must work (not nullable)") { + val inst = + SampleDouble(Double.MaxValue, Double.MinValue, 0.0, -123.4567) + val sj = sjCodecOf[SampleDouble] + val js = sj.toJson(inst) + js should matchJson("""{"d1":1.7976931348623157E308,"d2":-1.7976931348623157E308,"d3":0.0,"d4":-123.4567}""") + sj.fromJson(js) shouldEqual inst + } + + it("Float must work (not nullable)") { + val inst = SampleFloat(Float.MaxValue, Float.MinValue, 0.0f, -123.4567f) + val sj = sjCodecOf[SampleFloat] + val js = sj.toJson(inst) + js should matchJson("""{"f1":3.4028235E38,"f2":-3.4028235E38,"f3":0.0,"f4":-123.4567}""") + sj.fromJson(js) shouldEqual inst + } + + it("Int must work (not nullable)") { + val inst = SampleInt(Int.MaxValue, Int.MinValue, 0, 123) + val sj = sjCodecOf[SampleInt] + val js = sj.toJson(inst) + js should matchJson("""{"i1":2147483647,"i2":-2147483648,"i3":0,"i4":123}""") + sj.fromJson(js) shouldEqual inst + } + + it("Long must work (not nullable)") { + val inst = SampleLong(Long.MaxValue, Long.MinValue, 0L, 123L) + val sj = sjCodecOf[SampleLong] + val js = sj.toJson(inst) + js should matchJson("""{"l1":9223372036854775807,"l2":-9223372036854775808,"l3":0,"l4":123}""") + sj.fromJson(js) shouldEqual inst + } + + it("Short must work (not nullable)") { + val inst = SampleShort(Short.MaxValue, Short.MinValue, 0, 123) + val sj = sjCodecOf[SampleShort] + val js = sj.toJson(inst) + js should matchJson("""{"s1":32767,"s2":-32768,"s3":0,"s4":123}""") + sj.fromJson(js) shouldEqual inst + } + + it("String must work") { + val inst = SampleString("something\b\n\f\r\t☆", "", null) + val sj = sjCodecOf[SampleString] + val js = sj.toJson(inst) + // The weird '+' here is to break up the unicode so it won't be interpreted and wreck the test. + js should matchJson("""{"s1":"something\b\n\f\r\t\""" + """u2606","s2":"","s3":null}""") + sj.fromJson(js) shouldEqual inst + } + + it("Any type for all primitives must work") { + val sj = sjCodecOf[AnyShell] + val prims: List[(Any, String, Option[Any => String])] = List( + (null, """{"a":null}""", None), + (scala.math.BigDecimal(5), """{"a":5}""", None), + (scala.math.BigInt(5), """{"a":5}""", None), + (true, """{"a":true}""", None), + (5.toByte, """{"a":5}""", None), + ('x', """{"a":"x"}""", Some((c: Any) => c.toString)), + (5.0, """{"a":5.0}""", None), + (5.0.toFloat, """{"a":5.0}""", None), + (5, """{"a":5}""", None), + (5L, """{"a":5}""", None), + (5.toShort, """{"a":5}""", None), + ("foo", """{"a":"foo"}""", None), + (java.lang.Boolean.valueOf(true), """{"a":true}""", None), + (java.lang.Byte.valueOf(5.toByte), """{"a":5}""", None), + (java.lang.Character.valueOf('x'), """{"a":"x"}""", Some((c: Any) => c.toString)), + (java.lang.Double.valueOf(5.0), """{"a":5.0}""", None), + (java.lang.Float.valueOf(5.0.toFloat), """{"a":5.0}""", None), + (java.lang.Integer.valueOf(5), """{"a":5}""", None), + (java.lang.Long.valueOf(5), """{"a":5}""", None), + (java.lang.Short.valueOf(5.toShort), """{"a":5}""", None), + (java.lang.Integer.valueOf(5).asInstanceOf[java.lang.Number], """{"a":5}""", None), + (java.time.Duration.ofHours(5), """{"a":"PT5H"}""", Some((c: Any) => c.toString)), + (java.time.Instant.ofEpochSecond(1234567), """{"a":"1970-01-15T06:56:07Z"}""", Some((c: Any) => c.toString)), + (java.time.LocalDate.of(2024, 3, 15), """{"a":"2024-03-15"}""", Some((c: Any) => c.toString)), + (java.time.LocalDateTime.of(2024, 3, 15, 4, 15, 3), """{"a":"2024-03-15T04:15:03"}""", Some((c: Any) => c.toString)), + (java.time.LocalTime.of(4, 15, 3), """{"a":"04:15:03"}""", Some((c: Any) => c.toString)), + (java.time.MonthDay.of(12, 25), """{"a":"--12-25"}""", Some((c: Any) => c.toString)), + (java.time.OffsetDateTime.of(2024, 3, 15, 9, 15, 1, 0, java.time.ZoneOffset.ofHours(5)), """{"a":"2024-03-15T09:15:01+05:00"}""", Some((c: Any) => c.toString)), + (java.time.OffsetTime.of(9, 15, 1, 0, java.time.ZoneOffset.ofHours(5)), """{"a":"09:15:01+05:00"}""", Some((c: Any) => c.toString)), + (java.time.Period.ofDays(5), """{"a":"P5D"}""", Some((c: Any) => c.toString)), + (java.time.Year.of(2024), """{"a":"2024"}""", Some((c: Any) => c.toString)), + (java.time.YearMonth.of(2024, 3), """{"a":"2024-03"}""", Some((c: Any) => c.toString)), + (java.time.ZoneOffset.ofHours(5), """{"a":"+05:00"}""", Some((c: Any) => c.toString)), + (java.time.ZonedDateTime.parse("2007-12-03T10:15:30+01:00"), """{"a":"2007-12-03T10:15:30+01:00"}""", Some((c: Any) => c.toString)), + (java.time.ZoneId.of("GMT+2"), """{"a":"GMT+02:00"}""", Some((c: Any) => c.toString)) + ) + prims.map { case (v, j, fn) => + val inst = AnyShell(v) + val js = sj.toJson(inst) + js should matchJson(j) + fn match { + case Some(f) => sj.fromJson(js) shouldEqual (AnyShell(f(v))) + case None => sj.fromJson(js) shouldEqual inst + } + } + } + } + + // -------------------------------------------------------- + + describe(colorString("--- Negative Tests ---")) { + it("BigDecimal must break") { + val js = + """{"bd1":123,"bd2":1.23,"bd3":0,"bd4":123.456,"bd5":"0.1499999999999999944488848768742172978818416595458984375","bd6":null}""" + val msg: String = + """Expected a numerical value or null here at position [50] + |{"bd1":123,"bd2":1.23,"bd3":0,"bd4":123.456,"bd5":"0.149999999999999994448884... + |--------------------------------------------------^""".stripMargin + val ex = intercept[JsonParseError](sjCodecOf[SampleBigDecimal].fromJson(js)) + ex.show shouldEqual msg + } + + it("BigInt must break") { + val js = + """{"bi1":"-90182736451928374653345","bi2":90182736451928374653345,"bi3":0,"bi4":null}""" + val msg = + """Expected a numerical value or null here at position [7] + |{"bi1":"-90182736451928374653345","bi2":90182736451928374653345,"bi3":0,"bi4"... + |-------^""".stripMargin + val ex = intercept[JsonParseError](sjCodecOf[SampleBigInt].fromJson(js)) + ex.show shouldEqual msg + } + + it("Boolean must break") { + val js = """{"bool1":true,"bool2":"false"}""" + val sj = sjCodecOf[SampleBoolean] + val msg = """Expected 'true' or 'false' at position [22] + |{"bool1":true,"bool2":"false"} + |----------------------^""".stripMargin + val ex = intercept[JsonParseError](sj.fromJson(js)) + ex.show shouldEqual msg + val js2 = """{"bool1":true,"bool2":123}""" + val msg2 = """Expected 'true' or 'false' at position [22] + |{"bool1":true,"bool2":123} + |----------------------^""".stripMargin + val ex2 = intercept[JsonParseError](sj.fromJson(js2)) + ex2.show shouldEqual msg2 + val js3 = """{"bool1":true,"bool2":null}""" + val msg3 = """Expected 'true' or 'false' at position [22] + |{"bool1":true,"bool2":null} + |----------------------^""".stripMargin + val ex3 = intercept[JsonParseError](sj.fromJson(js3)) + ex3.show shouldEqual msg3 + } + + it("Byte must break") { + val js = """{"b1":true,"b2":-128,"b3":0,"b4":64}""" + val sj = sjCodecOf[SampleByte] + val msg = """Non-numeric character found when integer value expected at position [6] + |{"b1":true,"b2":-128,"b3":0,"b4":64} + |------^""".stripMargin + val ex = intercept[JsonParseError](sj.fromJson(js)) + ex.show shouldEqual msg + val js2 = """{"b1":12,"b2":-128,"b3":0,"b4":null}""" + val msg2 = """Non-numeric character found when integer value expected at position [31] + |{"b1":12,"b2":-128,"b3":0,"b4":null} + |-------------------------------^""".stripMargin + val ex2 = intercept[JsonParseError](sj.fromJson(js2)) + ex2.show shouldEqual msg2 + } + + it("Char must break") { + val js = """{"c1":null,"c2":"Y","c3":"Z"}""" + val sj = sjCodecOf[SampleChar] + val msg = """Char value cannot be null at position [6] + |{"c1":null,"c2":"Y","c3":"Z"} + |------^""".stripMargin + val ex = intercept[JsonParseError](sj.fromJson(js)) + ex.show shouldEqual msg + val js2 = """{"c1":"","c2":"Y","c3":"Z"}""" + val msg2 = """Char value expected but empty string found in json at position [6] + |{"c1":"","c2":"Y","c3":"Z"} + |------^""".stripMargin + val ex2 = intercept[JsonParseError](sj.fromJson(js2)) + ex2.show shouldEqual msg2 + } + + it("Double must break") { + val js = + """{"d1":1.79769313486E23157E308,"d2":-1.7976931348623157E308,"d3":0.0,"d4":-123.4567}""" + val msg = + """Expected ',' or '}' but found 'E' at position [25] + |{"d1":1.79769313486E23157E308,"d2":-1.7976931348623157E308,"d3":0.0,"d4":-123... + |-------------------------^""".stripMargin + val ex = intercept[JsonParseError](sjCodecOf[SampleDouble].fromJson(js)) + ex.show shouldEqual msg + } + + it("Float must break") { + val js = + """{"f1":3.4028235E38,"f2":"-3.4028235E38","f3":0.0,"f4":-123.4567}""" + val msg = + """Malformed Float/Double/BigDecimal at position [24] + |{"f1":3.4028235E38,"f2":"-3.4028235E38","f3":0.0,"f4":-123.4567} + |------------------------^""".stripMargin + val ex = intercept[JsonParseError](sjCodecOf[SampleFloat].fromJson(js)) + ex.show shouldEqual msg + } + + it("Int must break") { + val sj = sjCodecOf[SampleInt] + val js = """{"i1":2147483647,"i2":-2147483648,"i3":"0","i4":123}""" + val msg = """Non-numeric character found when integer value expected at position [39] + |{"i1":2147483647,"i2":-2147483648,"i3":"0","i4":123} + |---------------------------------------^""".stripMargin + val ex = intercept[JsonParseError](sj.fromJson(js)) + ex.show shouldEqual msg + val js2 = """{"i1":2147483647,"i2":-2147483648,"i3":2.3,"i4":123}""" + val msg2 = """Decimal digit 'e' or '.' found when integer value expected at position [40] + |{"i1":2147483647,"i2":-2147483648,"i3":2.3,"i4":123} + |----------------------------------------^""".stripMargin + val ex2 = intercept[JsonParseError](sj.fromJson(js2)) + ex2.show shouldEqual msg2 + } + + it("Long must break") { + val sj = sjCodecOf[SampleLong] + val js = + """{"l1":9223372036854775807,"l2":-9223372036854775808,"l3":true,"l4":123}""" + val msg = + """Unexpected character in Int/Long value: t at position [57] + |...23372036854775807,"l2":-9223372036854775808,"l3":true,"l4":123} + |----------------------------------------------------^""".stripMargin + val ex = intercept[JsonParseError](sj.fromJson(js)) + ex.show shouldEqual msg + val js2 = + """{"l1":9223372036854775807,"l2":-9223372036854775808,"l3":0.3,"l4":123}""" + val msg2 = + """Expected ',' or '}' but found '.' at position [58] + |...3372036854775807,"l2":-9223372036854775808,"l3":0.3,"l4":123} + |----------------------------------------------------^""".stripMargin + val ex2 = intercept[JsonParseError](sj.fromJson(js2)) + ex2.show shouldEqual msg2 + } + + it("Short must break") { + val sj = sjCodecOf[SampleShort] + val js = """{"s1":32767,"s2":true,"s3":0,"s4":123}""" + val msg = """Non-numeric character found when integer value expected at position [17] + |{"s1":32767,"s2":true,"s3":0,"s4":123} + |-----------------^""".stripMargin + val ex = intercept[JsonParseError](sj.fromJson(js)) + ex.show shouldEqual msg + val js2 = """{"s1":32767,"s2":3.4,"s3":0,"s4":123}""" + val msg2 = """Decimal digit 'e' or '.' found when integer value expected at position [18] + |{"s1":32767,"s2":3.4,"s3":0,"s4":123} + |------------------^""".stripMargin + val ex2 = intercept[JsonParseError](sj.fromJson(js2)) + ex2.show shouldEqual msg2 + } + + it("String must break") { + val js = """{"s1":"something","s2":-19,"s3":null}""" + val msg = """Expected a String value but got '-' at position [23] + |{"s1":"something","s2":-19,"s3":null} + |-----------------------^""".stripMargin + val ex = intercept[JsonParseError](sjCodecOf[SampleString].fromJson(js)) + ex.show shouldEqual msg + } + } + } diff --git a/src/test/scala/co.blocke.scalajack/json/primitives/SimpleSpec.scala b/src/test/scala/co.blocke.scalajack/json/primitives/SimpleSpec.scala new file mode 100644 index 00000000..b2b7452f --- /dev/null +++ b/src/test/scala/co.blocke.scalajack/json/primitives/SimpleSpec.scala @@ -0,0 +1,377 @@ +package co.blocke.scalajack +package json +package primitives + +import ScalaJack.* +import co.blocke.scala_reflection.* +import org.scalatest.funspec.AnyFunSpec +import org.scalatest.matchers.should.Matchers.* +import org.scalatest.* +import TestUtil.* +import java.time.* +import java.util.UUID +import java.net.{URI, URL} + +class SimpleSpec() extends AnyFunSpec with JsonMatchers: + + describe(colorString("-----------------------\n: Simple Type Tests :\n-----------------------", Console.YELLOW)) { + describe(colorString("+++ Positive Tests +++")) { + + it("Duration must work") { + val inst = SampleDuration(Duration.ZERO, Duration.parse("P2DT3H4M"), null) + val js = sjCodecOf[SampleDuration].toJson(inst) + js should matchJson("""{"d1":"PT0S","d2":"PT51H4M","d3":null}""") + sjCodecOf[SampleDuration].fromJson(js) shouldEqual (inst) + } + + it("Instant must work") { + val inst = SampleInstant( + Instant.EPOCH, + Instant.MAX, + Instant.MIN, + Instant.parse("2007-12-03T10:15:30.00Z"), + null + ) + val js = sjCodecOf[SampleInstant].toJson(inst) + js should matchJson("""{"i1":"1970-01-01T00:00:00Z","i2":"+1000000000-12-31T23:59:59.999999999Z","i3":"-1000000000-01-01T00:00:00Z","i4":"2007-12-03T10:15:30Z","i5":null}""") + sjCodecOf[SampleInstant].fromJson(js) shouldEqual (inst) + } + + it("LocalDateTime must work") { + val inst = SampleLocalDateTime( + LocalDateTime.MAX, + LocalDateTime.MIN, + LocalDateTime.parse("2007-12-03T10:15:30"), + null + ) + val js = sjCodecOf[SampleLocalDateTime].toJson(inst) + js should matchJson("""{"d1":"+999999999-12-31T23:59:59.999999999","d2":"-999999999-01-01T00:00:00","d3":"2007-12-03T10:15:30","d4":null}""") + sjCodecOf[SampleLocalDateTime].fromJson(js) shouldEqual (inst) + } + + it("LocalDate must work") { + val inst = SampleLocalDate( + LocalDate.MAX, + LocalDate.MIN, + LocalDate.parse("2007-12-03"), + null + ) + val js = sjCodecOf[SampleLocalDate].toJson(inst) + js should matchJson("""{"d1":"+999999999-12-31","d2":"-999999999-01-01","d3":"2007-12-03","d4":null}""") + sjCodecOf[SampleLocalDate].fromJson(js) shouldEqual (inst) + } + + it("LocalTime must work") { + val inst = SampleLocalTime( + LocalTime.MAX, + LocalTime.MIN, + LocalTime.MIDNIGHT, + LocalTime.NOON, + LocalTime.parse("10:15:30"), + null + ) + val js = sjCodecOf[SampleLocalTime].toJson(inst) + js should matchJson("""{"d1":"23:59:59.999999999","d2":"00:00:00","d3":"00:00:00","d4":"12:00:00","d5":"10:15:30","d6":null}""") + sjCodecOf[SampleLocalTime].fromJson(js) shouldEqual (inst) + } + + it("MonthDay must work") { + val inst = SampleMonthDay( + MonthDay.of(7, 1), + null + ) + val js = sjCodecOf[SampleMonthDay].toJson(inst) + js should matchJson("""{"m1":"--07-01","m2":null}""") + sjCodecOf[SampleMonthDay].fromJson(js) shouldEqual (inst) + } + + it("OffsetDateTime must work") { + val inst = SampleOffsetDateTime( + OffsetDateTime.MAX, + OffsetDateTime.MIN, + OffsetDateTime.parse("2007-12-03T10:15:30+01:00"), + null + ) + val js = sjCodecOf[SampleOffsetDateTime].toJson(inst) + js should matchJson("""{"o1":"+999999999-12-31T23:59:59.999999999-18:00","o2":"-999999999-01-01T00:00:00+18:00","o3":"2007-12-03T10:15:30+01:00","o4":null}""") + sjCodecOf[SampleOffsetDateTime].fromJson(js) shouldEqual (inst) + } + + it("OffsetTime must work") { + val inst = SampleOffsetTime( + OffsetTime.MAX, + OffsetTime.MIN, + OffsetTime.parse("10:15:30+01:00"), + null + ) + val js = sjCodecOf[SampleOffsetTime].toJson(inst) + js should matchJson("""{"o1":"23:59:59.999999999-18:00","o2":"00:00:00+18:00","o3":"10:15:30+01:00","o4":null}""") + sjCodecOf[SampleOffsetTime].fromJson(js) shouldEqual (inst) + } + + it("Period must work") { + val inst = SamplePeriod(Period.ZERO, Period.parse("P1Y2M3D"), null) + val js = sjCodecOf[SamplePeriod].toJson(inst) + js should matchJson("""{"p1":"P0D","p2":"P1Y2M3D","p3":null}""") + sjCodecOf[SamplePeriod].fromJson(js) shouldEqual (inst) + } + + it("Year must work") { + val inst = SampleYear(Year.of(Year.MAX_VALUE), Year.of(Year.MIN_VALUE), Year.parse("2020"), null) + val js = sjCodecOf[SampleYear].toJson(inst) + js should matchJson("""{"y1":"999999999","y2":"-999999999","y3":"2020","y4":null}""") + sjCodecOf[SampleYear].fromJson(js) shouldEqual (inst) + } + + it("YearMonth must work") { + val inst = SampleYearMonth(YearMonth.of(2020, 7), null) + val js = sjCodecOf[SampleYearMonth].toJson(inst) + js should matchJson("""{"y1":"2020-07","y2":null}""") + sjCodecOf[SampleYearMonth].fromJson(js) shouldEqual (inst) + } + + it("ZonedDateTime must work") { + val inst = SampleZonedDateTime( + ZonedDateTime.parse("2007-12-03T10:15:30+01:00[Europe/Paris]"), + null + ) + val js = sjCodecOf[SampleZonedDateTime].toJson(inst) + js should matchJson("""{"o1":"2007-12-03T10:15:30+01:00[Europe/Paris]","o2":null}""") + sjCodecOf[SampleZonedDateTime].fromJson(js) shouldEqual (inst) + } + + it("ZonedId must work") { + val inst = SampleZoneId( + ZoneId.of("America/Puerto_Rico"), + null + ) + val js = sjCodecOf[SampleZoneId].toJson(inst) + js should matchJson("""{"z1":"America/Puerto_Rico","z2":null}""") + sjCodecOf[SampleZoneId].fromJson(js) shouldEqual (inst) + } + + it("ZoneOffset must work") { + val ldt = LocalDateTime.parse("2007-12-03T10:15:30") + val zone = ZoneId.of("Europe/Berlin") + val zoneOffSet = zone.getRules().getOffset(ldt) + val inst = SampleZoneOffset( + null, + zoneOffSet + ) + val js = sjCodecOf[SampleZoneOffset].toJson(inst) + js should matchJson("""{"z1":null,"z2":"+01:00"}""") + sjCodecOf[SampleZoneOffset].fromJson(js) shouldEqual (inst) + } + + it("Net types URL and URI must work") { + val inst = SampleNet( + null, + new URI("https://www.foom.com").toURL(), + null, + new URI("https://www.foom.com") + ) + val js = sjCodecOf[SampleNet].toJson(inst) + js should matchJson("""{"u1":null,"u2":"https://www.foom.com","u3":null,"u4":"https://www.foom.com"}""") + sjCodecOf[SampleNet].fromJson(js) shouldEqual (inst) + } + + it("UUID must work") { + val inst = SampleUUID( + null, + UUID.fromString("580afe0d-81c0-458f-9e09-4486c7af0fe9") + ) + val js = sjCodecOf[SampleUUID].toJson(inst) + js should matchJson("""{"u1":null,"u2":"580afe0d-81c0-458f-9e09-4486c7af0fe9"}""") + sjCodecOf[SampleUUID].fromJson(js) shouldEqual (inst) + } + } + + describe(colorString("+++ Negative Tests +++")) { + it("Duration must break") { + val js = """{"d1":"PT0S","d2":21,"d3":null}""" + val msg = """Expected a String value but got '2' at position [18] + |{"d1":"PT0S","d2":21,"d3":null} + |------------------^""".stripMargin + val ex = intercept[co.blocke.scalajack.json.JsonParseError](sjCodecOf[SampleDuration].fromJson(js)) + ex.show shouldEqual msg + + val js2 = """{"d1":"PT0S","d2":"bogus","d3":null}""" + val msg2 = """Failed to parse Duration from input 'bogus' + |{"d1":"PT0S","d2":"bogus","d3":null} + |------------------------^""".stripMargin + the[java.time.format.DateTimeParseException] thrownBy sjCodecOf[SampleDuration].fromJson(js2) should have message """Text cannot be parsed to a Duration""" + } + + it("Instant must break") { + val js = + """{"i1":"1970-01-01T00:00:00Z","i2":false,"i3":"-1000000000-01-01T00:00:00Z","i4":"2007-12-03T10:15:30Z","i5":null}""" + val msg = + """Expected a String value but got 'f' at position [34] + |{"i1":"1970-01-01T00:00:00Z","i2":false,"i3":"-1000000000-01-01T00:00:00Z","i... + |----------------------------------^""".stripMargin + val ex = intercept[co.blocke.scalajack.json.JsonParseError](sjCodecOf[SampleInstant].fromJson(js)) + ex.show shouldEqual msg + + val js2 = + """{"i1":"1970-01-01T00:00:00Z","i2":"bogus","i3":"-1000000000-01-01T00:00:00Z","i4":"2007-12-03T10:15:30Z","i5":null}""" + the[java.time.format.DateTimeParseException] thrownBy sjCodecOf[SampleInstant].fromJson(js2) should have message """Text 'bogus' could not be parsed at index 0""" + } + + it("LocalDateTime must break") { + val js = + """{"d1":-1,"d2":"-999999999-01-01T00:00:00","d3":"2007-12-03T10:15:30","d4":null}""" + val msg = + """Expected a String value but got '-' at position [6] + |{"d1":-1,"d2":"-999999999-01-01T00:00:00","d3":"2007-12-03T10:15:30","d4":null} + |------^""".stripMargin + val ex = intercept[co.blocke.scalajack.json.JsonParseError](sjCodecOf[SampleLocalDateTime].fromJson(js)) + ex.show shouldEqual msg + val js2 = + """{"d1":"bogus","d2":"-999999999-01-01T00:00:00","d3":"2007-12-03T10:15:30","d1":null}""" + the[java.time.format.DateTimeParseException] thrownBy sjCodecOf[SampleLocalDateTime].fromJson(js2) should have message """Text 'bogus' could not be parsed at index 0""" + } + + it("LocalDate must break") { + val js = + """{"d1":-1,"d2":"-999999999-01-01","d3":"2007-12-03","d4":null}""" + val msg = """Expected a String value but got '-' at position [6] + |{"d1":-1,"d2":"-999999999-01-01","d3":"2007-12-03","d4":null} + |------^""".stripMargin + val ex = intercept[co.blocke.scalajack.json.JsonParseError](sjCodecOf[SampleLocalDate].fromJson(js)) + ex.show shouldEqual msg + val js2 = + """{"d1":"bogus","d2":"-999999999-01-01","d3":"2007-12-03","d4":null}""" + the[java.time.format.DateTimeParseException] thrownBy sjCodecOf[SampleLocalDate].fromJson(js2) should have message """Text 'bogus' could not be parsed at index 0""" + } + + it("LocalTime must break") { + val js = + """{"d1":"23:59:59.999999999","d2":"00:00:00","d3":"00:00:00","d4":"12:00:00","d5":false,"d6":null}""" + val msg = + """Expected a String value but got 'f' at position [80] + |...:"00:00:00","d3":"00:00:00","d4":"12:00:00","d5":false,"d6":null} + |----------------------------------------------------^""".stripMargin + val ex = intercept[co.blocke.scalajack.json.JsonParseError](sjCodecOf[SampleLocalTime].fromJson(js)) + ex.show shouldEqual msg + val js2 = + """{"d1":"23:59:59.999999999","d2":"00:00:00","d3":"00:00:00","d4":"12:00:00","d5":"Bogus","d6":null}""" + the[java.time.format.DateTimeParseException] thrownBy sjCodecOf[SampleLocalTime].fromJson(js2) should have message """Text 'Bogus' could not be parsed at index 0""" + } + + it("MonthDay must break") { + val js = """{"m1":25,"m2":null}""" + val msg = """Expected a String value but got '2' at position [6] + |{"m1":25,"m2":null} + |------^""".stripMargin + val ex = intercept[co.blocke.scalajack.json.JsonParseError](sjCodecOf[SampleMonthDay].fromJson(js)) + ex.show shouldEqual msg + val js2 = """{"m1":"R-07-01","m2":null}""" + the[java.time.format.DateTimeParseException] thrownBy sjCodecOf[SampleMonthDay].fromJson(js2) should have message """Text 'R-07-01' could not be parsed at index 0""" + } + + it("OffsetDateTime must break") { + val js = + """{"o1":"+999999999-12-31T23:59:59.999999999-18:00","o2":2,"o3":"2007-12-03T10:15:30+01:00","o4":null}""" + val msg = + """Expected a String value but got '2' at position [55] + |..."+999999999-12-31T23:59:59.999999999-18:00","o2":2,"o3":"2007-12-03T10:15:30... + |----------------------------------------------------^""".stripMargin + val ex = intercept[co.blocke.scalajack.json.JsonParseError](sjCodecOf[SampleOffsetDateTime].fromJson(js)) + ex.show shouldEqual msg + val js2 = + """{"o1":"+999999999-12-31T23:59:59.999999999-18:00","o2":"-999999999-01T00:00:00+18:00","o3":"2007-12-03T10:15:30+01:00","o4":null}""" + the[java.time.format.DateTimeParseException] thrownBy sjCodecOf[SampleOffsetDateTime].fromJson(js2) should have message """Text '-999999999-01T00:00:00+18:00' could not be parsed at index 13""" + } + + it("OffsetTime must break") { + val js = + """{"o1":"23:59:59.999999999-18:00","o2":false,"o3":"10:15:30+01:00","o4":null}""" + val msg = + """Expected a String value but got 'f' at position [38] + |{"o1":"23:59:59.999999999-18:00","o2":false,"o3":"10:15:30+01:00","o4":null} + |--------------------------------------^""".stripMargin + val ex = intercept[co.blocke.scalajack.json.JsonParseError](sjCodecOf[SampleOffsetTime].fromJson(js)) + ex.show shouldEqual msg + val js2 = + """{"o1":"23:59:59.999999999-18:00","o2":"00:00:00:00+18:00","o3":"10:15:30+01:00","o4":null}""" + the[java.time.format.DateTimeParseException] thrownBy sjCodecOf[SampleOffsetTime].fromJson(js2) should have message """Text '00:00:00:00+18:00' could not be parsed at index 8""" + } + + it("Period must break") { + val js = """{"p1":"P0D","p2":5,"p3":null}""" + val msg = """Expected a String value but got '5' at position [17] + |{"p1":"P0D","p2":5,"p3":null} + |-----------------^""".stripMargin + val ex = intercept[co.blocke.scalajack.json.JsonParseError](sjCodecOf[SamplePeriod].fromJson(js)) + ex.show shouldEqual msg + val js2 = """{"p1":"P0D","p2":"bogus","p3":null}""" + the[java.time.format.DateTimeParseException] thrownBy sjCodecOf[SamplePeriod].fromJson(js2) should have message """Text cannot be parsed to a Period""" + } + + it("Year must break") { + val js = """{"y1":"999999999","y2":5,"y3":"2020","y4":null}""" + val msg = """Expected a String value but got '5' at position [23] + |{"y1":"999999999","y2":5,"y3":"2020","y4":null} + |-----------------------^""".stripMargin + val ex = intercept[co.blocke.scalajack.json.JsonParseError](sjCodecOf[SampleYear].fromJson(js)) + ex.show shouldEqual msg + val js2 = """{"y1":"999999999","y2":"bogus","y3":"2020","y4":null}""" + the[java.time.format.DateTimeParseException] thrownBy sjCodecOf[SampleYear].fromJson(js2) should have message """Text 'bogus' could not be parsed at index 0""" + } + + it("YearMonth must break") { + val js = """{"y1":true,"y2":null}""" + val msg = """Expected a String value but got 't' at position [6] + |{"y1":true,"y2":null} + |------^""".stripMargin + val ex = intercept[co.blocke.scalajack.json.JsonParseError](sjCodecOf[SampleYearMonth].fromJson(js)) + ex.show shouldEqual msg + val js2 = """{"y1":"bogus","y2":null}""" + the[java.time.format.DateTimeParseException] thrownBy sjCodecOf[SampleYearMonth].fromJson(js2) should have message """Text 'bogus' could not be parsed at index 0""" + } + + it("ZonedId must break") { + val js = """{"z1":-3,"z2":null}""" + val msg = """Expected a String value but got '-' at position [6] + |{"z1":-3,"z2":null} + |------^""".stripMargin + val ex = intercept[co.blocke.scalajack.json.JsonParseError](sjCodecOf[SampleZoneId].fromJson(js)) + ex.show shouldEqual msg + val js2 = """{"z1":"Mars/Puerto_Rico","z2":null}""" + the[java.time.zone.ZoneRulesException] thrownBy sjCodecOf[SampleZoneId].fromJson(js2) should have message """Unknown time-zone ID: Mars/Puerto_Rico""" + } + + it("ZonedDateTime must break") { + val js = """{"o1":true,"o2":null}""" + val msg = """Expected a String value but got 't' at position [6] + |{"o1":true,"o2":null} + |------^""".stripMargin + val ex = intercept[co.blocke.scalajack.json.JsonParseError](sjCodecOf[SampleZonedDateTime].fromJson(js)) + ex.show shouldEqual msg + val js2 = """{"o1":"2007-12-03T10:15:30+01:00 Earth","o2":null}""" + the[java.time.format.DateTimeParseException] thrownBy sjCodecOf[SampleZonedDateTime].fromJson(js2) should have message """Text '2007-12-03T10:15:30+01:00 Earth' could not be parsed, unparsed text found at index 25""" + } + + it("Net types URL and URI must break") { + val js = """{"u1":null,"u2":13,"u3":null,"u4":"https://www.foom.com"}""" + val msg = """Expected a String value but got '1' at position [16] + |{"u1":null,"u2":13,"u3":null,"u4":"https://www.foom.com"} + |----------------^""".stripMargin + val ex = intercept[co.blocke.scalajack.json.JsonParseError](sjCodecOf[SampleNet].fromJson(js)) + ex.show shouldEqual msg + val js2 = """{"u1":null,"u2":"httpwww.foom.com","u3":null,"u4":"https://www.foom.com"}""" + the[java.lang.IllegalArgumentException] thrownBy sjCodecOf[SampleNet].fromJson(js2) should have message """URI is not absolute""" + } + + it("UUID must break") { + val js = """{"u1":null,"u2":[1,2,3]}""" + val msg = """Expected a String value but got '[' at position [16] + |{"u1":null,"u2":[1,2,3]} + |----------------^""".stripMargin + val ex = intercept[co.blocke.scalajack.json.JsonParseError](sjCodecOf[SampleUUID].fromJson(js)) + ex.show shouldEqual msg + val js2 = """{"u1":null,"u2":"bogus"}""" + the[java.lang.IllegalArgumentException] thrownBy sjCodecOf[SampleUUID].fromJson(js2) should have message """Invalid UUID string: bogus""" + } + } + }