Compare commits

..

421 commits

Author SHA1 Message Date
Zac Sweers
6ffb94647b
Switch to openjdk8 for travis builds (#885)
* Switch to openjdk8 for travis builds

oraclejdk8 isn't available unless we ust trusty dist, which is also an option

* Update deploy_shapshot.sh
2019-08-07 22:56:03 -04:00
Nuno Gomes
687acba760 removed limitation where subtypes should be unique (#856)
* removed limitation where subtypes should be unique

There can be use cases where different type labels should match with the same subtype

* added test for PolymorphicJsonAdapter non unique subtypes
2019-06-20 10:39:19 -05:00
Jesse Wilson
54c44c5570
Merge pull request #871 from ZacSweers/z/kotlin1340
Update to Kotlin 1.3.40
2019-06-19 21:47:41 -04:00
Zac Sweers
6a1a15b974 Update to Kotlin 1.3.40 2019-06-19 20:06:50 -05:00
Jesse Wilson
10ac30b0ed
Merge pull request #868 from sullis/maven-compiler-plugin-3.8.1
maven-compiler-plugin 3.8.1
2019-06-19 06:38:45 -04:00
Sean Sullivan
0142e2aaf0 maven-compiler-plugin 3.8.1 2019-06-18 17:00:31 -04:00
Jesse Wilson
f1965f0f46
Merge pull request #821 from Syeberman/sye/covariant-map-values
Support covariant Map values, which are used by Kotlin
2019-06-06 10:49:51 -04:00
Sye van der Veen
7e417840e2 Support covariant Map values, which are used by Kotlin 2019-05-28 10:51:55 -04:00
Zac Sweers
0943ef5a61 Allow custom generators (#847)
* Extract generatedJsonAdapterName to public API for other generators/consumers

* Fix kapt location in tests

* Add IDE-generated dependency-reduced-pom.xml to gitignore

This always bites me

* Add generator property to JsonClass and skip in processor

* Opportunistically fix formatting for generateAdapter doc

* Extract NullSafeJsonAdapter for delegate testing

* Add custom adapter tests

* Allow no-moshi constructors for generated adapters

* Fix rebase issue

* Use something other than nullSafe() for lenient check

This no longer propagates lenient

* Add missing copyrights

* Add top-level class note

* Add note about working against Moshi's generated signature

* Add missing bit to "requirements for"

* Note kotlin requirement relaxed in custom generators

* Style
2019-05-15 20:42:08 -04:00
Zac Sweers
a5020ddb3c
Support gradle incremental processing in code gen (#824)
* Support gradle incremental processing in code gen

This adds support for incremental compilation in gradle via incap helper and marking the code gen as `ISOLATING`.

Depends on a newer version of KotlinPoet that has https://github.com/square/kotlinpoet/pull/647

Resolves #589

* Opportunistically update to auto-service 1.0-rc5

Supports incremental compilation and moves annotations to a separate artifact

* 1.2.0 final!

* Mark compiler embeddales as test only
2019-04-17 18:12:11 -07:00
Jesse Wilson
11a547023c
Merge pull request #834 from square/jakew/dont-wrap/2019-04-10
Don't wrap between throw and exception type
2019-04-10 08:38:38 -07:00
Jake Wharton
bcb6fd4a4d Don't wrap between throw and exception type 2019-04-10 10:28:30 -04:00
Oliver Bestmann
3994723c3c Use StringBuilder to deduplicate String constants 2019-04-02 11:49:40 -04:00
Zac Sweers
30c973e494
Merge pull request #825 from square/z/kotlin13
Update to Kotlin 1.3.21
2019-03-25 20:00:40 -07:00
Zac Sweers
1c3dc89da1 Update to Kotlin 1.3.21
Resolves #777
2019-03-25 19:44:03 -07:00
Eric Cochran
3e848c0cdd Use KotlinPoet's MemberName for emptySet import. (#818)
* Update to KotlinPoet 1.1.0.

* Use KotlinPoet's MemberName for emptySet import.
2019-03-14 02:57:08 -07:00
Eric Cochran
be6f3eb2af
Unconditionally close the peeked JsonReader. (#810)
It doesn't have an effect now, but this is for the future when closing the peeked source also clears buffers.
2019-02-17 17:00:29 -08:00
Eric Cochran
a83a9b79e4
Use peekJson in the DefaultOnDataMismatchAdapter example. (#809) 2019-02-15 17:09:50 -08:00
Eric Cochran
d843950731
Make checkstyle happy on JsonReader column width. (#811) 2019-02-15 17:08:46 -08:00
Eric Cochran
13a40edf5b
Correct error for duplicate JSON key for Kotlin. (#789)
Before, the KotlinJsonAdapter threw "java.lang.IndexOutOfBoundsException: Index: 0, Size: 0" on a duplicate key when the key matched a property that was not a constructor parameter.
2019-02-15 11:18:50 -08:00
Eric Cochran
126c8ea961
Support fail on unknown in PolymorphicJsonAdapterFactory. (#792)
Before, this would fail when skipping to find the index of the label.
Note that this still requires the type to have a label field.
2019-02-15 11:16:12 -08:00
Thomas Vos
f68035859e Fix typo in documentation for JsonReader.peekJson() (#797) 2019-02-15 11:15:38 -08:00
Eric Cochran
e04c80c1c6
Remove old TODO after KotlinPoet fix. (#790) 2019-02-15 11:15:02 -08:00
Jake Wharton
b8610c7fc7
Merge pull request #805 from Egorand/egorand/190209/broken-travis-config
Remove broken Travis config block
2019-02-09 20:18:26 -05:00
Egor Andreevici
136df54467 Remove broken Travis config block 2019-02-09 20:05:32 -05:00
Zac Sweers
ded3bccc60 Some miscellaneous kotlinpoet updates (#786)
* KotlinPoet 1.0.1

* Nix asNullableIf in favor of plain copy() function use

* Add a test for packages that look like classes

Resolves #783
2019-01-13 10:18:00 -05:00
Jesse Wilson
3588c98b86
Merge pull request #778 from square/z/defaultValue
Add support for default values in PolymorphicJsonAdapterFactory
2018-12-31 22:04:07 -05:00
Zac Sweers
fead71bca0 Add support for default values in PolymorphicJsonAdapterFactory
Picking from #741
Resolves #739
2018-12-31 16:21:57 -08:00
Zac Sweers
5153295988
Merge pull request #771 from square/z/removeRelocation
Remove shading of KotlinPoet
2018-12-11 19:35:47 -08:00
Zac Sweers
e6de367369 Remove shading of KotlinPoet
Resolves #770
2018-12-11 16:19:29 -08:00
Jesse Wilson
9f6197b67c
Merge pull request #769 from Egorand/egorand/181210/kotlinpoet-1.0.0
KotlinPoet 1.0.0
2018-12-11 11:05:12 -05:00
Egor Andreevici
6393692926 KotlinPoet 1.0.0 2018-12-10 19:59:37 -05:00
Erik Huizinga
85e86f1fa8 Add section about null safety (#757)
* Add section about null safety

Null safety is an important feature of the Kotlin language.
The fact that Moshi supports wrapping existing adapters in a null safe or non-null variant is important for null safety.
Therefore it makes sense to promote this feature in the readme, as this is one of the first places people should look for support and such features.

* Move factory method section out of Kotlin section

* Fix syntax highlighting

* Remove isLenient() reference

* Rewrite Kotlin example as Java, reformat comments
2018-12-03 22:50:52 -05:00
Jesse Wilson
2e241bff6f
Merge pull request #764 from square/jwilson.1128.kp10rc3
Upgrade to KotlinPoet 1.0-RC3
2018-11-29 07:03:27 -05:00
Jesse Wilson
ca4d8f5e34 Upgrade to KotlinPoet 1.0-RC3 2018-11-28 22:20:07 -05:00
Jesse Wilson
a2b39e29e8
Merge pull request #760 from square/eric.toJsonCoercesRuntimeTypeForMaps
Unignore ObjectAdapterTest's toJsonCoercesRuntimeTypeForMaps.
2018-11-27 21:48:38 -05:00
Eric Cochran
737f58cecb
Unignore ObjectAdapterTest's toJsonCoercesRuntimeTypeForMaps 2018-11-26 17:04:40 -08:00
Artem Daugel-Dauge
9aaef1f6f8 Treat warnings as errors in moshi-kotlin tests (#730)
* Fix/supress warnings in tests

* Treat kotlinc warnings as errors in tests
2018-11-23 22:24:19 -05:00
Jesse Wilson
2265f5e9b8
Merge pull request #755 from square/z/removesudo
Remove sudo: false from travis config
2018-11-23 22:22:24 -05:00
Zac Sweers
22259af1d1
Remove sudo: false from travis config
https://blog.travis-ci.com/2018-11-19-required-linux-infrastructure-migration
2018-11-23 19:12:30 -08:00
Zac Sweers
1f2a9373b7
Merge pull request #754 from square/z/kotlinTypesFollowup
Use faster isAnnotationPresent check
2018-11-20 17:51:59 -08:00
Zac Sweers
dbe7bfa15f Use faster isAnnotationPresent check
Followup from https://github.com/square/moshi/pull/749#discussion_r235224202
2018-11-20 17:20:09 -08:00
Jesse Wilson
afb82cb3e8
Merge pull request #751 from square/eric.platform-type
Improve error message for platform types.
2018-11-20 20:05:13 -05:00
Zac Sweers
c64138bf0a
Refuse kotlin classes in ClassJsonAdapter (#749)
* Refuse kotlin classes in ClassJsonAdapter

* Fail()

* Tweak message
2018-11-19 23:36:52 -08:00
Zac Sweers
4550da0abe
Merge pull request #750 from square/eric.tests
Fix KotlinJsonAdapter tests that were missing the factory.
2018-11-19 23:27:31 -08:00
Jake Wharton
f4f7c656bd
Merge pull request #752 from square/eric.p
Fix peekJson Javadoc paragraph formatting.
2018-11-19 13:46:49 -05:00
Eric Cochran
7d1657d76c
Fix peekJson Javadoc paragraph formatting. 2018-11-19 10:42:41 -08:00
Eric Cochran
a920b9be3a Improve error message for platform types.
The error message about platform types will never contain qualifiers.
Also, simplify platform type check.
2018-11-19 02:31:07 -08:00
Eric Cochran
1f46203285
Fix KotlinJsonAdapter tests that were missing the factory. 2018-11-19 01:58:25 -08:00
Jesse Wilson
8b17ecedae
Merge pull request #745 from square/eric.incorrect-skipValue
Fix infinite loop when calling skipValue at the end an object or array.
2018-11-18 10:18:02 -05:00
Eric Cochran
5f0b2ee8e3 Allow Object base type for PolymorphicJsonAdapterFactory. (#744)
* Allow Object base type for PolymorphicJsonAdapterFactory

This works now.
Using general types like Object, Map, or List for the base type is error-prone, but the checks for these cases are not worth the code cost.

* Delete redundant test.

Let's not encourage users to use Object as a base type by showing it in a test.
2018-11-18 10:15:41 -05:00
Zac Sweers
6e6abf54ab
Merge pull request #747 from square/eric.null-safe-adapters
Fix test for null-safe KotlinJsonAdapterFactory adapters.
2018-11-17 03:00:32 -08:00
Eric Cochran
38b08f81e6
Fix test for null-safe KotlinJsonAdapterFactory adapters. 2018-11-17 02:23:28 -08:00
Eric Cochran
5bf14098d1 Fix infinite loop when calling skipValue at the end an object or array.
Fail with a clear message. This is useful when iteratoring through an array, making assumptions about the size without using hasNext.

Also, disallow calling skipValue at the end of a document. This is a behavior change.
2018-11-16 10:20:38 -08:00
Jesse Wilson
5f5631e34f
Replace Okio 2.x with 2.1 2018-11-10 14:42:18 -05:00
Jesse Wilson
98459dd8ab
Reference 1.8 in the README 2018-11-09 09:41:10 +10:00
Jesse Wilson
6b6c1af907 [maven-release-plugin] prepare for next development iteration 2018-11-09 10:04:52 +11:00
Jesse Wilson
eb7110bf59 [maven-release-plugin] prepare release moshi-parent-1.8.0 2018-11-09 10:04:36 +11:00
Jesse Wilson
69d4f40a12 Update changelog for 1.8.0 2018-11-09 10:01:07 +11:00
Eric Cochran
878b3ff93b Add CheckReturnValue annotation to peekJson. (#736)
* Add CheckReturnValue annotation to peekJson.

* Suppress test warning.
2018-11-09 07:41:45 +11:00
Jake Wharton
103ae3cec6
Merge pull request #737 from square/eric.CheckReturnValue-writer
Add CheckReturnValue annotation to beginFlatten.
2018-11-06 11:06:02 -08:00
Eric Cochran
81db25825d
Add CheckReturnValue annotation to beginFlatten. 2018-11-06 10:15:01 -08:00
Stefan M
cf22bf9bce Add Gradle Kotlin DSL for dependencies (#719)
* Add Kotlin DSL for dependencies

* Use the "Kotlin" sytnax
2018-11-06 00:05:15 -08:00
Eric Cochran
5c41565f39 Fall back to reflection when generated adapter is not found. (#728)
* Fall back to reflection when generated adapter is not found.

This is handy for development builds where kapt is disabled and models still have @JsonClass(generatedAdapter = true).

* Add test for Kotlin reflection fallback behavior.
2018-11-06 18:28:36 +11:00
Jesse Wilson
a8102bccd2
Merge pull request #733 from daugeldauge/fix-variance-issues
Fix variance issues in kotlin-codegen
2018-11-05 07:22:40 +11:00
Artem Daugel-Dauge
8a22f6b133 Do not check required property for null second time (#732)
* Do not check required property second time

* Fix codestyle
2018-11-04 09:44:10 -08:00
Artem Daugel-Dauge
0e3a52b729 Use javaObjectType instead of Java primitives while creating parametrized type (#731)
* Add test for collection of primitives

* Use javaObjectType instead of Java primitives while creating parametrized type
2018-11-04 09:42:51 -08:00
Artem Daugel-Dauge
e0be5f5a54 Fix variance issues 2018-11-04 17:11:22 +03:00
Jesse Wilson
1acc70dd70
Merge pull request #710 from technoir42/keep-nested-class-adapters
Retain generated JsonAdapters for nested classes
2018-11-04 08:23:23 +10:00
technoir
9e3d2345f9 Retain generated JsonAdapters for nested classes 2018-11-04 00:52:05 +03:00
Jesse Wilson
fc4b92710e
Merge pull request #729 from square/jwilson.1103.fix_lookup_chain_prose
Fix prose in deferred adapters
2018-11-04 04:11:05 +10:00
Jesse Wilson
978d4dddd5 Fix prose in deferred adapters 2018-11-03 22:36:17 +11:00
Jesse Wilson
0cb2aef646
Merge pull request #726 from square/eric.kp$
Update to KotlinPoet 1.0.0-RC2.
2018-11-03 12:13:26 +10:00
Jesse Wilson
1e4be0e16f
Merge pull request #727 from square/eric.getBuffer
Use BufferedSource.getBuffer().
2018-11-03 12:12:05 +10:00
Eric Cochran
fe7ba863b4
Use BufferedSource.getBuffer(). 2018-11-02 17:03:45 -07:00
Eric Cochran
0795e9cbd5 Update to KotlinPoet 1.0.0-RC2.
Also, use KotlinPoet's escaping for $ characters in formatted strings.
This is partly a revert of 01f600c (https://github.com/square/moshi/pull/604/).
2018-11-02 16:59:27 -07:00
Jesse Wilson
9eb142d05b
Merge pull request #714 from square/jwilson.1021.fix_deferred_races
Fix a race condition on deferred adapters
2018-10-22 21:31:52 -04:00
Jesse Wilson
ce65ff5527 Fix a race condition on deferred adapters
This changes how we lookup and cache adapters. Previously we were pretty
optimistic about putting adapters in the cache; these adapters could have
depended upon stubs that were incomplete.

Now we're a lot more careful: we only put adapters in the cache if the
root object that triggered a set of recursive calls was constructed
successfully.

Closes: https://github.com/square/moshi/issues/679
2018-10-22 21:06:31 -04:00
Jesse Wilson
daa0441ab3
Merge pull request #713 from square/jwilson.1019.runtime_polymorphic
Rename RuntimeJsonAdapterFactory to PolymorphicJsonAdapterFactory
2018-10-20 00:54:14 -04:00
Jesse Wilson
89103b6d13 Rename RuntimeJsonAdapterFactory to PolymorphicJsonAdapterFactory
Also expand the documentation.
2018-10-20 00:47:48 -04:00
Jesse Wilson
1896e0f118
Merge pull request #697 from jocmp/master
Make RuntimeJsonAdapterFactory public
2018-10-19 23:33:06 -04:00
Jesse Wilson
ed5577b463
Merge pull request #712 from square/jwilson.1019.use_flatten
Don't decode into memory in RuntimeJsonAdapterFactory
2018-10-19 12:18:20 -04:00
Jesse Wilson
6125d8c7b1 Don't decode into memory in RuntimeJsonAdapterFactory
Instead use our new flatten API to decode directly to the stream.
2018-10-19 12:07:58 -04:00
Jesse Wilson
01a406eac9
Merge pull request #708 from square/jwilson.1013.flattening
JSON flattening.
2018-10-19 11:23:03 -04:00
Jesse Wilson
f28bca609a JSON flattening.
This is a new API that makes it possible to do more interesting things
with composition. It's currently write-only; doing composition on reads
is much more difficult.
2018-10-19 10:59:19 -04:00
Jesse Wilson
08bfedaeb2
Merge pull request #707 from square/jwilson.1009.streaming_RuntimeJsonAdapterFactory
Change RuntimeJsonAdapterFactory to peek for type names.
2018-10-13 20:43:53 -04:00
Jesse Wilson
24ac43a799 Change RuntimeJsonAdapterFactory to peek for type names.
This is a bit awkward because JsonReader.Options doesn't tell you
what its values are.

Also awkward because we don't yet have an equivalent to stream
the encode of the value.
2018-10-09 23:10:31 -04:00
Jesse Wilson
cfaf62c95f
Merge pull request #706 from square/eric.javadoc
Fix Javadoc code formatting for JsonReader.peek().
2018-10-08 15:54:57 -04:00
Eric Cochran
931673c264
Fix Javadoc code formatting for JsonReader.peek(). 2018-10-08 12:48:44 -07:00
Jesse Wilson
2a4d37ece4
Merge pull request #704 from square/jwilson.1007.peekutf8
Implement peekJson() for JsonUtf8Reader
2018-10-08 12:27:02 -04:00
Jesse Wilson
01c45ca2a8
Merge pull request #705 from square/jwilson.1008.register_as_factory
Document how to register JsonAdapters
2018-10-08 12:24:17 -04:00
Jesse Wilson
7055391230 Implement peekJson() for JsonUtf8Reader
This required an upgrade to Okio 1.16.0.
2018-10-08 11:44:03 -04:00
Jesse Wilson
fe22970973 Document how to register JsonAdapters
Closes: https://github.com/square/moshi/issues/698
2018-10-08 11:14:57 -04:00
Jesse Wilson
eb64d186a6
Merge pull request #680 from square/jwilson.0923.peekvalue
Implement peekJson() for JsonValueReader
2018-10-07 16:59:20 -04:00
Jesse Wilson
10e2b77585
Merge pull request #703 from square/z/allow-other-processors
Allow other processors to process JsonClass
2018-10-06 12:40:15 -04:00
Zac Sweers
a34ca365f0
Allow other processors to process JsonClass
Someone at KotlinConf brought this to my attention, as they're trying to generate their own adapter factory but can't since moshi consumes the annotation and doesn't allow others.
2018-10-06 17:05:06 +02:00
Zac Sweers
bd2f2c4c28 Mention proguard 6 requirement (#686)
* Mention proguard 6 requirement

* Mention AGP instead
2018-09-29 22:59:52 -04:00
Zac Sweers
e0861cca57
Improve enum exception message (#694)
* Improve enum exception message

It is applicable to enums for proguard reasons, just not with code gen as well

* Update test message
2018-09-28 00:24:58 -04:00
Zac Sweers
21a9c56975
Merge pull request #693 from technoir42/keep-enum-fields
Always keep fields of annotated enums
2018-09-27 23:42:22 -04:00
Zac Sweers
2990583a1f
Merge pull request #695 from square/jwilson.0927.dollar
Don't use @Language when the literal includes a dollar sign
2018-09-27 23:41:43 -04:00
Josiah Campbell
80f651a3eb Make RuntimeJsonAdapterFactory public 2018-09-27 10:44:55 -05:00
Jesse Wilson
a5d35730de Don't use @Language when the literal includes a dollar sign
It makes IntelliJ grumpy.
https://youtrack.jetbrains.com/issue/KT-27224
2018-09-27 09:45:44 -04:00
sergey
b5db9fa7f1 Always keep fields of annotated enums 2018-09-27 00:54:39 +03:00
Gabriel Ittner
b7055944a9 Proguard config: keep field names of annotated enums (#691)
closes #689
2018-09-25 11:52:05 -07:00
Jesse Wilson
705ddc24e3
Merge pull request #687 from hzsweers/z/removeExtensions
Completely remove companion object jsonAdapter extension function gen
2018-09-25 07:08:54 -04:00
Jesse Wilson
11cbd5eae2
Merge pull request #688 from square/eric.version
Remove duplicate kotlin-metadata version declaration.
2018-09-25 07:06:16 -04:00
Eric Cochran
526717ec36
Remove duplicate kotlin-metadata version declaration.
The version is already defined in the root.
2018-09-25 00:17:32 -07:00
Zac Sweers
86c8671d64 Completely remove companion object jsonAdapter extension function gen
It's broken currently (see #611), and after talking with @swankjesse decided it's best to just nix this API
2018-09-25 00:58:52 -04:00
Jesse Wilson
00dcac60d4 Implement peekJson() for JsonValueReader
The JsonUtf8Reader variant relies on an update to Okio that we're
not quite ready for.

This one became straightforward after I changed out the iterators
to be cloneable. That's necessary to split one iterator into two.
It's too bad the platform's built-in iterators are not cloneable;
that would have been convenient and potentially more efficient.

Related to https://github.com/square/moshi/issues/672
2018-09-24 23:21:07 -04:00
Jesse Wilson
2cb81857ac [maven-release-plugin] prepare for next development iteration 2018-09-24 22:55:49 -04:00
Jesse Wilson
9a652f8788 [maven-release-plugin] prepare release moshi-parent-1.7.0 2018-09-24 22:55:35 -04:00
Jesse Wilson
662386cc03
Merge pull request #685 from square/jwilson.0924.internal_dopcs
Don't include the 'internal' package in Javadocs
2018-09-24 22:50:43 -04:00
Jesse Wilson
026d16ba68 Don't include the 'internal' package in Javadocs 2018-09-24 22:35:29 -04:00
Jesse Wilson
efd324b82f Update changelog for 1.7.0. 2018-09-24 22:26:55 -04:00
Jesse Wilson
73ca7765c0
Merge pull request #684 from square/jwilson.0924.upgrayedd
Upgrade Kotlin, AssertJ, compile-testing, and kotlin-metadata depende…
2018-09-24 22:13:43 -04:00
Jesse Wilson
6e6e533869
Merge pull request #683 from square/jwilson.0924.change_visibility
Hide RuntimeJsonAdapterFactory for the forthcoming release.
2018-09-24 21:49:25 -04:00
Jesse Wilson
735f0c39f7 Upgrade Kotlin, AssertJ, compile-testing, and kotlin-metadata dependencies 2018-09-24 21:48:25 -04:00
Jesse Wilson
11241a2b02 Hide RuntimeJsonAdapterFactory for the forthcoming release.
We don't yet have the behavior we want for looking ahead at the type label.
2018-09-24 21:03:20 -04:00
Jesse Wilson
f873bd93b7
Merge pull request #682 from sullis/maven-compiler-plugin-3.8.0
maven-compiler-plugin 3.8.0
2018-09-24 10:31:31 -04:00
Sean Sullivan
4392642f03 maven-compiler-plugin 3.8.0 2018-09-23 21:12:54 -07:00
Jesse Wilson
f466909967
Merge pull request #674 from square/eric.2018-09-18.codegen-adapter-lookup-error-hint
Use the adapter lookup hint API in Kotlin codegen adapters.
2018-09-23 21:16:52 -04:00
Eric Cochran
5a46cd6bd5
Use BufferedSource.indexOf instead of loops. (#677)
Cleanup of a rare code path for block comments in lenient readers.
2018-09-20 21:20:11 -07:00
Eric Cochran
242b7b1055 Use the adapter lookup hint API in Kotlin codegen adapters. 2018-09-20 21:00:18 -07:00
Jesse Wilson
1ba25ef3f9
Merge pull request #660 from square/eric.fix-preemptive-null-check
Allow user adapters to convert null to non-null in codegen.
2018-09-20 23:36:40 -04:00
Jesse Wilson
a813cd1352
Merge pull request #665 from square/eric.runtime-adapter-object-base
Fix RuntimeJsonAdapterFactory breaking with Object as the base type.
2018-09-20 23:34:30 -04:00
Jesse Wilson
226c0c14f2
Merge pull request #673 from square/eric.property-name-hint
Use the adapter lookup hint API in the Kotlin reflection adapter.
2018-09-20 23:32:21 -04:00
Eric Cochran
05cfb77430
Use the adapter lookup hint API in the Kotlin reflection adapter. 2018-09-18 18:00:22 -07:00
Eric Cochran
810199a442 Disallow Object for RuntimeJsonAdapterFactory's base type. 2018-09-18 17:57:11 -07:00
Benoît Quenaudon
e0cdcd4ff8
Merge pull request #668 from square/bquenaudon/2018-09-13/fix-jsonadapter-test
Fix JsonQualifiers test
2018-09-14 09:00:13 -04:00
Benoit Quenaudon
df730cafb9
Fix JsonQualifiers test 2018-09-13 15:02:06 -04:00
Eric Cochran
895c3ddb49 Track field names in the adapter lookup stack. (#616)
* Track field names in the adapter lookup stack.

This allows for an error message that includes field names to track down the cause of adapter creation failure for deeply nested structures.

* Use a single stack in Moshi client.

* Make optional fieldName parameter public API.

* Simplify error message.
2018-09-12 22:27:58 -04:00
Jesse Wilson
83b6b26e63
Merge pull request #659 from square/eric.exception-cause
Add exception cause for method adapter creation.
2018-09-12 22:24:22 -04:00
Jesse Wilson
62c14b872d
Merge pull request #658 from square/eric.okio15
Update to Okio 1.15.0.
2018-09-12 22:23:56 -04:00
Zac Sweers
d6ad1b8bad
Merge pull request #667 from oldergod/bquenaudon/2018-09-12/androidx
Don't reflect on androidX
2018-09-12 15:08:01 -07:00
Benoit Quenaudon
c606f43a3d
Don't reflect on androidX 2018-09-12 15:11:42 -04:00
Eric Cochran
8e151b1df3 Allow user adapters to convert null to non-null in codegen.
Delegate to installed adapters instead of checking for null.
2018-09-11 22:25:42 -07:00
Eric Cochran
46a42bc7ed Add exception cause for method adapter creation. 2018-09-11 22:25:09 -07:00
Eric Cochran
3fa09dd110 Update to Okio 1.15.0. 2018-09-11 22:24:38 -07:00
Jesse Wilson
9050e42038
Merge pull request #656 from square/jwilson.0910.nicer_exceptions
Generate nicer stacktraces when creating a generated adapter fails.
2018-09-11 22:08:18 -04:00
Jesse Wilson
76cd590ca3 Generate nicer stacktraces when creating a generated adapter fails.
Otherwise we end up with many InvocationTargetExceptions layered in, which makes
it difficult to identify what actually failed.
2018-09-11 22:02:54 -04:00
Jesse Wilson
5bd9632756
Merge pull request #663 from square/jwilson.0911.fix_nonkotlin_test_
Don't do Kotlin reflection on a non-Kotlin class.
2018-09-11 22:02:27 -04:00
Jesse Wilson
c04f1bafde Don't do Kotlin reflection on a non-Kotlin class.
Closes: https://github.com/square/moshi/issues/662
2018-09-11 21:50:02 -04:00
Jesse Wilson
9f2ed487b2
Merge pull request #653 from square/eric.copyrightheader
Add copyright header to EnumJsonAdapterTest.
2018-09-10 20:26:18 -04:00
Jesse Wilson
484fdfe4b6
Merge pull request #655 from square/eric.2018-08-17.nullSafe-generated-adapters
Make generated adapters null-safe.
2018-09-10 19:44:26 -04:00
Eric Cochran
e7c745aac8 Make generated adapters null-safe.
This is in alignment with the lookup for Java and Kotlin reflective adapters.
2018-09-10 16:31:24 -07:00
Eric Cochran
67d07eb450
Add copyright header to EnumJsonAdapterTest. 2018-09-10 15:33:39 -07:00
Jesse Wilson
d1c2cf9c6c
Merge pull request #649 from square/jwilson.0909.uppercase_property
Test support for uppercase property names
2018-09-09 23:32:10 -04:00
Jesse Wilson
c008e0e2b6 Test support for uppercase property names
I couldn't reproduce the reported issue.

Closes: https://github.com/square/moshi/issues/574
2018-09-09 23:25:56 -04:00
Jesse Wilson
29d08353ec
Merge pull request #650 from square/jwilson.0909.private_transient
Test that @Transient private properties are ignored
2018-09-09 23:24:53 -04:00
Jesse Wilson
1ba07d4b7d Test that @Transient private properties are ignored
Closes: https://github.com/square/moshi/issues/643
2018-09-09 23:17:50 -04:00
Jesse Wilson
cb86194f8f
Merge pull request #651 from square/jwilson.0909.enclosed_parameterized
Call Types.newParameterizedTypeWithOwner when necessary.
2018-09-09 23:16:38 -04:00
Jesse Wilson
f980521c8e
Merge pull request #652 from square/jwilson.0909.include_labels
Include labels when encoding with RuntimeJsonAdapterFactory.
2018-09-09 23:12:44 -04:00
Jesse Wilson
e7cae30bd8
Merge pull request #648 from square/jwilson.0909.delegate_key
Change DelegateKey to use AnnotationSpec instead of AnnotationMirror
2018-09-09 23:12:26 -04:00
Jesse Wilson
29bb93bc29 Include labels when encoding with RuntimeJsonAdapterFactory.
Otherwise the adapter is not symmetric.
2018-09-09 17:43:42 -04:00
Jesse Wilson
306664fb6a Call Types.newParameterizedTypeWithOwner when necessary.
Otherwise we crash with an exception attempting to create an adapter
for an enclosed type that has a type parameter.

I ran into this looking for a test case for issue 615.
https://github.com/square/moshi/issues/615
2018-09-09 17:05:33 -04:00
Jesse Wilson
7382145318 Change DelegateKey to use AnnotationSpec instead of AnnotationMirror
AnnotationSpec implements equals() in the way we need, but
AnnotationMirror doesn't. As a consequence this fixes a problem
where we were generating redundant adapters.

Closes: https://github.com/square/moshi/issues/563
2018-09-09 15:27:21 -04:00
Jesse Wilson
4f3c418202
Merge pull request #647 from square/jwilson.0909.confirm_one_way_code_gen
Confirm you can use codegen with adapter methods
2018-09-09 14:00:04 -04:00
Jesse Wilson
f5fe86dd78 Confirm you can use codegen with adapter methods
We had a bug report that said using just @ToJson wasn't working.
This test attempts to show it should work just fine.

https://github.com/square/moshi/issues/545
2018-09-09 12:16:18 -04:00
Jesse Wilson
2a593da06c
Merge pull request #628 from gabrielittner/master
Embed ProGuard rules in the jar
2018-09-08 14:58:44 -04:00
Gabriel Ittner
24e0777ebd
add period to comments 2018-09-07 08:04:56 +02:00
Jesse Wilson
3c470575f4
Merge pull request #640 from square/eric.null-enum
Allow null fallback enum value in EnumJsonAdapter.
2018-09-03 09:47:16 -04:00
Eric Cochran
8a8cde0ce3 Allow null fallback enum value in EnumJsonAdapter. 2018-08-31 00:51:34 -07:00
Gabriel Ittner
597da2d861
keep BuiltInsLoaderImpl for kotlin reflect artifact 2018-08-24 10:47:22 +02:00
Jesse Wilson
f9c53f39f4
Merge pull request #629 from stoyicker/patch-1
Remove unnecessary isAccessible check
2018-08-19 16:21:39 -04:00
Jorge Antonio Díaz-Benito Soriano
56e67088a9
Remove unnecessary isAccessible check
https://github.com/square/moshi/issues/624
2018-08-19 20:46:43 +02:00
Jesse Wilson
354db6b46f
Merge pull request #627 from square/eric.2018-08-17.boxed-primitive-adapters
Make nullable primitives in codegen use boxed type adapters.
2018-08-19 07:36:12 -04:00
Jesse Wilson
0c773a38f4
Merge pull request #623 from square/jwilson.0814.dokka
Generate Kotlin documentation with Dokka
2018-08-19 07:30:52 -04:00
Gabriel Ittner
4bbc5b2ff8
Embed ProGuard rules in the jar 2018-08-19 11:14:47 +02:00
Eric Cochran
1b17423343 Make nullable primitives in codegen use boxed type adapters. 2018-08-17 13:41:43 -07:00
Eric Cochran
83f60d6bd7 Fix README link to jar. (#622) 2018-08-14 22:54:30 -04:00
Jesse Wilson
9ea1f845a8 Generate Kotlin documentation with Dokka 2018-08-14 20:20:15 -04:00
Eric Cochran
29bdc0aa45
Add EnumJsonAdapter to adapters module. (#607) 2018-08-07 15:05:56 -07:00
Eric Cochran
137ffc992f
Fix incorrect path in enum adapter error message. (#613) 2018-08-06 22:46:41 -07:00
Jesse Wilson
34f8f9472f
Merge pull request #612 from square/jwilson.0805.extension_property
Test to confirm that extension properties are not encoded or decoded
2018-08-06 04:36:34 -04:00
Eric Cochran
0f1fa3d385 Add RuntimeJsonAdapterFactory to adapters module. (#606)
* Add RuntimeJsonAdapterFactory to adapters module.

* Make RuntimeJsonAdapterFactory create null-safe adapters.

* Add copyright headers.
2018-08-06 04:35:53 -04:00
Jesse Wilson
9251309c3f
Merge pull request #603 from square/eric.patch-1
Simplify Options initializer generation.
2018-08-05 22:36:29 -04:00
Jesse Wilson
01f600cdd3
Merge pull request #604 from square/eric.💲
Fix dollar sign escaping for json key names.
2018-08-05 22:34:21 -04:00
Jesse Wilson
c360a1c840 Test to confirm that extension properties are not encoded or decoded 2018-08-05 22:20:25 -04:00
Jesse Wilson
3aa31d9135
Merge pull request #608 from square/eric.checkstyle
Remove unused import.
2018-08-05 21:26:52 -04:00
Eric Cochran
ee873b64f6
Remove unused import. 2018-07-26 17:28:15 -07:00
Eric Cochran
248be5805b Fix dollar sign escaping for json key names. 2018-07-24 18:03:23 -07:00
Eric Cochran
6bb83abf84
Simplify Options initializer generation. 2018-07-24 17:51:18 -07:00
Zac Sweers
78821bbc80 Update to KotlinPoet 1.0.0-RC1 (#599)
* Update to kotlinpoet 1.0.0-RC1

* Move to WildcardTypeName.STAR

* simpleNames() function -> simpleNames property

* packageName() fun -> packageName property

* simpleName() fun -> simpleName property

* Check if bounds are empty for TypeVariableName. If so, use no-bounds creator

* Use new parameterizedBy/plusParameter API where appropriate
2018-07-17 22:44:20 -04:00
Jesse Wilson
bcd61c9621
Merge pull request #592 from runningcode/no/readme-update
Update readme to add information about depending on stdlib.
2018-07-03 16:57:56 -04:00
Nelson Osacky
2d7d2c116d Update readme to add information about depending on stdlib. 2018-07-03 22:52:49 +02:00
Jesse Wilson
0b0883db68
Merge pull request #584 from square/eric.emptyDocumentHasNextReturnsFalse
Fix hasNext to return false at document end.
2018-06-24 10:34:46 -04:00
Jesse Wilson
de79ed364e
Merge pull request #582 from hzsweers/z/fixGeneratedAnnotation
Fix GeneratedAnnotation member definition
2018-06-24 10:03:08 -04:00
Eric Cochran
627e62f507 Fix hasNext to return false at document end. 2018-06-22 15:18:22 -07:00
Zac Sweers
c9aee2e853 Fix GeneratedAnnotation member definition
Before this was just adding both as varargs of "value"
2018-06-15 22:09:13 -07:00
Jesse Wilson
03ada87e90
Merge pull request #576 from square/eric.private-transient
Allow private transient Kotlin properties.
2018-06-07 10:26:44 -04:00
Eric Cochran
4666e06910 Allow private transient Kotlin properties. 2018-06-06 17:12:29 -07:00
Jake Wharton
cd9e600955
Merge pull request #559 from square/eric.word
Remove redundant "to" in doc.
2018-05-23 15:29:30 -04:00
Eric Cochran
df3a6ce2ae
Remove redundant "to" in doc. 2018-05-23 12:28:07 -07:00
Zac Sweers
3ecdfb6374 Fix generic typealiases (#551)
* Add generic type alias

* Fix missing resolveAliases check to fix generics

* Reword to useAbbreviatedType
2018-05-17 22:06:43 -04:00
Jesse Wilson
7470536606
Merge pull request #553 from LouisCAD/patch-1
Fix codegen dependency configuration in README
2018-05-17 10:20:55 -04:00
Louis CAD
dd86599d5b
Fix codegen dependency configuration
annotationProcessor configuration is for java. Kotlin uses kapt
2018-05-17 16:14:35 +02:00
Zac Sweers
b956b06f6d Fix companion object names not being resolved (#549)
* Fix companion object names not being resolved

This slipped through the cracks before the release

Fixes #546

* Add braces on the else clause for symmetry
2018-05-16 15:44:07 -04:00
Jesse Wilson
d48e3aaa27
Merge pull request #548 from square/jwilson.0516.fix_parent_relative_link
Fix the link to the parent pom.xml in kotlin/reflect
2018-05-16 14:48:47 -04:00
Jesse Wilson
03f17310bc Fix the link to the parent pom.xml in kotlin/reflect 2018-05-16 14:43:38 -04:00
Jesse Wilson
6142a167e9
Merge pull request #543 from square/eric.checkreturn
Add more CheckReturnValues for JsonReader.
2018-05-15 22:03:02 -04:00
Eric Cochran
d31f3c2482 Add more CheckReturnValues for JsonReader.
This encourages skipName over nextName.
2018-05-15 18:37:29 -07:00
Pierre Degand
b5a50d8281 Update Proguard configuration for codegen (#542)
* Update Proguard configuration for codegen

-keepnames will prevent Proguard from renaming the class during obfuscation phase but won't protect the class from code shrinking.
In most cases, the generated are never referenced directly in the code as the adapters are loaded dynamically.
This means that, for Proguard, the class is unused and it will remove the generated adapters during the shrinking phase.

The adapters need to be kept as well as there primary constructor.

* Keep fields of generated JsonAdapter
2018-05-15 21:34:56 -04:00
Jesse Wilson
16938ab83a
Merge pull request #541 from LouisCAD/patch-1
Add missing val and fix indentation in README
2018-05-15 06:15:33 -04:00
Jesse Wilson
279b1e00a7
Merge pull request #539 from 3flex/patch-1
README: fix typo
2018-05-15 06:14:42 -04:00
Jesse Wilson
40817a2f2a
Merge pull request #538 from hzsweers/patch-2
Fix adapter factory name in proguard mention
2018-05-15 06:13:59 -04:00
Louis CAD
6187be0c59
Add missing val and fix indentation in README 2018-05-15 09:23:13 +02:00
Matthew Haughton
373209640d
README: fix typo 2018-05-15 16:52:11 +10:00
Zac Sweers
0c39719d12
Fix adapter factory name in proguard mention
Derp
2018-05-14 20:54:30 -07:00
Jesse Wilson
a8616ff10d
Merge pull request #537 from hzsweers/patch-1
Update proguard rules to differentiate between reflect or codegen
2018-05-14 23:35:30 -04:00
Zac Sweers
defebcf8e5
Update proguard rules to differentiate between reflect or codegen 2018-05-14 20:22:23 -07:00
Jesse Wilson
6e411eb243 Update ProGuard rules for Kotlin codegen 2018-05-14 23:17:20 -04:00
Jesse Wilson
31ef245eeb [maven-release-plugin] prepare for next development iteration 2018-05-14 23:00:15 -04:00
Jesse Wilson
bf4d1f8693 [maven-release-plugin] prepare release moshi-parent-1.6.0 2018-05-14 23:00:02 -04:00
Jesse Wilson
6deb12bdc8 Update README for 1.6.0. 2018-05-14 22:57:59 -04:00
Jesse Wilson
0b26628232
Merge pull request #536 from square/jwilson.0514.cast_shade
Shade unreleased KotlinPoet 0.7.0.
2018-05-14 22:52:35 -04:00
Jesse Wilson
9c55f5df59
Merge pull request #535 from square/jwilson.0514.skipName
Generated adapters should use skipName(), not nextName().
2018-05-14 22:52:28 -04:00
Jesse Wilson
b857388796 Shade unreleased KotlinPoet 0.7.0.
That way we won't collide with other annotation processors if they have an
incompatible version.
2018-05-14 22:40:29 -04:00
Jesse Wilson
dd3043722e Generated adapters should use skipName(), not nextName(). 2018-05-14 22:39:33 -04:00
Jesse Wilson
50a5ef3e7d Update changelog for 1.6 2018-05-14 21:54:34 -04:00
Jesse Wilson
c935fe36a8 Update readme for Kotlin codegen 2018-05-14 21:46:55 -04:00
Jesse Wilson
62e6363914
Merge pull request #534 from square/jwilson.0514.yield
Change the Kotlin reflection adapter to yield to the codegen adapter
2018-05-14 21:46:26 -04:00
Jesse Wilson
dd84b9f8f8 Change the Kotlin reflection adapter to yield to the codegen adapter 2018-05-14 21:07:46 -04:00
Jesse Wilson
3f1e4b5a3d
Merge pull request #533 from square/jwilson.0514.kotlin_reflect
Move Kotlin reflection into a kotlin/reflect directory
2018-05-14 18:03:10 -04:00
Jesse Wilson
60cb608956 Move Kotlin reflection into a kotlin/reflect directory
The maven coordinates stay the same.
2018-05-14 17:28:28 -04:00
Jesse Wilson
a0df085b81
Merge pull request #532 from square/jwilson.0514.move_kompiler
Move the Kotlin code generator to the kotlin/ module
2018-05-14 16:36:34 -04:00
Jesse Wilson
9f69029ef0 Move the Kotlin code generator to the kotlin/ module 2018-05-14 16:29:52 -04:00
Jesse Wilson
c5c4cac6c3
Merge pull request #531 from square/jwilson.0513.track_test_cases
Finish migrating tests from the reflective adapter
2018-05-14 12:10:33 -04:00
Jesse Wilson
13952c5430 Finish migrating tests from the reflective adapter 2018-05-13 21:52:06 -04:00
Zac Sweers
298aff24f5 Fix nullability not being preserved and clean up from shadowed names (#529)
* Add helper TypeName.asNullableIf extension

* Add missing nullability preservers to TypeResolver

* Fix shadowed names and add more missing nullable stuff
2018-05-13 14:38:02 -04:00
Jesse Wilson
986cc4c794 [maven-release-plugin] prepare for next development iteration 2018-05-06 21:50:45 -04:00
Jesse Wilson
c2f890879c [maven-release-plugin] prepare release moshi-parent-1.6.0-RC1 2018-05-06 21:50:39 -04:00
Jesse Wilson
1407ca4392 Update changelog for forthcoming release 2018-05-06 21:48:28 -04:00
Jesse Wilson
7b1177adbc
Merge pull request #503 from square/eric.write-from-source
Allow writing out raw JSON.
2018-05-06 21:11:34 -04:00
Jesse Wilson
c39fc12729
Merge pull request #523 from hzsweers/z/nonNullTypeVariabels
Fix nullable properties of TypeVariable types
2018-05-06 21:10:38 -04:00
Zac Sweers
4b610329bd Full JsonQualifier support in kotlin codegen. 2018-05-06 21:09:28 -04:00
Zac Sweers
54aca07ca1 Fix nullable properties of TypeVariable types
We were forgetting to apply the property's nullability to the resolved type.

Fixes #521
2018-05-04 13:29:50 -07:00
Jesse Wilson
10a5dc827b
Merge pull request #524 from square/jwilson.0504.green_green
Fix some tests that have the wrong expected exception message
2018-05-04 15:49:16 -04:00
Jesse Wilson
c35e3a1550 Fix some tests that have the wrong expected exception message
The message got improved in a conflicting change to these tests being added.
2018-05-04 15:39:55 -04:00
Jesse Wilson
eb24a23568
Merge pull request #511 from square/eric.non-null
Fix error message for assigning to non-null properties.
2018-05-04 15:06:35 -04:00
Jesse Wilson
98c4358615
Merge pull request #519 from square/eric.kotlin-path
Fix path for non-null value message.
2018-05-04 14:31:39 -04:00
Jesse Wilson
bb2705128c
Merge pull request #520 from square/eric.skipName
Add JsonReader.skipName.
2018-05-04 14:14:00 -04:00
Eric Cochran
b848f1cc52 Add JsonReader.skipName. 2018-04-30 18:41:18 -07:00
Eric Cochran
dfaf3405b2 Fix path for non-null value message. 2018-04-30 18:18:34 -07:00
Eric Cochran
a0cd8a4fc0 Allow writing out raw JSON. 2018-04-29 23:49:47 -07:00
Jesse Wilson
7018cec47d
Merge pull request #516 from hzsweers/z/metadata14
Update to kotlin-metadata 1.4 and use shaded compiler
2018-04-29 20:45:20 -04:00
Zac Sweers
d195203865 Update to kotlin-metadata 1.4 and use shaded compiler
Per https://github.com/Takhion/kotlin-metadata/releases/tag/v1.4.0

Now the compiler is shaded and not prone to breaking on kotlin updates, making it more robust until Jetbrains releases an official API for reading metadata.
2018-04-28 20:30:30 -07:00
Jesse Wilson
b96397f6eb
Merge pull request #500 from square/eric.coherent-nesting-problem-error-message
Add coherent error message for uneconded map keys.
2018-04-28 21:14:45 -04:00
Jake Wharton
4f3f74f016
Merge pull request #515 from square/eric.versions
Group dependency version codes together.
2018-04-27 10:27:41 -04:00
Jake Wharton
16fd551176
Merge pull request #513 from square/eric.ep
Update Error Prone to 2.3.1.
2018-04-27 10:27:14 -04:00
Jake Wharton
fa4aa364e1
Merge pull request #512 from square/eric.moshi-docs
Fix up out-of-date comments.
2018-04-27 10:27:03 -04:00
Eric Cochran
1589ca8ddb Group dependency version codes together. 2018-04-25 16:40:20 -07:00
Eric Cochran
0d8b5efaa1
Update Error Prone to 2.3.1. 2018-04-25 16:37:46 -07:00
Eric Cochran
44e6fbd067 Add coherent error message for unencoded map keys. 2018-04-25 11:36:47 -07:00
Eric Cochran
b125f06e70
Fix up out-of-date comments. 2018-04-24 13:17:40 -07:00
Eric Cochran
51d23b5b33 Fix error message for assigning to non-null properties.
instead of falling down to "Required property 'a' missing at $"
2018-04-24 13:05:22 -07:00
Jesse Wilson
1c68437f3c
Merge pull request #506 from square/jwilson.0415.type_resolver
Begin to resolve supertype type parameters
2018-04-16 22:34:03 -04:00
Jesse Wilson
48698a61ad
Merge pull request #507 from square/eric.null-assertion
Disallow null Type from entering user code.
2018-04-16 22:32:59 -04:00
Eric Cochran
84745b0537 Disallow null Type from entering user code.
Otherwise, Moshi.adapter(null) will end up calling into user JsonAdapter factories with a null Type parameter.
Kotlin fails with java.lang.IllegalArgumentException: Parameter specified as non-null is null
That message ends up too far away from the real error.
2018-04-15 21:04:17 -07:00
Jesse Wilson
cc2c818341 Begin to resolve supertype type parameters 2018-04-15 23:21:15 -04:00
Eric Cochran
b860b6da4f
Make error message for dangling names consistent. (#501) 2018-04-15 19:48:32 -07:00
Jesse Wilson
3a2367036c
Merge pull request #505 from square/jwilson.0415.honor_kotlin_supertypes
Support generated adapters for Kotlin superclasses
2018-04-15 15:51:25 -04:00
Jesse Wilson
9401a810f0 Support generated adapters for Kotlin superclasses 2018-04-15 14:37:49 -04:00
Jesse Wilson
8d24d89abf
Model target types, parameters, constructors and properties (#504)
This is intended to make it easier to implement support for subtypes.
2018-04-15 13:46:58 -04:00
Eric Cochran
78091aeb46 Fix JsonUtf8Writer to be strict about names in the wrong place. (#502) 2018-04-15 09:39:04 -04:00
Zac Sweers
941229b6c9 Add apoptions support in KotlinCompilerCall (#499)
* Add apoptions support to KotlinCompilerCall

* Add bad annotated annotation test
2018-04-11 22:30:21 -04:00
Jesse Wilson
f9b758b5bf
Merge pull request #498 from square/jwilson.0410.multiple_types_example
Add an example that decodes multiple formats
2018-04-10 22:20:52 -04:00
Jesse Wilson
c4e4e8582d Add an example that decodes multiple formats 2018-04-10 22:08:31 -04:00
Jesse Wilson
91417d58c6
Merge pull request #496 from square/jwilson.0409.setters
Support properties that don't have a backing field.
2018-04-10 21:15:55 -04:00
Jesse Wilson
d1df4740d5 Support properties that don't have a backing field.
Currently our main loop to gather PropertyGenerators goes over the backing fields.
This needs to change to iterate over the properties themselves. That leads to a lot
of churn. The net result is slightly more compatibility with the reflective adapter.
2018-04-09 00:08:15 -04:00
Jesse Wilson
e80cf48484
Merge pull request #490 from square/jwilson.0407.generate_nonnull
Use JsonAdapter.nonNull() in generated adapters.
2018-04-08 21:05:44 -07:00
Jesse Wilson
cb9c084d30 Use JsonAdapter.nonNull() in generated adapters.
Also extract a type for the delegate key.

Also fix the generator to reject inner classes, abstract classes,
and local classes.
2018-04-08 20:54:50 -04:00
Jesse Wilson
0a49ae3ac8
Merge pull request #491 from square/eric.jsonclass-data
Remove data class limitation from JsonClass doc.
2018-04-08 05:26:37 -07:00
Jesse Wilson
ceef5dc682
Merge pull request #493 from square/eric.never-type-equals
Fix type checks with custom adapters.
2018-04-08 05:26:13 -07:00
Jesse Wilson
e43a173f46
Merge pull request #494 from square/eric.2018-04-07.with-no-annotations
Make "no adapter" error message friendlier.
2018-04-08 04:27:55 -07:00
Eric Cochran
ba1318cc45 Make "no adapter" error message friendlier.
In the very common case, there are no JsonQualifier annotations for the type.
2018-04-08 00:48:38 -07:00
Eric Cochran
fa1f10dc77
Fix type checks with custom adapters.
Moshi.Builder.add(Type, ...) adds a factory that had a broken type equality check.
Closes #128
2018-04-08 00:42:46 -07:00
Eric Cochran
c4a2e7657f
Remove data class limitation from JsonClass doc. 2018-04-07 20:33:22 -07:00
Jesse Wilson
2cc878da81
Merge pull request #489 from jemaystermind/jemaystermind/indent-json
Indent JSON string properly
2018-04-07 05:07:03 -07:00
Jeremy Tecson
bfa14a0d66
Indent JSON string properly 2018-04-07 17:59:02 +08:00
Jesse Wilson
5ecb55ad1e
Merge pull request #488 from square/eric.okio-update
Update to Okio 1.14.0.
2018-04-06 20:20:52 -07:00
Jesse Wilson
73f8774aa8
Merge pull request #487 from square/eric.adapters-readme
Use direct link to adapters snapshots.
2018-04-06 20:20:32 -07:00
Jesse Wilson
b52d63dfbf
Merge pull request #486 from square/eric.integration-reflect
Remove kotlin-reflect dep from integration-test.
2018-04-06 20:19:55 -07:00
Eric Cochran
083210eb40 Add generated code file comment. (#485) 2018-04-06 20:18:43 -07:00
Eric Cochran
14f2dcc357
Update to Okio 1.14.0. 2018-04-06 17:10:33 -07:00
Eric Cochran
e5e4fde1dc
Use direct link to adapters snapshots. 2018-04-06 16:23:51 -07:00
Eric Cochran
7d4a10f521
Remove kotlin-reflect dep from integration-test. 2018-04-06 16:17:26 -07:00
Eric Cochran
dbdf48777c Simplify example code. (#483)
* Simplify example code.

This was copied code from a different example that used the delegate annotations.

* Make brackets consistent.
2018-04-05 11:05:40 -07:00
Jesse Wilson
7cab83a8f2
Merge pull request #482 from square/jwilson.0404.nonNull
JsonAdapter.nonNull() forbids explicit nulls in the JSON body
2018-04-05 06:42:19 -07:00
Jesse Wilson
8dd8645b61
Merge pull request #481 from square/eric.fallback-enum-sample
Add example for custom qualifier with an element.
2018-04-05 06:41:29 -07:00
Jesse Wilson
466f77aabe JsonAdapter.nonNull() forbids explicit nulls in the JSON body
This adapter modifier throws exceptions if an unexpected null is
encountered. This may pair nicely with Kotlin.
2018-04-04 23:08:36 -07:00
Eric Cochran
fbe95fe51e Add example for custom qualifier with an element. 2018-04-04 18:41:15 -07:00
Jesse Wilson
ad69a4f495
Merge pull request #477 from square/eric.util
Hide Types.resolve.
2018-04-04 10:26:14 -07:00
Eric Cochran
a931184edf Hide Types.resolve. 2018-04-03 23:35:32 -07:00
Jesse Wilson
c1b93247e3
Merge pull request #475 from square/eric.resolve-generics
Resolve generic property types in KotlinJsonAdapter.
2018-04-04 01:18:31 -04:00
Jesse Wilson
75f2d5c8dd
Merge pull request #474 from hzsweers/z/newmetadata
Update to kotlin-metadata 1.3.0 and just use mavencentral
2018-04-04 01:16:03 -04:00
Jesse Wilson
42f4f956e0
Merge pull request #473 from square/jwilson.0403.qualifiers_names_transient
Handle qualifiers, names, and transient in generated adapters
2018-04-04 00:42:47 -04:00
Eric Cochran
dc450e6192 Resolve generic property types in KotlinJsonAdapter. 2018-04-03 17:37:38 -07:00
Zac Sweers
5f4c46f402 Update to kotlin-metadata 1.3.0 and just use mavencentral 2018-04-03 13:23:38 -07:00
Jesse Wilson
d555d24d94 Handle qualifiers, names, and transient in generated adapters 2018-04-03 11:00:40 -04:00
Zac Sweers
5c45d1e0d9 Generate @Generated annotation onto adapters when possible (#466)
* Generate `@Generated` annotation onto adapters when possible

Part of #461

This leverages AutoCommon's `GeneratedAnnotations#generatedAnnotation` API to generate a `@Generated` annotation where possible. This keeps with conventions in other code gen tools, and also allows for more fine grained proguard rules for keeping generated adapter names, like so:

```proguard
-keepnames @com.squareup.moshi.<wherever this ends up>.MoshiSerializable class *

# Java < 9
-keepnames @javax.annotation.Generated class **JsonAdapter

#  Java 9+
-keepnames @javax.annotation.processing.Generated class **JsonAdapter
```

Generated annotation looks like this:

```kotlin
@Generated(
        value = ["com.squareup.moshi.MoshiKotlinCodeGenProcessor"],
        comments = "https://github.com/square/moshi"
)
```

This doooooes also replace `elements` in `AdapterGenerator` with a `ProcessingEnv`, but I figured that was less evil than polluting it with both `elements` and plumbing down an `env` separately simultaneously. This does also hit a weird ambiguity case due to `KotlinMetadataUtils`' repeat declaration, so a good reason for removing that in the future. Figured it best to punt on a better final place for this to another time.

* Remove names and brackets

* Add moshi.generated option

* Switch back to element property rather than processingEnv

* Fold the kotlin-codegen-runtime into Moshi itself.

Rename @MoshiSerializable to @JsonClass. Like @Json, I'm anticipating
a future where there are other interesting properties on this annotation.
Perhaps a future feature where Moshi is strict and only adapts types that
have a '@JsonClass' annotation.

Also rename MoshiKotlinCodeGenProcessor to JsonClassCodeGenProcessor. We
may later support other ways of generating code here; perhaps for regular
Java types.

* Generate `@Generated` annotation onto adapters when possible

Part of #461

This leverages AutoCommon's `GeneratedAnnotations#generatedAnnotation` API to generate a `@Generated` annotation where possible. This keeps with conventions in other code gen tools, and also allows for more fine grained proguard rules for keeping generated adapter names, like so:

```proguard
-keepnames @com.squareup.moshi.<wherever this ends up>.MoshiSerializable class *

# Java < 9
-keepnames @javax.annotation.Generated class **JsonAdapter

#  Java 9+
-keepnames @javax.annotation.processing.Generated class **JsonAdapter
```

Generated annotation looks like this:

```kotlin
@Generated(
        value = ["com.squareup.moshi.MoshiKotlinCodeGenProcessor"],
        comments = "https://github.com/square/moshi"
)
```

This doooooes also replace `elements` in `AdapterGenerator` with a `ProcessingEnv`, but I figured that was less evil than polluting it with both `elements` and plumbing down an `env` separately simultaneously. This does also hit a weird ambiguity case due to `KotlinMetadataUtils`' repeat declaration, so a good reason for removing that in the future. Figured it best to punt on a better final place for this to another time.

* Fix rebase conflicts and sync with remote
2018-04-03 03:27:57 -04:00
Jesse Wilson
e0d84e1fee
Merge pull request #472 from square/jwilson.0402.beyond_data_classes
Support non-data classes for generated JsonAdapters
2018-04-02 19:38:19 -04:00
Jesse Wilson
b3d7dfd603 Support non-data classes for generated JsonAdapters
This is towards making the reflection and codegen adapters work the same.
The process is relatively straightforward: try to promote all of the tests
in KotlinCodeGenTest to be passing tests in GeneratedAdaptersTest or
compile failures in CompilerTest
2018-04-02 00:37:17 -04:00
Jesse Wilson
7750d179be
Merge pull request #471 from square/jwilson.0331.kompile_testing
Call the kotlin compiler from within a test case.
2018-04-01 05:50:59 -04:00
Jesse Wilson
0c24bd4846 Call the kotlin compiler from within a test case.
This is a fragile first step.
2018-03-31 00:57:50 -04:00
Jesse Wilson
bb7a1c7a27
Merge pull request #464 from square/jwilson.0324.fold_runtime
Fold the kotlin-codegen-runtime into Moshi itself.
2018-03-28 20:59:58 -04:00
Jesse Wilson
982f9c94f6 Fold the kotlin-codegen-runtime into Moshi itself.
Rename @MoshiSerializable to @JsonClass. Like @Json, I'm anticipating
a future where there are other interesting properties on this annotation.
Perhaps a future feature where Moshi is strict and only adapts types that
have a '@JsonClass' annotation.

Also rename MoshiKotlinCodeGenProcessor to JsonClassCodeGenProcessor. We
may later support other ways of generating code here; perhaps for regular
Java types.
2018-03-28 20:26:20 -04:00
Szymon Kozak
351bc57554 Change compile to implementation in README (#467) 2018-03-27 17:43:46 -07:00
Jesse Wilson
d045947ea7
Split the MoshiKotlinCodeGenProcessor into multiple types. (#462)
Move more behavior into the types: Adapter and Property now have many
instance methods and are now mutable value classes rather than immutable
data classes.

This changes how some of the bookkeeping code works. Previously there were
some pretty tricky types: maps with pairs as keys, indexed and non-indexed
lists of properties. With this change more of the logic operates directly
on the properties.
2018-03-24 20:53:08 -04:00
Zac Sweers
96e074d030 Kotlin Code Gen module (#435)
* Add kotlin code gen modules

* Update kotlin to 1.2

* Add a serializable dummy class

* Try using kapt configuration from kotlin-examples repo

Still no luck!

* Use proper allocated name for assignment too

* Use selectName() API

* Clean up constructor parameter annotations & plumbing for qualifiers

* Updates poms and kotlin code gen processor to support tests.

* Ignore kotlin code gen tests for now

None of these are data classes tests right now, which is the only thing this supports right now

* Replace $ with _ in class names for consistency

* Shortcut Array types to arrayOf

* Add DataClassTest

* Try generated option first, fall back to maven after

* More idiomatic handling

* Only use nonnullable types for adapter properties

* Code dump of kotshi tests

* Comment out specifics to get compiling

* Generics support!

* Fix double primitive default

* Pick up temporary snapshot for Any fix

* Invariance should just be null

* Better handling of nullably-bound variance

* Just assume the first jvm constructor for now as jvmMethodSig is flaky

* Specify types param if needed

* Don't do lazy delegation

* Clean up nullable typevariablename boundaries

* Add type variables to extension function on companion object

* Use properties instead of allocated names for more robustness

Since we're already on a snapshot

* If there are no type variables, make it null for simpler handling

* Fix generics and Type[] handling

* Fix unnecessary as casts on primitive defaults

* Reference spec directly for possible bangs

* Use nullSafe() adapters for anything nullable or with default values

* Use object type in makeType()

Types.java cares

* Make TestPrimitiveDefaultValues work

* Re-enable TestClassWithJavaKeyword

* Ignore remaining tests that are pending decisions or JsonQualifier support

* Remove customnames test as we're just going to stick with simple @Json

* Add toString() implementations

* Reenable default values testing, adapt to kotlin lang support

* Remove primitive adapters bits since we're not using it

* Clean up a bunch of leftover comments

* Switch to only nullable handling, report missing properties

This makes all nullable handling for local properties the same, and removes defaults for primitives in the process. It simplifies the handling a lot, and leans on kotlin language features to take care of null handling (null checking and then throwing the lazily evaluated list of missing properties).

One minor change from what kotshi does - this reports the serialized name in the missing properties, not the property name. We could look at supporting this though if we want.

* Implement JsonQualifier support

* Use Kapt for AutoService/processor declaration

* Checkstyle

* Remove unused primite type checks

* Add test verifying mutable and immutable collections work

* Fix test name

* Standardize isRequired checks

* Add more nullability and mutability tests

* Kotlinpoet 0.7.0 final

* Switch to new vararg overload for annotation class adapter()

* Make suffix just JsonAdapter without underscore

* Switch to just a regular constructor for MoshiSerializableFactory

* Remove constructor caching

* Remove unnecessary framework class checks

* Nix unnecessary superclass lookups, inline constructor lookup

* Nix null token check in reads

* Nix null check in writes, do !! on first value use

* Nix null checks in favor of serializeNulls

* Inline null checks and fail eagerly

* Fix double _Adapter

* First pass at simplifying adapter names

* Inline names to options property, life into class and rm companion

* Differentiate between absent and null, use nullSafe() as needed

* Group together compile and test dependencies

* Remove incorrect comment

* Revert formatting

* Set, not mutable set

* Collapse else-if nesting to one when

* Cleaner formatting test code

* Collapse more to locals

* Collapse more

* Return a nonnullable type in fromJson

* Remove redundant out variance

* Use KClass where appropriate

* End comment in period

* Remove redundant comment

* Throw on unrecognized type in simplified name

* Use illegalargumentexception instead

* Emit a nullcheck at the beginning of toJson instead

* Remove extra newline

* Simplify processing to be less abusive

* Skip using asClassName() when possible

* Use addComment()

* Switch to declared constructors

Technically more correct since we're defining these

* Unmodifiable set

* return adapter(type, annotationTypes[0])

* Slight optimization - check if the type is parameterized first

If the type is a parameterized type, then we know they'll have the two-arg constructor. This way we don't always try and fail the single arg constructor on parameterized types

* Add test for type aliases, optimize to reuse adapters if possible

This is a tiny optimization to make type aliases (which did already work) reuse adapter properties if they already exist for the backing type. What this means is that if you have:

typealias Foo = String

and properties
foo: Foo
bar: String

you'll only get one adapter property field for String, and both will use it

* Use string templating where possible

* Remove all the kotshi tests
2018-03-11 21:17:55 -04:00
Eric Cochran
e6c2ebedde Disallow null annotation set in adapter lookup. (#460) 2018-03-10 06:42:07 -05:00
Ekalips
ce879634cc Add single quotes to variable names (#452) (#458) 2018-02-27 09:12:43 -05:00
Jake Wharton
dfc075515d
Merge pull request #451 from square/jwilson.0225.https_links
Use HTTPS links in documentation where possible.
2018-02-25 22:20:02 -05:00
Jesse Wilson
caedfea74b Use HTTPS links in documentation where possible. 2018-02-25 22:09:56 -05:00
Eric Cochran
8e28dd4ad7 Fail earlier for some incorrect owner types. (#450) 2018-02-24 04:46:42 -05:00
Eric Cochran
ed1ea5a755 JsonAdapter.fromJson(String) must fully consume. (#441)
* JsonAdapter.fromJson(String) must fully consume.

* Replace field with method.
2018-02-21 20:15:38 -05:00
Eric Cochran
aede26d5e1 Clarify negation in condition. (#444) 2018-02-18 06:56:29 -05:00
Eric Cochran
3b89cf1fcb Fix ClassJsonAdapter to handle ParameterizedTypes. (#422) 2018-02-17 22:56:05 -05:00
Eric Cochran
834a401122 Crash earlier for property type conflicts. (#377) 2018-02-17 22:51:02 -05:00
Robert Stoll
a00860ee1d mention that KotlinJsonAdapterFactory validates (#439)
See #438 for the use case. The documentation was not accurate enough IMO. Thus, mention that KotlinJsonAdapterFactory is required for validation.
2018-02-17 22:17:42 -05:00
Jake Wharton
a6d31ba0b4
Merge pull request #440 from square/jakew/tiny-tweak/2018-02-13
Reduce visual complexity of branching.
2018-02-13 16:35:23 -05:00
Jake Wharton
bc7d849362 Reduce visual complexity of branching. 2018-02-13 15:04:34 -05:00
Jake Wharton
b131d3bba0
Merge pull request #434 from square/jwilson.0207.modulename
Move modules into their own packages.
2018-02-07 09:25:18 -05:00
Jesse Wilson
5ad9d31bd8 Move modules into their own packages.
This sets the Automatic-Module-Name for moshi, moshi-adapters, and moshi-kotlin.
It moves moshi-adapters into its own .adapters package and forwards the existing
adapter. It moves the moshi-kotlin into its own .kotlin package and forwards the
existing adapter.

I'm not certain this is necessary or sufficient, but I think it's the right idea
for JPMS compatibility.
2018-02-07 04:41:06 -05:00
Eric Cochran
d26b2a151f Fix error message for invalid toJson signature. (#431)
void toJson(JsonWriter, JsonAdapter) does not make sense. This looks like my copy-paste error.
2018-02-03 23:50:22 -05:00
Eric Cochran
5b194964a9 Disallow irregular Kotlin classes. (#424) 2018-01-10 22:07:13 -05:00
Eric Cochran
9deeb62e77 Add permalink to ISO8601Utils.java. (#425)
This file was removed from master a few months ago.
2018-01-10 21:57:00 -05:00
Eric Cochran
dba2f05b13 Improve error message for local classes. (#423) 2018-01-10 21:56:37 -05:00
Jesse Wilson
5d12c22f44
ByteStrings example. (#419)
Closes: https://github.com/square/moshi/issues/31
2018-01-07 23:04:25 -05:00
Jesse Wilson
7205690bf5
Add a standard example for JSON adapter factories. (#420)
Closes: https://github.com/square/moshi/issues/136
2018-01-07 23:04:07 -05:00
Jesse Wilson
359244e996
Fix JsonValueReader to support up to 255 levels of nesting. (#417)
Follow up to https://github.com/square/moshi/pull/349
2018-01-07 14:55:02 -05:00
Eric Cochran
d2ef4b5a61 Clarify error for non-null Kotlin properties. (#376)
Instead of throwing an InvocationTargetException.
2018-01-07 14:07:56 -05:00
Eric Cochran
a210d89a55 Don't handle WildcardTypes in ClassJsonAdapter. (#406) 2018-01-07 14:05:49 -05:00
Jesse Wilson
0a6e836762
Support up to 255 levels of nesting. (#349)
Closes: https://github.com/square/moshi/issues/348
2018-01-07 12:17:00 -05:00
Jake Wharton
8cde0e5d72
Merge pull request #413 from Egorand/egorand/raw-string
Use raw string literals to improve test data readability
2017-12-15 12:53:35 -05:00
Egor Andreevici
07f5d708dd Use raw string literals to improve test data readability 2017-12-15 14:01:45 +02:00
Eric Cochran
f53a77d311
Fix doc reference to Token. (#403)
JsonReader.Token is less redundant than Gson's JsonReader.JsonToken.
2017-12-01 12:07:26 -08:00
Eric Cochran
a8b1550e7e
Make selectString consistent across JsonReaders. (#399)
Make JsonValueReader.selectString return -1 for non-strings instead of throwing.
2017-11-27 15:33:03 -08:00
Jake Wharton
20ffd22110
Merge pull request #398 from sullis/kotlin-1.1.60
kotlin 1.1.60
2017-11-24 16:39:28 -05:00
Sean Sullivan
b06f65d2e9 kotlin 1.1.60 2017-11-24 16:27:37 -05:00
Eric Cochran
f922371fa8 Let JsonValueReader.nextString read numbers. (#390)
* Let JsonValueReader.nextString read numbers.

This adds parity with JsonUtf8Reader and lets big number literals in JSON be read in as strings in Java.

* Remove trailing 0 in float literal.
2017-11-24 07:23:12 -05:00
Jesse Wilson
03323ae998
Merge pull request #392 from square/eric.primitives
Add error message for accidental primitive usage.
2017-11-21 19:06:13 -05:00
Eric Cochran
2b7e5a3453 Add error message for accidental primitive usage.
Also, add a test for the requirement.
2017-11-11 18:37:04 -08:00
Eric Cochran
b7a91e0557 Update to Error Prone 2.1.2. (#384) 2017-11-06 11:49:22 -08:00
Eric Cochran
fd5c5ee2df
Fix a typo in the changelog. (#386) 2017-11-05 13:57:59 -08:00
Jake Wharton
d6742be404
Merge pull request #383 from square/eric.20171104.checkreturnvalue
Add @CheckReturnValue to appropriate public APIs.
2017-11-05 08:51:57 -08:00
Jesse Wilson
5812c994b1
Merge pull request #385 from square/eric.20171105.transient_constructor_error
Clarify error message for transient parameters.
2017-11-05 11:16:54 -05:00
Eric Cochran
b583adac37 Clarify error message for transient parameters.
Otherwise, the error message is misleading: "No property for required constructor $parameter"
2017-11-05 01:06:54 -08:00
Eric Cochran
e643a04ee5 Add @CheckReturnValue to appropriate public APIs. 2017-11-04 21:13:07 -07:00
Jesse Wilson
4ac0d6f5ef
Merge pull request #375 from square/eric.kotlin_readme
Document adding the KotlinJsonAdapterFactory.
2017-10-29 09:11:09 -04:00
Eric Cochran
88ec00bcf4
Document adding the KotlinJsonAdapterFactory.
If you add a custom Kotlin JsonAdapter factory after the KotlinJsonAdapterFactory, you're going to have a bad time.
2017-10-29 05:38:48 +00:00
Jake Wharton
f06b43b2a4
Merge pull request #374 from square/eric.cleanup_33
Update to Kotlin 1.1.51.
2017-10-28 10:13:54 +01:00
Eric Cochran
e0ad48cd97 Update to Kotlin 1.1.51. 2017-10-28 10:07:05 +01:00
Jesse Wilson
8bf298ac14 Merge pull request #360 from square/eric.20171004.delegate-adapters
Allow delegates for intermediates in adapters.
2017-10-04 21:53:12 -04:00
Eric Cochran
4376a50f1f Clarify delegation with qualifiers test. (#359) 2017-10-04 16:49:23 -07:00
Eric Cochran
f847d47daa Remove Types.equal. (#358)
Nobody uses this helper method.
2017-10-04 15:54:17 -07:00
Eric Cochran
de336ef86e Allow delegates for intermediates in adapters. 2017-10-04 15:53:33 -07:00
Jake Wharton
224369155e Merge pull request #355 from square/eric.20170925.proguard
Update ProGuard config for CheckReturnValue.
2017-09-25 15:51:55 -04:00
Eric Cochran
165e3628be Update ProGuard config for CheckReturnValue. 2017-09-25 15:03:36 -04:00
Jake Wharton
22c3b02b3f Merge pull request #354 from square/eric.20170925.checkreturnvalue
Add CheckReturnValue for toJson's string result.
2017-09-25 14:45:48 -04:00
Eric Cochran
2db89355f1 Add CheckReturnValue for toJson's string result.
This helps Error Prone and the IDE find accidental usages of toJson(value) instead of toJson(writer, value).
2017-09-25 14:18:31 -04:00
Jesse Wilson
8dfe9edc00 Merge pull request #352 from emmaguy/emmaguy/add-recipe-type-adapter-delegate
Add an example custom Adapter which delegates
2017-09-25 13:31:20 -04:00
Jesse Wilson
f5ceb91e0f Merge pull request #353 from square/eric.20170925.unwrap-write
Fix not writing value to JsonWriter in example.
2017-09-25 13:30:32 -04:00
Eric Cochran
eed3295495 Fix not writing value to JsonWriter in example. 2017-09-25 13:02:36 -04:00
Emma Guy
816f6f81c6 Add an example custom adapter which delegates 2017-09-24 13:40:01 +01:00
John Carlson
e8a2596841 Add ProGuard rule for JsonQualifier (#342) 2017-08-04 18:15:11 -04:00
Jake Wharton
da1ed8f5c3 Merge pull request #339 from oldergod/patch-1
Fix builder calls to new API
2017-07-31 23:59:33 -04:00
Benoît Quenaudon
5125fc2f27 Fix builder calls to new API
Update the example with the right API https://github.com/square/moshi/blob/master/moshi/src/main/java/com/squareup/moshi/Moshi.java#L155
2017-08-01 12:58:20 +09:00
Jesse Wilson
b4ad3b9789 Merge pull request #336 from Jawnnypoo/patch-1
Add moshi-kotlin documentation
2017-07-31 21:56:17 -04:00
Jesse Wilson
f12031ecba Merge pull request #337 from Jawnnypoo/patch-2
Document `moshi-adapters` artifact
2017-07-27 00:04:05 -04:00
John Carlson
76df51bfde Correction to docs of Rfc3339DateJsonAdapter 2017-07-26 14:49:46 -05:00
John Carlson
54c026f5db Document moshi-adapters artifact 2017-07-26 14:35:38 -05:00
John Carlson
ab5b3a468e Add moshi-kotlin documentation 2017-07-26 12:42:02 -05:00
Jake Wharton
c755894af3 Merge pull request #335 from square/eric.0724.null_kotlin
JsonAdapter.Factory.create rejects the null Type.
2017-07-24 20:57:34 +01:00
Eric Cochran
1340ef8935 JsonAdapter.Factory.create rejects the null Type.
This fails already in Types.getRawType, but a compiler error is preferable.
2017-07-24 11:06:22 -07:00
Jesse Wilson
fdd38cddd8 Merge pull request #333 from square/eric.0719.nonnull_annotations
Fail earlier with null annotation set.
2017-07-19 15:07:41 -04:00
Eric Cochran
00694e9878 Fail earlier with null annotation set. 2017-07-19 11:29:06 -07:00
Jesse Wilson
76f50df2cc Merge pull request #327 from square/eric.0620.date-cause
Add cause to malformed date string exception.
2017-06-20 18:14:17 -04:00
Eric Cochran
b7f771a70f Add cause to malformed date string exception. 2017-06-20 00:15:11 -07:00
Jesse Wilson
3c225fcad7 Merge pull request #321 from OleksandrKucherenko/patch-1
proguard config updates
2017-06-18 13:40:14 -04:00
Jake Wharton
f89544fd70 Merge pull request #324 from square/jwilson.0610.test_for_323
Add a test to demonstrate the 32-parameter limit
2017-06-10 21:28:35 -04:00
jwilson
d3926a7f86 Add a test to demonstrate the 32-parameter limit 2017-06-10 21:07:01 -04:00
Oleksandr
bcb150eb06 proguard config updates
updated proguard configuration for custom Json Adapters @FromJson/@ToJson cases
2017-06-09 14:18:38 +02:00
Jesse Wilson
aee3216ca1 Merge pull request #316 from square/jwilson.0526.delegate_core_adapter
Change the adapter for Object.class to delegate.
2017-05-27 06:09:42 -04:00
jwilson
9e9655b556 Change the adapter for Object.class to delegate.
Previously if we ever had an opaque Object, the content of this object
would always only use the built-in adapters for its members.

This changes the built-in Object adapter to do one layer of type checking
and then to delegate to user-supplied adapters.

The big upside of this is that application code can now change the default
numeric type to use when decoding an untyped object. Typically this will
be used to replace our default of Double with a user-specified numeric type
like BigDecimal.
2017-05-26 23:44:46 -04:00
Jake Wharton
cda6bb9f14 Merge pull request #312 from oldergod/patch-1
FIX TYPO: Voila -> Voilà
2017-05-21 22:07:11 -04:00
Benoît Quenaudon
c4436cef80 FIX TYPO: Voila -> Voilà 2017-05-22 11:01:32 +09:00
jwilson
798f14bda5 [maven-release-plugin] prepare for next development iteration 2017-05-14 22:20:00 -04:00
jwilson
f42ae45f4c [maven-release-plugin] prepare release moshi-parent-1.5.0 2017-05-14 22:19:55 -04:00
Jesse Wilson
77a1f388db Fix Javadoc builds with the jsr305 dependency. (#306)
Because multiple modules share a package we need to share the dependency on
everything in package-info.java.
2017-05-14 22:18:35 -04:00
jwilson
4050e45e82 Update changelog for 1.5 2017-05-14 22:02:51 -04:00
Jake Wharton
5031a313b7 Merge pull request #305 from square/jwilson.0514.okio113
Upgrade to Okio 1.13.
2017-05-14 19:00:58 -07:00
jwilson
c8d8ee1fff Upgrade to Okio 1.13. 2017-05-14 21:58:48 -04:00
Jake Wharton
7d0c952102 Merge pull request #304 from square/jwilson.0514.factory_class
Use a class for KotlinJsonAdapterFactory.
2017-05-14 20:06:33 -04:00
jwilson
7d5c4adc8d Use a class for KotlinJsonAdapterFactory. 2017-05-14 20:01:26 -04:00
Niklas Baudy
10c77d7979 Fix typo in Changelog. (#302) 2017-05-10 07:19:34 -04:00
Jake Wharton
de4c2e782e Merge pull request #300 from square/jwilson.0507.synthetic_properties
Support more kinds of properties in KotlinJsonAdapter
2017-05-07 17:31:21 -04:00
jwilson
494992dab8 Support more kinds of properties in KotlinJsonAdapter
This makes it possible to have synthetic properties that have no
state.

Also test properties that are synthetic and have no backing field.

Closes: https://github.com/square/moshi/issues/299
2017-05-07 16:18:12 -04:00
Eric Cochran
e59dbf4f96 Add @Nullable to result of Types.nextAnnotations. (#298) 2017-05-06 21:07:59 -04:00
Jesse Wilson
c65b3bf1cb Import jsr305 and use it to mark @Nullable stuff. (#297) 2017-05-06 20:31:24 -04:00
Jake Wharton
dac5f695b3 Merge pull request #296 from square/jwilson.0506.checkstyle_77
Enable Checkstyle 7.7.
2017-05-06 12:13:00 -07:00
jwilson
0ea1959b7e Enable Checkstyle 7.7. 2017-05-06 14:48:55 -04:00
Eric Cochran
13fd0b252c Throw NPE for null indent string in factory method. (#289)
Fail when creating the JsonAdapter rather than when using it.
2017-04-29 21:46:08 -04:00
Serj Lotutovici
f942e0fd52 Make Types.equals(Type, Type) public. (#292) 2017-04-29 21:45:06 -04:00
Jake Wharton
bcec358554 Merge pull request #286 from square/jwilson.0421.enums
KotlinJsonAdapter shouldn't convert enums.
2017-04-21 10:05:10 -05:00
jwilson
cd1542363d KotlinJsonAdapter shouldn't convert enums.
Closes: https://github.com/square/moshi/issues/284
2017-04-21 09:54:45 -05:00
Eric Cochran
d95dd07c56 Fix Types.equals for arrays. (#279) 2017-04-20 17:27:47 -05:00
Christian Brüggemann
e76110b4b1 Fix Factory visibility (#282)
* Fix Factory visibility

* Remove redundant constructor keyword
2017-04-20 17:26:57 -05:00
Eric Cochran
448a2d3298 Treat negative zero as a number, not a long. (#285)
Updates logic from https://github.com/google/gson/issues/1053
2017-04-20 17:22:06 -05:00
Jake Wharton
b4c43ae771 Merge pull request #283 from square/jwilson.0420.more_kotlin_stuff
Handle nulls symetrically in KotlinJsonAdapter.
2017-04-20 11:08:21 -05:00
jwilson
6112993919 Handle nulls symetrically in KotlinJsonAdapter.
When writing nulls we omit them, and when a value is omitted we assume
it is null.
2017-04-20 08:38:18 -05:00
Jesse Wilson
81bbe870f1 KotlinJsonAdapter (#281)
* Add kotlin-module with support for Kotlin data classes

* Naming and style changes to KotlinJsonAdapter.

Biggest changes:

 * Attempt to support regular classes and data classes
 * Avoid parameter hashing when indexing is sufficient for
   constructor parameters
2017-04-18 23:51:37 -04:00
Jesse Wilson
8c18caf574 Add a test to confirm types are canonicalized. (#278)
Obsoletes https://github.com/square/moshi/pull/129/files
2017-04-16 12:15:53 -04:00
Eric Cochran
11dbc3c50b Fix @ToJson IAE message. (#275) 2017-03-28 05:57:53 -04:00
Eric Cochran
718f832864 Allow easy delegates in adapter methods. (#272) 2017-03-27 21:48:19 -04:00
Eric Cochran
1b634bbb74 Update adapter methods ISE message. (#273) 2017-03-27 19:25:31 -04:00
Jake Wharton
05b0a46961 Add error-prone compiler. (#259)
* Fix error-prone warning

* Add error-prone compiler.

* Suppress warning about calling getClass() on annotation.
2017-02-14 22:28:24 -05:00
Jesse Wilson
b6ebe53ffb Merge pull request #257 from square/eric/readme-1.4.0
Fix README version number
2017-02-05 17:49:23 -05:00
Eric Cochran
1e37d8dc42 Fix README version number 2017-02-05 14:38:05 -08:00
Jesse Wilson
af09de142d Merge pull request #254 from square/eric/readme
Remove package API from README
2017-02-04 19:40:01 -05:00
Eric Cochran
0a70c5c4ab Remove package API from README
Types.createJsonQualifierImplementation() is not a public API.
2017-02-04 15:39:35 -08:00
jwilson
49092ece96 [maven-release-plugin] prepare for next development iteration 2017-02-04 15:20:11 -05:00
112 changed files with 10964 additions and 962 deletions

View file

@ -3,10 +3,10 @@
# Deploy a jar, source jar, and javadoc jar to Sonatype's snapshot repo.
#
# Adapted from https://coderwall.com/p/9b_lfq and
# http://benlimmer.com/2013/12/26/automatically-publish-javadoc-to-gh-pages-with-travis-ci/
# https://benlimmer.com/2013/12/26/automatically-publish-javadoc-to-gh-pages-with-travis-ci/
SLUG="square/moshi"
JDK="oraclejdk8"
JDK="openjdk8"
BRANCH="master"
set -e

1
.gitignore vendored
View file

@ -12,6 +12,7 @@ lib
target
pom.xml.*
release.properties
dependency-reduced-pom.xml
.idea
*.iml

View file

@ -1,8 +1,7 @@
language: java
jdk:
- oraclejdk7
- oraclejdk8
- openjdk8
after_success:
- .buildscript/deploy_snapshot.sh
@ -19,8 +18,6 @@ branches:
notifications:
email: false
sudo: false
cache:
directories:
- $HOME/.m2

View file

@ -1,6 +1,160 @@
Change Log
==========
## Version 1.8.0
_2018-11-09_
* New: Support JSON objects that include type information in the JSON. The new
`PolymorphicJsonAdapterFactory` writes a type field when encoding, and reads it when decoding.
* New: Fall back to the reflection-based `KotlinJsonAdapterFactory` if it is enabled and a
generated adapter is not found. This makes it possible to use reflection-based JSON adapters in
development (so you don't have to wait for code to be regenerated on every build) and generated
JSON adapters in production (so you don't need the kotlin-reflect library).
* New: The `peekJson()` method on `JsonReader` let you read ahead on a JSON stream without
consuming it. This builds on Okio's new `Buffer.peek()` API.
* New: The `beginFlatten()` and `endFlatten()` methods on `JsonWriter` suppress unwanted nesting
when composing adapters. Previously it was necessary to flatten objects in memory before writing.
* New: Upgrade to Okio 1.16.0. We don't yet require Kotlin-friendly Okio 2.1 but Moshi works fine
with that release.
```kotlin
implementation("com.squareup.okio:okio:1.16.0")
```
* Fix: Don't return partially-constructed adapters when using a Moshi instance concurrently.
* Fix: Eliminate warnings and errors in generated `.kt` triggered by type variance, primitive
types, and required values.
* Fix: Improve the supplied rules (`moshi.pro`) to better retain symbols used by Moshi. We
recommend R8 when shrinking code.
* Fix: Remove code generation companion objects. This API was neither complete nor necessary.
## Version 1.7.0
_2018-09-24_
* New: `EnumJsonAdapter` makes it easy to specify a fallback value for unknown enum constants.
By default Moshi throws an `JsonDataException` if it reads an unknown enum constant. With this
you can specify a fallback value or null.
```java
new Moshi.Builder()
.add(EnumJsonAdapter.create(IsoCurrency.class)
.withUnknownFallback(IsoCurrency.USD))
.build();
```
Note that this adapter is in the optional `moshi-adapters` module.
```groovy
implementation 'com.squareup.moshi:moshi-adapters:1.7.0'
```
* New: Embed R8/ProGuard rules in the `.jar` file.
* New: Use `@CheckReturnValue` in more places. We hope this will encourage you to use `skipName()`
instead of `nextName()` for better performance!
* New: Forbid automatic encoding of platform classes in `androidx`. As with `java.*`, `android.*`,
and `kotlin.*` Moshi wants you to specify how to encode platform types.
* New: Improve error reporting when creating an adapter fails.
* New: Upgrade to Okio 1.15.0. We don't yet require Kotlin-friendly Okio 2.x but Moshi works fine
with that release.
```groovy
implementation 'com.squareup.okio:okio:1.15.0'
```
* Fix: Return false from `JsonReader.hasNext()` at document's end.
* Fix: Improve code gen to handle several broken cases. Our generated adapters had problems with
nulls, nested parameterized types, private transient properties, generic type aliases, fields
with dollar signs in their names, and named companion objects.
## Version 1.6.0
_2018-05-14_
* **Moshi now supports codegen for Kotlin.** We've added a new annotation processor that generates
a small and fast JSON adapter for your Kotlin types. It can be used on its own or with the
existing `KotlinJsonAdapterFactory` adapter.
* **Moshi now resolves all type parameters.** Previously Moshi wouldn't resolve type parameters on
top-level classes.
* New: Support up to 255 levels of nesting when reading and writing JSON. Previously Moshi would
reject JSON input that contained more than 32 levels of nesting.
* New: Write encoded JSON to a stream with `JsonWriter.value(BufferedSource)`. Use this to emit a
JSON value without decoding it first.
* New: `JsonAdapter.nonNull()` returns a new JSON adapter that forbids explicit nulls in the JSON
body. Use this to detect and fail eagerly on unwanted nulls.
* New: `JsonReader.skipName()` is like `nextName()` but it avoids allocating when a name is
unknown. Use this when `JsonReader.selectName()` returns -1.
* New: Automatic module name of `com.squareup.moshi` for use with the Java Platform Module System.
This moves moshi-adapters into its own `.adapters` package and forwards the existing adapter. It
moves the moshi-kotlin into its own `.kotlin.reflect` package and forwards the existing adapter.
* New: Upgrade to Okio 1.14.0.
```xml
<dependency>
<groupId>com.squareup.okio</groupId>
<artifactId>okio</artifactId>
<version>1.14.0</version>
</dependency>
com.squareup.okio:okio:1.14.0
```
* Fix: Fail fast if there are trailing non-whitespace characters in the JSON passed to
`JsonAdapter.fromJson(String)`. Previously such data was ignored!
* Fix: Fail fast when Kotlin types are abstract, inner, or object instances.
* Fix: Fail fast if `name()` is called out of sequence.
* Fix: Handle asymmetric `Type.equals()` methods when doing type comparisons. Previously it was
possible that a registered type adapter would not be used because its `Type.equals()` method was
not consistent with a user-provided type.
* Fix: `JsonValueReader.selectString()` now returns -1 for non-strings instead of throwing.
* Fix: Permit reading numbers as strings when the `JsonReader` was created from a JSON value. This
was always supported when reading from a stream but broken when reading from a decoded value.
* Fix: Delegate to user-adapters in the adapter for Object.class. Previously when Moshi encountered
an opaque Object it would only use the built-in adapters. With this change user-installed
adapters for types like `String` will always be honored.
## Version 1.5.0
_2017-05-14_
* **Moshi now uses `@Nullable` to annotate all possibly-null values.** We've added a compile-time
dependency on the JSR 305 annotations. This is a [provided][maven_provided] dependency and does
not need to be included in your build configuration, `.jar` file, or `.apk`. We use
`@ParametersAreNonnullByDefault` and all parameters and return types are never null unless
explicitly annotated `@Nullable`.
* **Warning: Moshi APIs in this update are source-incompatible for Kotlin callers.** Nullability
was previously ambiguous and lenient but now the compiler will enforce strict null checks.
* **Kotlin models are now supported via the `moshi-kotlin` extension.** `KotlinJsonAdapterFactory`
is the best way to use Kotlin with Moshi. It honors default values and is null-safe. Kotlin
users that don't use this factory should write custom adapters for their JSON types. Otherwise
Moshi cannot properly initialize delegated properties of the objects it decodes.
* New: Upgrade to Okio 1.13.0.
```xml
<dependency>
<groupId>com.squareup.okio</groupId>
<artifactId>okio</artifactId>
<version>1.13.0</version>
</dependency>
com.squareup.okio:okio:1.13.0
```
* New: You may now declare delegates in `@ToJson` and `@FromJson` methods. If one of the arguments
to the method is a `JsonAdapter` of the same type, that will be the next eligible adapter for
that type. This may be useful for composing adapters.
* New: `Types.equals(Type, Type)` makes it easier to compare types in `JsonAdapter.Factory`.
* Fix: Retain the sign on negative zero.
## Version 1.4.0
_2017-02-04_
@ -33,8 +187,8 @@ your application classes.
format of the encoded JSON.
* New: `JsonReader.selectName()` and `selectString()` optimize decoding JSON with known names and
values.
* New: `Types.nextAnnotations()` and `Types.createJsonQualifierImplementation()` reduce the amount
of code required to implement a custom `JsonAdapter.Factory`.
* New: `Types.nextAnnotations()` reduces the amount of code required to implement a custom
`JsonAdapter.Factory`.
* Fix: Don't fail on large longs that have a fractional component like `9223372036854775806.0`.
## Version 1.3.1
@ -72,7 +226,7 @@ _2016-10-15_
`StackOverflowError` due to excessive recursion.
* Fix: Require enclosed types to specify their enclosing type with
`Types.newParameterizedTypeWithOwner()`. Previously this API did not exist and looking up
adapters for enclosed parameterized types as not possible.
adapters for enclosed parameterized types was not possible.
* Fix: Fail on invalid escapes. Previously any character could be escaped. With this fix only
characters permitted to be escaped may be escaped. Use `JsonReader.setLenient(true)` to read
JSON documents that escape characters that should not be escaped.
@ -134,7 +288,7 @@ _2015-09-27_
code should keep a reference to required adapters in a field.
* New: The `Types` factory class makes it possible to compose types like `List<Card>` or
`Map<String, Integer>`. This is useful to look up JSON adapters for parameterized types.
* New: `JsonAdapter.failOnUnknown()` returns a new JSON adapter that throws if an unknonw value is
* New: `JsonAdapter.failOnUnknown()` returns a new JSON adapter that throws if an unknown value is
encountered on the stream. Use this in development and debug builds to detect typos in field
names. This feature shouldnt be used in production because it makes migrations very difficult.
@ -158,3 +312,4 @@ _2015-06-16_
[rfc_7159]: https://tools.ietf.org/html/rfc7159
[gson]: https://github.com/google/gson
[jackson]: http://wiki.fasterxml.com/JacksonHome
[maven_provided]: https://maven.apache.org/guides/introduction/introduction-to-dependency-mechanism.html

159
README.md
View file

@ -120,7 +120,7 @@ Moshi moshi = new Moshi.Builder()
.build();
```
Voila:
Voilà:
```json
{
@ -213,20 +213,53 @@ JsonAdapter<Event> jsonAdapter = moshi.adapter(Event.class);
Event event = jsonAdapter.fromJson(json);
```
### Adapter convenience methods
Moshi provides a number of convenience methods for `JsonAdapter` objects:
- `nullSafe()`
- `nonNull()`
- `lenient()`
- `failOnUnknown()`
- `indent()`
- `serializeNulls()`
These factory methods wrap an existing `JsonAdapter` into additional functionality.
For example, if you have an adapter that doesn't support nullable values, you can use `nullSafe()` to make it null safe:
```java
String dateJson = "\"2018-11-26T11:04:19.342668Z\"";
String nullDateJson = "null";
// RFC 3339 date adapter, doesn't support null by default
// See also: https://github.com/square/moshi/tree/master/adapters
JsonAdapter<Date> adapter = new Rfc3339DateJsonAdapter();
Date date = adapter.fromJson(dateJson);
System.out.println(date); // Mon Nov 26 12:04:19 CET 2018
Date nullDate = adapter.fromJson(nullDateJson);
// Exception, com.squareup.moshi.JsonDataException: Expected a string but was NULL at path $
Date nullDate = adapter.nullSafe().fromJson(nullDateJson);
System.out.println(nullDate); // null
```
In contrast to `nullSafe()` there is `nonNull()` to make an adapter refuse null values. Refer to the Moshi JavaDoc for details on the various methods.
### Parse JSON Arrays
Say we have a JSON string of this structure:
```java
```json
[
{
"rank": "4",
"suit": "CLUBS"
},
{
"rank": "A",
"suit": "HEARTS"
}
{
"rank": "4",
"suit": "CLUBS"
},
{
"rank": "A",
"suit": "HEARTS"
}
]
```
@ -472,26 +505,121 @@ public final class BlackjackHand {
}
```
Kotlin
------
Moshi is a great JSON library for Kotlin. It understands Kotlins non-nullable types and default
parameter values. When you use Kotlin with Moshi you may use reflection, codegen, or both.
#### Reflection
The reflection adapter uses Kotlins reflection library to convert your Kotlin classes to and from
JSON. Enable it by adding the `KotlinJsonAdapterFactory` to your `Moshi.Builder`:
```kotlin
val moshi = Moshi.Builder()
// ... add your own JsonAdapters and factories ...
.add(KotlinJsonAdapterFactory())
.build()
```
Moshis adapters are ordered by precedence, so you always want to add the Kotlin adapter after your
own custom adapters. Otherwise the `KotlinJsonAdapterFactory` will take precedence and your custom
adapters will not be called.
The reflection adapter requires the following additional dependency:
```xml
<dependency>
<groupId>com.squareup.moshi</groupId>
<artifactId>moshi-kotlin</artifactId>
<version>1.8.0</version>
</dependency>
```
```kotlin
implementation("com.squareup.moshi:moshi-kotlin:1.8.0")
```
Note that the reflection adapter transitively depends on the `kotlin-reflect` library which is a
2.5 MiB .jar file.
#### Codegen
Moshis Kotlin codegen support is an annotation processor. It generates a small and fast adapter for
each of your Kotlin classes at compile time. Enable it by annotating each class that you want to
encode as JSON:
```kotlin
@JsonClass(generateAdapter = true)
data class BlackjackHand(
val hidden_card: Card,
val visible_cards: List<Card>
)
```
The codegen adapter requires that your Kotlin types and their properties be either `internal` or
`public` (this is Kotlins default visibility).
Kotlin codegen has no additional runtime dependency. Youll need to [enable kapt][kapt] and then
add the following to your build to enable the annotation processor:
```xml
<dependency>
<groupId>com.squareup.moshi</groupId>
<artifactId>moshi-kotlin-codegen</artifactId>
<version>1.8.0</version>
<scope>provided</scope>
</dependency>
```
```kotlin
kapt("com.squareup.moshi:moshi-kotlin-codegen:1.8.0")
```
You must also have the `kotlin-stdlib` dependency on the classpath during compilation in order for
the compiled code to have the required metadata annotations that Moshi's processor looks for.
#### Limitations
If your Kotlin class has a superclass, it must also be a Kotlin class. Neither reflection or codegen
support Kotlin types with Java supertypes or Java types with Kotlin supertypes. If you need to
convert such classes to JSON you must create a custom type adapter.
The JSON encoding of Kotlin types is the same whether using reflection or codegen. Prefer codegen
for better performance and to avoid the `kotlin-reflect` dependency; prefer reflection to convert
both private and protected properties. If you have configured both, generated adapters will be used
on types that are annotated `@JsonClass(generateAdapter = true)`.
Download
--------
Download [the latest JAR][dl] or depend via Maven:
```xml
<dependency>
<groupId>com.squareup.moshi</groupId>
<artifactId>moshi</artifactId>
<version>1.3.1</version>
<version>1.8.0</version>
</dependency>
```
or Gradle:
```groovy
compile 'com.squareup.moshi:moshi:1.3.1'
```kotlin
implementation("com.squareup.moshi:moshi:1.8.0")
```
Snapshots of the development version are available in [Sonatype's `snapshots` repository][snap].
R8 / ProGuard
--------
If you are using R8 or ProGuard add the options from [this file](https://github.com/square/moshi/blob/master/moshi/src/main/resources/META-INF/proguard/moshi.pro). If using Android, this requires Android Gradle Plugin 3.2.0+.
The `moshi-kotlin` artifact additionally requires the options from [this file](https://github.com/square/moshi/blob/master/kotlin/reflect/src/main/resources/META-INF/proguard/moshi-kotlin.pro)
You might also need rules for Okio which is a dependency of this library.
License
--------
@ -510,9 +638,10 @@ License
limitations under the License.
[dl]: https://search.maven.org/remote_content?g=com.squareup.moshi&a=moshi&v=LATEST
[dl]: https://search.maven.org/classic/remote_content?g=com.squareup.moshi&a=moshi&v=LATEST
[snap]: https://oss.sonatype.org/content/repositories/snapshots/com/squareup/moshi/
[okio]: https://github.com/square/okio/
[okhttp]: https://github.com/square/okhttp/
[gson]: https://github.com/google/gson/
[javadoc]: http://square.github.io/moshi/1.x/moshi/
[javadoc]: https://square.github.io/moshi/1.x/moshi/
[kapt]: https://kotlinlang.org/docs/reference/kapt.html

37
adapters/README.md Normal file
View file

@ -0,0 +1,37 @@
Adapters
===================
Prebuilt Moshi `JsonAdapter`s for various things, such as `Rfc3339DateJsonAdapter` for parsing `java.util.Date`s
To use, supply an instance of your desired converter when building your `Moshi` instance.
```java
Moshi moshi = new Moshi.Builder()
.add(Date.class, new Rfc3339DateJsonAdapter())
//etc
.build();
```
Download
--------
Download [the latest JAR][1] or grab via [Maven][2]:
```xml
<dependency>
<groupId>com.squareup.moshi</groupId>
<artifactId>moshi-adapters</artifactId>
<version>latest.version</version>
</dependency>
```
or [Gradle][2]:
```groovy
implementation 'com.squareup.moshi:moshi-adapters:latest.version'
```
Snapshots of the development version are available in [Sonatype's `snapshots` repository][snap].
[1]: https://search.maven.org/remote_content?g=com.squareup.moshi&a=moshi-adapters&v=LATEST
[2]: http://search.maven.org/#search%7Cga%7C1%7Cg%3A%22com.squareup.moshi%22%20a%3A%22moshi-adapters%22
[snap]: https://oss.sonatype.org/content/repositories/snapshots/com/squareup/moshi/moshi-adapters/

View file

@ -6,7 +6,7 @@
<parent>
<groupId>com.squareup.moshi</groupId>
<artifactId>moshi-parent</artifactId>
<version>1.4.0</version>
<version>1.9.0-SNAPSHOT</version>
</parent>
<artifactId>moshi-adapters</artifactId>
@ -17,6 +17,11 @@
<artifactId>moshi</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>com.google.code.findbugs</groupId>
<artifactId>jsr305</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
@ -28,4 +33,20 @@
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<configuration>
<archive>
<manifestEntries>
<Automatic-Module-Name>com.squareup.moshi.adapters</Automatic-Module-Name>
</manifestEntries>
</archive>
</configuration>
</plugin>
</plugins>
</build>
</project>

View file

@ -19,17 +19,18 @@ import java.io.IOException;
import java.util.Date;
/**
* Formats dates using <a href="https://www.ietf.org/rfc/rfc3339.txt">RFC 3339</a>, which is
* formatted like {@code 2015-09-26T18:23:50.250Z}.
* @deprecated this class moved to avoid a package name conflict in the Java Platform Module System.
* The new class is {@code com.squareup.moshi.adapters.Rfc3339DateJsonAdapter}.
*/
public final class Rfc3339DateJsonAdapter extends JsonAdapter<Date> {
@Override public synchronized Date fromJson(JsonReader reader) throws IOException {
String string = reader.nextString();
return Iso8601Utils.parse(string);
com.squareup.moshi.adapters.Rfc3339DateJsonAdapter delegate
= new com.squareup.moshi.adapters.Rfc3339DateJsonAdapter();
@Override public Date fromJson(JsonReader reader) throws IOException {
return delegate.fromJson(reader);
}
@Override public synchronized void toJson(JsonWriter writer, Date value) throws IOException {
String string = Iso8601Utils.format(value);
writer.value(string);
@Override public void toJson(JsonWriter writer, Date value) throws IOException {
delegate.toJson(writer, value);
}
}

View file

@ -0,0 +1,110 @@
/*
* Copyright (C) 2018 Square, Inc.
*
* 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 com.squareup.moshi.adapters;
import com.squareup.moshi.Json;
import com.squareup.moshi.JsonAdapter;
import com.squareup.moshi.JsonDataException;
import com.squareup.moshi.JsonReader;
import com.squareup.moshi.JsonWriter;
import java.io.IOException;
import java.util.Arrays;
import javax.annotation.Nullable;
/**
* A JsonAdapter for enums that allows having a fallback enum value when a deserialized string does
* not match any enum value. To use, add this as an adapter for your enum type on your {@link
* com.squareup.moshi.Moshi.Builder Moshi.Builder}:
*
* <pre> {@code
*
* Moshi moshi = new Moshi.Builder()
* .add(CurrencyCode.class, EnumJsonAdapter.create(CurrencyCode.class)
* .withUnknownFallback(CurrencyCode.USD))
* .build();
* }</pre>
*/
public final class EnumJsonAdapter<T extends Enum<T>> extends JsonAdapter<T> {
final Class<T> enumType;
final String[] nameStrings;
final T[] constants;
final JsonReader.Options options;
final boolean useFallbackValue;
final @Nullable T fallbackValue;
public static <T extends Enum<T>> EnumJsonAdapter<T> create(Class<T> enumType) {
return new EnumJsonAdapter<>(enumType, null, false);
}
/**
* Create a new adapter for this enum with a fallback value to use when the JSON string does not
* match any of the enum's constants. Note that this value will not be used when the JSON value is
* null, absent, or not a string. Also, the string values are case-sensitive, and this fallback
* value will be used even on case mismatches.
*/
public EnumJsonAdapter<T> withUnknownFallback(@Nullable T fallbackValue) {
return new EnumJsonAdapter<>(enumType, fallbackValue, true);
}
EnumJsonAdapter(Class<T> enumType, @Nullable T fallbackValue, boolean useFallbackValue) {
this.enumType = enumType;
this.fallbackValue = fallbackValue;
this.useFallbackValue = useFallbackValue;
try {
constants = enumType.getEnumConstants();
nameStrings = new String[constants.length];
for (int i = 0; i < constants.length; i++) {
String constantName = constants[i].name();
Json annotation = enumType.getField(constantName).getAnnotation(Json.class);
String name = annotation != null ? annotation.name() : constantName;
nameStrings[i] = name;
}
options = JsonReader.Options.of(nameStrings);
} catch (NoSuchFieldException e) {
throw new AssertionError("Missing field in " + enumType.getName(), e);
}
}
@Override public @Nullable T fromJson(JsonReader reader) throws IOException {
int index = reader.selectString(options);
if (index != -1) return constants[index];
String path = reader.getPath();
if (!useFallbackValue) {
String name = reader.nextString();
throw new JsonDataException("Expected one of "
+ Arrays.asList(nameStrings) + " but was " + name + " at path " + path);
}
if (reader.peek() != JsonReader.Token.STRING) {
throw new JsonDataException(
"Expected a string but was " + reader.peek() + " at path " + path);
}
reader.skipValue();
return fallbackValue;
}
@Override public void toJson(JsonWriter writer, T value) throws IOException {
if (value == null) {
throw new NullPointerException(
"value was null! Wrap in .nullSafe() to write nullable values.");
}
writer.value(nameStrings[value.ordinal()]);
}
@Override public String toString() {
return "EnumJsonAdapter(" + enumType.getName() + ")";
}
}

View file

@ -13,8 +13,9 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.squareup.moshi;
package com.squareup.moshi.adapters;
import com.squareup.moshi.JsonDataException;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
@ -23,7 +24,7 @@ import java.util.TimeZone;
/**
* Jacksons date formatter, pruned to Moshi's needs. Forked from this file:
* https://github.com/FasterXML/jackson-databind/blob/master/src/main/java/com/fasterxml/jackson/databind/util/ISO8601Utils.java
* https://github.com/FasterXML/jackson-databind/blob/67ebf7305f492285a8f9f4de31545f5f16fc7c3a/src/main/java/com/fasterxml/jackson/databind/util/ISO8601Utils.java
*
* Utilities methods for manipulating dates in iso8601 format. This is much much faster and GC
* friendly than using SimpleDateFormat so highly suitable if you (un)serialize lots of date
@ -190,7 +191,7 @@ final class Iso8601Utils {
// If we get a ParseException it'll already have the right message/offset.
// Other exception types can convert here.
} catch (IndexOutOfBoundsException | IllegalArgumentException e) {
throw new JsonDataException("Not an RFC 3339 date: " + date);
throw new JsonDataException("Not an RFC 3339 date: " + date, e);
}
}

View file

@ -0,0 +1,301 @@
/*
* Copyright (C) 2011 Google Inc.
*
* 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 com.squareup.moshi.adapters;
import com.squareup.moshi.JsonAdapter;
import com.squareup.moshi.JsonDataException;
import com.squareup.moshi.JsonReader;
import com.squareup.moshi.JsonWriter;
import com.squareup.moshi.Moshi;
import com.squareup.moshi.Types;
import java.io.IOException;
import java.lang.annotation.Annotation;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import javax.annotation.CheckReturnValue;
import javax.annotation.Nullable;
/**
* A JsonAdapter factory for objects that include type information in the JSON. When decoding JSON
* Moshi uses this type information to determine which class to decode to. When encoding Moshi uses
* the objects class to determine what type information to include.
*
* <p>Suppose we have an interface, its implementations, and a class that uses them:
*
* <pre> {@code
*
* interface HandOfCards {
* }
*
* class BlackjackHand extends HandOfCards {
* Card hidden_card;
* List<Card> visible_cards;
* }
*
* class HoldemHand extends HandOfCards {
* Set<Card> hidden_cards;
* }
*
* class Player {
* String name;
* HandOfCards hand;
* }
* }</pre>
*
* <p>We want to decode the following JSON into the player model above:
*
* <pre> {@code
*
* {
* "name": "Jesse",
* "hand": {
* "hand_type": "blackjack",
* "hidden_card": "9D",
* "visible_cards": ["8H", "4C"]
* }
* }
* }</pre>
*
* <p>Left unconfigured, Moshi would incorrectly attempt to decode the hand object to the abstract
* {@code HandOfCards} interface. We configure it to use the appropriate subtype instead:
*
* <pre> {@code
*
* Moshi moshi = new Moshi.Builder()
* .add(PolymorphicJsonAdapterFactory.of(HandOfCards.class, "hand_type")
* .withSubtype(BlackjackHand.class, "blackjack")
* .withSubtype(HoldemHand.class, "holdem"))
* .build();
* }</pre>
*
* <p>This class imposes strict requirements on its use:
*
* <ul>
* <li>Base types may be classes or interfaces.
* <li>Subtypes must encode as JSON objects.
* <li>Type information must be in the encoded object. Each message must have a type label like
* {@code hand_type} whose value is a string like {@code blackjack} that identifies which type
* to use.
* <li>Each type identifier must be unique.
* </ul>
*
* <p>For best performance type information should be the first field in the object. Otherwise Moshi
* must reprocess the JSON stream once it knows the object's type.
*
* <p>If an unknown subtype is encountered when decoding, this will throw a {@link
* JsonDataException}. If an unknown type is encountered when encoding, this will throw an {@link
* IllegalArgumentException}.
*
* <p>If you want to specify a custom unknown fallback for decoding, you can do so via
* {@link #withDefaultValue(Object)}. This instance should be immutable, as it is shared.
*/
public final class PolymorphicJsonAdapterFactory<T> implements JsonAdapter.Factory {
final Class<T> baseType;
final String labelKey;
final List<String> labels;
final List<Type> subtypes;
@Nullable final T defaultValue;
final boolean defaultValueSet;
PolymorphicJsonAdapterFactory(
Class<T> baseType,
String labelKey,
List<String> labels,
List<Type> subtypes,
@Nullable T defaultValue,
boolean defaultValueSet) {
this.baseType = baseType;
this.labelKey = labelKey;
this.labels = labels;
this.subtypes = subtypes;
this.defaultValue = defaultValue;
this.defaultValueSet = defaultValueSet;
}
/**
* @param baseType The base type for which this factory will create adapters. Cannot be Object.
* @param labelKey The key in the JSON object whose value determines the type to which to map the
* JSON object.
*/
@CheckReturnValue
public static <T> PolymorphicJsonAdapterFactory<T> of(Class<T> baseType, String labelKey) {
if (baseType == null) throw new NullPointerException("baseType == null");
if (labelKey == null) throw new NullPointerException("labelKey == null");
return new PolymorphicJsonAdapterFactory<>(
baseType,
labelKey,
Collections.<String>emptyList(),
Collections.<Type>emptyList(),
null,
false);
}
/**
* Returns a new factory that decodes instances of {@code subtype}. When an unknown type is found
* during encoding an {@linkplain IllegalArgumentException} will be thrown. When an unknown label
* is found during decoding a {@linkplain JsonDataException} will be thrown.
*/
public PolymorphicJsonAdapterFactory<T> withSubtype(Class<? extends T> subtype, String label) {
if (subtype == null) throw new NullPointerException("subtype == null");
if (label == null) throw new NullPointerException("label == null");
if (labels.contains(label)) {
throw new IllegalArgumentException("Labels must be unique.");
}
List<String> newLabels = new ArrayList<>(labels);
newLabels.add(label);
List<Type> newSubtypes = new ArrayList<>(subtypes);
newSubtypes.add(subtype);
return new PolymorphicJsonAdapterFactory<>(baseType,
labelKey,
newLabels,
newSubtypes,
defaultValue,
defaultValueSet);
}
/**
* Returns a new factory that with default to {@code defaultValue} upon decoding of unrecognized
* labels. The default value should be immutable.
*/
public PolymorphicJsonAdapterFactory<T> withDefaultValue(@Nullable T defaultValue) {
return new PolymorphicJsonAdapterFactory<>(baseType,
labelKey,
labels,
subtypes,
defaultValue,
true);
}
@Override
public JsonAdapter<?> create(Type type, Set<? extends Annotation> annotations, Moshi moshi) {
if (Types.getRawType(type) != baseType || !annotations.isEmpty()) {
return null;
}
List<JsonAdapter<Object>> jsonAdapters = new ArrayList<>(subtypes.size());
for (int i = 0, size = subtypes.size(); i < size; i++) {
jsonAdapters.add(moshi.adapter(subtypes.get(i)));
}
return new PolymorphicJsonAdapter(labelKey,
labels,
subtypes,
jsonAdapters,
defaultValue,
defaultValueSet
).nullSafe();
}
static final class PolymorphicJsonAdapter extends JsonAdapter<Object> {
final String labelKey;
final List<String> labels;
final List<Type> subtypes;
final List<JsonAdapter<Object>> jsonAdapters;
@Nullable final Object defaultValue;
final boolean defaultValueSet;
/** Single-element options containing the label's key only. */
final JsonReader.Options labelKeyOptions;
/** Corresponds to subtypes. */
final JsonReader.Options labelOptions;
PolymorphicJsonAdapter(String labelKey,
List<String> labels,
List<Type> subtypes,
List<JsonAdapter<Object>> jsonAdapters,
@Nullable Object defaultValue,
boolean defaultValueSet) {
this.labelKey = labelKey;
this.labels = labels;
this.subtypes = subtypes;
this.jsonAdapters = jsonAdapters;
this.defaultValue = defaultValue;
this.defaultValueSet = defaultValueSet;
this.labelKeyOptions = JsonReader.Options.of(labelKey);
this.labelOptions = JsonReader.Options.of(labels.toArray(new String[0]));
}
@Override public Object fromJson(JsonReader reader) throws IOException {
JsonReader peeked = reader.peekJson();
peeked.setFailOnUnknown(false);
int labelIndex;
try {
labelIndex = labelIndex(peeked);
} finally {
peeked.close();
}
if (labelIndex == -1) {
reader.skipValue();
return defaultValue;
}
return jsonAdapters.get(labelIndex).fromJson(reader);
}
private int labelIndex(JsonReader reader) throws IOException {
reader.beginObject();
while (reader.hasNext()) {
if (reader.selectName(labelKeyOptions) == -1) {
reader.skipName();
reader.skipValue();
continue;
}
int labelIndex = reader.selectString(labelOptions);
if (labelIndex == -1 && !defaultValueSet) {
throw new JsonDataException("Expected one of "
+ labels
+ " for key '"
+ labelKey
+ "' but found '"
+ reader.nextString()
+ "'. Register a subtype for this label.");
}
return labelIndex;
}
throw new JsonDataException("Missing label for " + labelKey);
}
@Override public void toJson(JsonWriter writer, Object value) throws IOException {
Class<?> type = value.getClass();
int labelIndex = subtypes.indexOf(type);
if (labelIndex == -1) {
throw new IllegalArgumentException("Expected one of "
+ subtypes
+ " but found "
+ value
+ ", a "
+ value.getClass()
+ ". Register this subtype.");
}
JsonAdapter<Object> adapter = jsonAdapters.get(labelIndex);
writer.beginObject();
writer.name(labelKey).value(labels.get(labelIndex));
int flattenToken = writer.beginFlatten();
adapter.toJson(writer, value);
writer.endFlatten(flattenToken);
writer.endObject();
}
@Override public String toString() {
return "PolymorphicJsonAdapter(" + labelKey + ")";
}
}
}

View file

@ -0,0 +1,46 @@
/*
* Copyright (C) 2015 Square, Inc.
*
* 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 com.squareup.moshi.adapters;
import com.squareup.moshi.JsonAdapter;
import com.squareup.moshi.JsonReader;
import com.squareup.moshi.JsonWriter;
import java.io.IOException;
import java.util.Date;
/**
* Formats dates using <a href="https://www.ietf.org/rfc/rfc3339.txt">RFC 3339</a>, which is
* formatted like {@code 2015-09-26T18:23:50.250Z}. To use, add this as an adapter for {@code
* Date.class} on your {@link com.squareup.moshi.Moshi.Builder Moshi.Builder}:
*
* <pre> {@code
*
* Moshi moshi = new Moshi.Builder()
* .add(Date.class, new Rfc3339DateJsonAdapter())
* .build();
* }</pre>
*/
public final class Rfc3339DateJsonAdapter extends JsonAdapter<Date> {
@Override public synchronized Date fromJson(JsonReader reader) throws IOException {
String string = reader.nextString();
return Iso8601Utils.parse(string);
}
@Override public synchronized void toJson(JsonWriter writer, Date value) throws IOException {
String string = Iso8601Utils.format(value);
writer.value(string);
}
}

View file

@ -0,0 +1,75 @@
/*
* Copyright (C) 2018 Square, Inc.
*
* 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 com.squareup.moshi.adapters;
import com.squareup.moshi.Json;
import com.squareup.moshi.JsonDataException;
import com.squareup.moshi.JsonReader;
import okio.Buffer;
import org.junit.Test;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.Assert.fail;
@SuppressWarnings("CheckReturnValue")
public final class EnumJsonAdapterTest {
@Test public void toAndFromJson() throws Exception {
EnumJsonAdapter<Roshambo> adapter = EnumJsonAdapter.create(Roshambo.class);
assertThat(adapter.fromJson("\"ROCK\"")).isEqualTo(Roshambo.ROCK);
assertThat(adapter.toJson(Roshambo.PAPER)).isEqualTo("\"PAPER\"");
}
@Test public void withJsonName() throws Exception {
EnumJsonAdapter<Roshambo> adapter = EnumJsonAdapter.create(Roshambo.class);
assertThat(adapter.fromJson("\"scr\"")).isEqualTo(Roshambo.SCISSORS);
assertThat(adapter.toJson(Roshambo.SCISSORS)).isEqualTo("\"scr\"");
}
@Test public void withoutFallbackValue() throws Exception {
EnumJsonAdapter<Roshambo> adapter = EnumJsonAdapter.create(Roshambo.class);
JsonReader reader = JsonReader.of(new Buffer().writeUtf8("\"SPOCK\""));
try {
adapter.fromJson(reader);
fail();
} catch (JsonDataException expected) {
assertThat(expected).hasMessage(
"Expected one of [ROCK, PAPER, scr] but was SPOCK at path $");
}
assertThat(reader.peek()).isEqualTo(JsonReader.Token.END_DOCUMENT);
}
@Test public void withFallbackValue() throws Exception {
EnumJsonAdapter<Roshambo> adapter = EnumJsonAdapter.create(Roshambo.class)
.withUnknownFallback(Roshambo.ROCK);
JsonReader reader = JsonReader.of(new Buffer().writeUtf8("\"SPOCK\""));
assertThat(adapter.fromJson(reader)).isEqualTo(Roshambo.ROCK);
assertThat(reader.peek()).isEqualTo(JsonReader.Token.END_DOCUMENT);
}
@Test public void withNullFallbackValue() throws Exception {
EnumJsonAdapter<Roshambo> adapter = EnumJsonAdapter.create(Roshambo.class)
.withUnknownFallback(null);
JsonReader reader = JsonReader.of(new Buffer().writeUtf8("\"SPOCK\""));
assertThat(adapter.fromJson(reader)).isNull();
assertThat(reader.peek()).isEqualTo(JsonReader.Token.END_DOCUMENT);
}
enum Roshambo {
ROCK,
PAPER,
@Json(name = "scr") SCISSORS
}
}

View file

@ -0,0 +1,300 @@
/*
* Copyright (C) 2018 Square, Inc.
*
* 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 com.squareup.moshi.adapters;
import com.squareup.moshi.JsonAdapter;
import com.squareup.moshi.JsonDataException;
import com.squareup.moshi.JsonReader;
import com.squareup.moshi.Moshi;
import java.io.IOException;
import java.util.Collections;
import java.util.Map;
import okio.Buffer;
import org.junit.Test;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.Assert.fail;
@SuppressWarnings("CheckReturnValue")
public final class PolymorphicJsonAdapterFactoryTest {
@Test public void fromJson() throws IOException {
Moshi moshi = new Moshi.Builder()
.add(PolymorphicJsonAdapterFactory.of(Message.class, "type")
.withSubtype(Success.class, "success")
.withSubtype(Error.class, "error"))
.build();
JsonAdapter<Message> adapter = moshi.adapter(Message.class);
assertThat(adapter.fromJson("{\"type\":\"success\",\"value\":\"Okay!\"}"))
.isEqualTo(new Success("Okay!"));
assertThat(adapter.fromJson("{\"type\":\"error\",\"error_logs\":{\"order\":66}}"))
.isEqualTo(new Error(Collections.<String, Object>singletonMap("order", 66d)));
}
@Test public void toJson() {
Moshi moshi = new Moshi.Builder()
.add(PolymorphicJsonAdapterFactory.of(Message.class, "type")
.withSubtype(Success.class, "success")
.withSubtype(Error.class, "error"))
.build();
JsonAdapter<Message> adapter = moshi.adapter(Message.class);
assertThat(adapter.toJson(new Success("Okay!")))
.isEqualTo("{\"type\":\"success\",\"value\":\"Okay!\"}");
assertThat(adapter.toJson(new Error(Collections.<String, Object>singletonMap("order", 66))))
.isEqualTo("{\"type\":\"error\",\"error_logs\":{\"order\":66}}");
}
@Test public void unregisteredLabelValue() throws IOException {
Moshi moshi = new Moshi.Builder()
.add(PolymorphicJsonAdapterFactory.of(Message.class, "type")
.withSubtype(Success.class, "success")
.withSubtype(Error.class, "error"))
.build();
JsonAdapter<Message> adapter = moshi.adapter(Message.class);
JsonReader reader =
JsonReader.of(new Buffer().writeUtf8("{\"type\":\"data\",\"value\":\"Okay!\"}"));
try {
adapter.fromJson(reader);
fail();
} catch (JsonDataException expected) {
assertThat(expected).hasMessage("Expected one of [success, error] for key 'type' but found"
+ " 'data'. Register a subtype for this label.");
}
assertThat(reader.peek()).isEqualTo(JsonReader.Token.BEGIN_OBJECT);
}
@Test public void specifiedFallbackSubtype() throws IOException {
Error fallbackError = new Error(Collections.<String, Object>emptyMap());
Moshi moshi = new Moshi.Builder()
.add(PolymorphicJsonAdapterFactory.of(Message.class, "type")
.withSubtype(Success.class, "success")
.withSubtype(Error.class, "error")
.withDefaultValue(fallbackError))
.build();
JsonAdapter<Message> adapter = moshi.adapter(Message.class);
Message message = adapter.fromJson("{\"type\":\"data\",\"value\":\"Okay!\"}");
assertThat(message).isSameAs(fallbackError);
}
@Test public void specifiedNullFallbackSubtype() throws IOException {
Moshi moshi = new Moshi.Builder()
.add(PolymorphicJsonAdapterFactory.of(Message.class, "type")
.withSubtype(Success.class, "success")
.withSubtype(Error.class, "error")
.withDefaultValue(null))
.build();
JsonAdapter<Message> adapter = moshi.adapter(Message.class);
Message message = adapter.fromJson("{\"type\":\"data\",\"value\":\"Okay!\"}");
assertThat(message).isNull();
}
@Test public void unregisteredSubtype() {
Moshi moshi = new Moshi.Builder()
.add(PolymorphicJsonAdapterFactory.of(Message.class, "type")
.withSubtype(Success.class, "success")
.withSubtype(Error.class, "error"))
.build();
JsonAdapter<Message> adapter = moshi.adapter(Message.class);
try {
adapter.toJson(new EmptyMessage());
} catch (IllegalArgumentException expected) {
assertThat(expected).hasMessage("Expected one of [class"
+ " com.squareup.moshi.adapters.PolymorphicJsonAdapterFactoryTest$Success, class"
+ " com.squareup.moshi.adapters.PolymorphicJsonAdapterFactoryTest$Error] but found"
+ " EmptyMessage, a class"
+ " com.squareup.moshi.adapters.PolymorphicJsonAdapterFactoryTest$EmptyMessage. Register"
+ " this subtype.");
}
}
@Test public void nonStringLabelValue() throws IOException {
Moshi moshi = new Moshi.Builder()
.add(PolymorphicJsonAdapterFactory.of(Message.class, "type")
.withSubtype(Success.class, "success")
.withSubtype(Error.class, "error"))
.build();
JsonAdapter<Message> adapter = moshi.adapter(Message.class);
try {
adapter.fromJson("{\"type\":{},\"value\":\"Okay!\"}");
fail();
} catch (JsonDataException expected) {
assertThat(expected).hasMessage("Expected a string but was BEGIN_OBJECT at path $.type");
}
}
@Test public void nonObjectDoesNotConsume() throws IOException {
Moshi moshi = new Moshi.Builder()
.add(PolymorphicJsonAdapterFactory.of(Message.class, "type")
.withSubtype(Success.class, "success")
.withSubtype(Error.class, "error"))
.build();
JsonAdapter<Message> adapter = moshi.adapter(Message.class);
JsonReader reader = JsonReader.of(new Buffer().writeUtf8("\"Failure\""));
try {
adapter.fromJson(reader);
fail();
} catch (JsonDataException expected) {
assertThat(expected).hasMessage("Expected BEGIN_OBJECT but was STRING at path $");
}
assertThat(reader.nextString()).isEqualTo("Failure");
assertThat(reader.peek()).isEqualTo(JsonReader.Token.END_DOCUMENT);
}
@Test public void nonUniqueSubtypes() throws IOException {
Moshi moshi = new Moshi.Builder()
.add(PolymorphicJsonAdapterFactory.of(Message.class, "type")
.withSubtype(Success.class, "success")
.withSubtype(Success.class, "data")
.withSubtype(Error.class, "error"))
.build();
JsonAdapter<Message> adapter = moshi.adapter(Message.class);
assertThat(adapter.fromJson("{\"type\":\"success\",\"value\":\"Okay!\"}"))
.isEqualTo(new Success("Okay!"));
assertThat(adapter.fromJson("{\"type\":\"data\",\"value\":\"Data!\"}"))
.isEqualTo(new Success("Data!"));
assertThat(adapter.fromJson("{\"type\":\"error\",\"error_logs\":{\"order\":66}}"))
.isEqualTo(new Error(Collections.<String, Object>singletonMap("order", 66d)));
}
@Test public void uniqueLabels() {
PolymorphicJsonAdapterFactory<Message> factory =
PolymorphicJsonAdapterFactory.of(Message.class, "type")
.withSubtype(Success.class, "data");
try {
factory.withSubtype(Error.class, "data");
fail();
} catch (IllegalArgumentException expected) {
assertThat(expected).hasMessage("Labels must be unique.");
}
}
@Test public void nullSafe() throws IOException {
Moshi moshi = new Moshi.Builder()
.add(PolymorphicJsonAdapterFactory.of(Message.class, "type")
.withSubtype(Success.class, "success")
.withSubtype(Error.class, "error"))
.build();
JsonAdapter<Message> adapter = moshi.adapter(Message.class);
JsonReader reader = JsonReader.of(new Buffer().writeUtf8("null"));
assertThat(adapter.fromJson(reader)).isNull();
assertThat(reader.peek()).isEqualTo(JsonReader.Token.END_DOCUMENT);
}
/**
* Longs that do not have an exact double representation are problematic for JSON. It is a bad
* idea to use JSON for these values! But Moshi tries to retain long precision where possible.
*/
@Test public void unportableTypes() throws IOException {
Moshi moshi = new Moshi.Builder()
.add(PolymorphicJsonAdapterFactory.of(Message.class, "type")
.withSubtype(MessageWithUnportableTypes.class, "unportable"))
.build();
JsonAdapter<Message> adapter = moshi.adapter(Message.class);
assertThat(adapter.toJson(new MessageWithUnportableTypes(9007199254740993L)))
.isEqualTo("{\"type\":\"unportable\",\"long_value\":9007199254740993}");
MessageWithUnportableTypes decoded = (MessageWithUnportableTypes) adapter.fromJson(
"{\"type\":\"unportable\",\"long_value\":9007199254740993}");
assertThat(decoded.long_value).isEqualTo(9007199254740993L);
}
@Test public void failOnUnknownMissingTypeLabel() throws IOException {
Moshi moshi = new Moshi.Builder()
.add(PolymorphicJsonAdapterFactory.of(Message.class, "type")
.withSubtype(MessageWithType.class, "success"))
.build();
JsonAdapter<Message> adapter = moshi.adapter(Message.class).failOnUnknown();
MessageWithType decoded = (MessageWithType) adapter.fromJson(
"{\"value\":\"Okay!\",\"type\":\"success\"}");
assertThat(decoded.value).isEqualTo("Okay!");
}
interface Message {
}
static final class Success implements Message {
final String value;
Success(String value) {
this.value = value;
}
@Override public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Success)) return false;
Success success = (Success) o;
return value.equals(success.value);
}
@Override public int hashCode() {
return value.hashCode();
}
}
static final class Error implements Message {
final Map<String, Object> error_logs;
Error(Map<String, Object> error_logs) {
this.error_logs = error_logs;
}
@Override public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Error)) return false;
Error error = (Error) o;
return error_logs.equals(error.error_logs);
}
@Override public int hashCode() {
return error_logs.hashCode();
}
}
static final class EmptyMessage implements Message {
@Override public String toString() {
return "EmptyMessage";
}
}
static final class MessageWithUnportableTypes implements Message {
final long long_value;
MessageWithUnportableTypes(long long_value) {
this.long_value = long_value;
}
}
static final class MessageWithType implements Message {
final String type;
final String value;
MessageWithType(String type, String value) {
this.type = type;
this.value = value;
}
}
}

View file

@ -13,8 +13,9 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.squareup.moshi;
package com.squareup.moshi.adapters;
import com.squareup.moshi.JsonAdapter;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;

View file

@ -1,9 +1,10 @@
<?xml version="1.0"?>
<!DOCTYPE module PUBLIC
"-//Puppy Crawl//DTD Check Configuration 1.2//EN"
"http://www.puppycrawl.com/dtds/configuration_1_2.dtd">
"-//Puppy Crawl//DTD Check Configuration 1.3//EN"
"http://www.puppycrawl.com/dtds/configuration_1_3.dtd">
<module name="Checker">
<module name="SuppressWarningsFilter"/>
<module name="NewlineAtEndOfFile"/>
<module name="FileLength"/>
<module name="FileTabCharacter"/>
@ -44,7 +45,7 @@
<module name="LocalVariableName"/>
<module name="MemberName"/>
<module name="MethodName"/>
<module name="PackageName"/>
<!--<module name="PackageName"/>-->
<module name="ParameterName"/>
<module name="StaticVariableName"/>
<module name="TypeName"/>
@ -56,7 +57,9 @@
<module name="IllegalImport"/>
<!-- defaults to sun.* packages -->
<module name="RedundantImport"/>
<module name="UnusedImports"/>
<module name="UnusedImports">
<property name="processJavadoc" value="true"/>
</module>
<!-- Checks for Size Violations. -->
@ -64,7 +67,9 @@
<module name="LineLength">
<property name="max" value="100"/>
</module>
<module name="MethodLength"/>
<module name="MethodLength">
<property name="max" value="200"/>
</module>
<!-- Checks for whitespace -->
@ -78,7 +83,15 @@
<module name="ParenPad"/>
<module name="TypecastParenPad"/>
<module name="WhitespaceAfter"/>
<module name="WhitespaceAround"/>
<module name="WhitespaceAround">
<property name="tokens"
value="ASSIGN, BAND, BAND_ASSIGN, BOR, BOR_ASSIGN, BSR, BSR_ASSIGN, BXOR, BXOR_ASSIGN,
COLON, DIV, DIV_ASSIGN, DO_WHILE, EQUAL, GE, GT, LAND, LCURLY, LE, LITERAL_CATCH,
LITERAL_DO, LITERAL_ELSE, LITERAL_FINALLY, LITERAL_FOR, LITERAL_IF, LITERAL_RETURN,
LITERAL_SWITCH, LITERAL_SYNCHRONIZED, LITERAL_TRY, LITERAL_WHILE, LOR, LT, MINUS,
MINUS_ASSIGN, MOD, MOD_ASSIGN, NOT_EQUAL, PLUS, PLUS_ASSIGN, QUESTION, SL, SLIST,
SL_ASSIGN, SR, SR_ASSIGN, STAR, STAR_ASSIGN, LITERAL_ASSERT, TYPE_EXTENSION_AND"/>
</module>
<!-- Modifier Checks -->
@ -108,7 +121,7 @@
<!--module name="InnerAssignment"/-->
<!--module name="MagicNumber"/-->
<!--module name="MissingSwitchDefault"/-->
<module name="RedundantThrows"/>
<!--<module name="RedundantThrows"/>-->
<module name="SimplifyBooleanExpression"/>
<module name="SimplifyBooleanReturn"/>
@ -127,5 +140,8 @@
<!--module name="FinalParameters"/-->
<!--module name="TodoComment"/-->
<module name="UpperEll"/>
<!-- Make the @SuppressWarnings annotations available to Checkstyle -->
<module name="SuppressWarningsHolder"/>
</module>
</module>

View file

@ -6,12 +6,17 @@
<parent>
<groupId>com.squareup.moshi</groupId>
<artifactId>moshi-parent</artifactId>
<version>1.4.0</version>
<version>1.9.0-SNAPSHOT</version>
</parent>
<artifactId>moshi-examples</artifactId>
<dependencies>
<dependency>
<groupId>com.google.code.findbugs</groupId>
<artifactId>jsr305</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.squareup.moshi</groupId>
<artifactId>moshi</artifactId>

View file

@ -0,0 +1,57 @@
/*
* Copyright (C) 2018 Square, Inc.
*
* 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 com.squareup.moshi.recipes;
import com.squareup.moshi.JsonAdapter;
import com.squareup.moshi.JsonReader;
import com.squareup.moshi.JsonWriter;
import com.squareup.moshi.Moshi;
import java.io.IOException;
import okio.ByteString;
public final class ByteStrings {
public void run() throws Exception {
String json = "\"TW9zaGksIE9saXZlLCBXaGl0ZSBDaGluPw\"";
Moshi moshi = new Moshi.Builder()
.add(ByteString.class, new Base64ByteStringAdapter())
.build();
JsonAdapter<ByteString> jsonAdapter = moshi.adapter(ByteString.class);
ByteString byteString = jsonAdapter.fromJson(json);
System.out.println(byteString);
}
/**
* Formats byte strings using <a href="http://www.ietf.org/rfc/rfc2045.txt">Base64</a>. No line
* breaks or whitespace is included in the encoded form.
*/
public final class Base64ByteStringAdapter extends JsonAdapter<ByteString> {
@Override public ByteString fromJson(JsonReader reader) throws IOException {
String base64 = reader.nextString();
return ByteString.decodeBase64(base64);
}
@Override public void toJson(JsonWriter writer, ByteString value) throws IOException {
String string = value.base64();
writer.value(string);
}
}
public static void main(String[] args) throws Exception {
new ByteStrings().run();
}
}

View file

@ -0,0 +1,113 @@
/*
* Copyright (C) 2018 Square, Inc.
*
* 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 com.squareup.moshi.recipes;
import com.squareup.moshi.JsonAdapter;
import com.squareup.moshi.JsonReader;
import com.squareup.moshi.JsonWriter;
import com.squareup.moshi.Moshi;
import com.squareup.moshi.Types;
import java.io.IOException;
import java.lang.annotation.Annotation;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;
import javax.annotation.Nullable;
public final class CustomAdapterFactory {
public void run() throws Exception {
Moshi moshi = new Moshi.Builder()
.add(new SortedSetAdapterFactory())
.build();
JsonAdapter<SortedSet<String>> jsonAdapter = moshi.adapter(
Types.newParameterizedType(SortedSet.class, String.class));
TreeSet<String> model = new TreeSet<>();
model.add("a");
model.add("b");
model.add("c");
String json = jsonAdapter.toJson(model);
System.out.println(json);
}
/**
* This class composes an adapter for any element type into an adapter for a sorted set of those
* elements. For example, given a {@code JsonAdapter<MovieTicket>}, use this to get a
* {@code JsonAdapter<SortedSet<MovieTicket>>}. It works by looping over the input elements when
* both reading and writing.
*/
static final class SortedSetAdapter<T> extends JsonAdapter<SortedSet<T>> {
private final JsonAdapter<T> elementAdapter;
SortedSetAdapter(JsonAdapter<T> elementAdapter) {
this.elementAdapter = elementAdapter;
}
@Override public SortedSet<T> fromJson(JsonReader reader) throws IOException {
TreeSet<T> result = new TreeSet<>();
reader.beginArray();
while (reader.hasNext()) {
result.add(elementAdapter.fromJson(reader));
}
reader.endArray();
return result;
}
@Override public void toJson(JsonWriter writer, SortedSet<T> set) throws IOException {
writer.beginArray();
for (T element : set) {
elementAdapter.toJson(writer, element);
}
writer.endArray();
}
}
/**
* Moshi asks this class to create JSON adapters. It only knows how to create JSON adapters for
* {@code SortedSet} types, so it returns null for all other requests. When it does get a request
* for a {@code SortedSet<X>}, it asks Moshi for an adapter of the element type {@code X} and then
* uses that to create an adapter for the set.
*/
static class SortedSetAdapterFactory implements JsonAdapter.Factory {
@Override public @Nullable JsonAdapter<?> create(
Type type, Set<? extends Annotation> annotations, Moshi moshi) {
if (!annotations.isEmpty()) {
return null; // Annotations? This factory doesn't apply.
}
if (!(type instanceof ParameterizedType)) {
return null; // No type parameter? This factory doesn't apply.
}
ParameterizedType parameterizedType = (ParameterizedType) type;
if (parameterizedType.getRawType() != SortedSet.class) {
return null; // Not a sorted set? This factory doesn't apply.
}
Type elementType = parameterizedType.getActualTypeArguments()[0];
JsonAdapter<Object> elementAdapter = moshi.adapter(elementType);
return new SortedSetAdapter<>(elementAdapter).nullSafe();
}
}
public static void main(String[] args) throws Exception {
new CustomAdapterFactory().run();
}
}

View file

@ -0,0 +1,66 @@
/*
* Copyright (C) 2015 Square, Inc.
*
* 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 com.squareup.moshi.recipes;
import com.squareup.moshi.FromJson;
import com.squareup.moshi.Json;
import com.squareup.moshi.JsonAdapter;
import com.squareup.moshi.Moshi;
import com.squareup.moshi.JsonReader;
import javax.annotation.Nullable;
import java.io.IOException;
public final class CustomAdapterWithDelegate {
public void run() throws Exception {
// We want to match any Stage that starts with 'in-progress' as Stage.IN_PROGRESS
// and leave the rest of the enum values as to match as normal.
Moshi moshi = new Moshi.Builder().add(new StageAdapter()).build();
JsonAdapter<Stage> jsonAdapter = moshi.adapter(Stage.class);
System.out.println(jsonAdapter.fromJson("\"not-started\""));
System.out.println(jsonAdapter.fromJson("\"in-progress\""));
System.out.println(jsonAdapter.fromJson("\"in-progress-step1\""));
}
public static void main(String[] args) throws Exception {
new CustomAdapterWithDelegate().run();
}
private enum Stage {
@Json(name = "not-started") NOT_STARTED,
@Json(name = "in-progress") IN_PROGRESS,
@Json(name = "rejected") REJECTED,
@Json(name = "completed") COMPLETED
}
private static final class StageAdapter {
@FromJson
@Nullable
Stage fromJson(JsonReader jsonReader, JsonAdapter<Stage> delegate) throws IOException {
String value = jsonReader.nextString();
Stage stage;
if (value.startsWith("in-progress")) {
stage = Stage.IN_PROGRESS;
} else {
stage = delegate.fromJsonValue(value);
}
return stage;
}
}
}

View file

@ -21,7 +21,7 @@ import com.squareup.moshi.recipes.models.Player;
public final class CustomFieldName {
public void run() throws Exception {
String json = ""
String json = ""
+ "{"
+ " \"username\": \"jesse\","
+ " \"lucky number\": 32"

View file

@ -24,6 +24,7 @@ import java.io.IOException;
import java.lang.annotation.Annotation;
import java.lang.reflect.Type;
import java.util.Set;
import javax.annotation.Nullable;
public final class DefaultOnDataMismatchAdapter<T> extends JsonAdapter<T> {
private final JsonAdapter<T> delegate;
@ -35,17 +36,20 @@ public final class DefaultOnDataMismatchAdapter<T> extends JsonAdapter<T> {
}
@Override public T fromJson(JsonReader reader) throws IOException {
// Read the value first so that the reader will be in a known state even if there's an
// exception. Otherwise it may be awkward to recover: it might be between calls to
// beginObject() and endObject() for example.
Object jsonValue = reader.readJsonValue();
// Use the delegate to convert the JSON value to the target type.
// Use a peeked reader to leave the reader in a known state even if there's an exception.
JsonReader peeked = reader.peekJson();
T result;
try {
return delegate.fromJsonValue(jsonValue);
// Attempt to decode to the target type with the peeked reader.
result = delegate.fromJson(peeked);
} catch (JsonDataException e) {
return defaultValue;
result = defaultValue;
} finally {
peeked.close();
}
// Skip the value back on the reader, no matter the state of the peeked reader.
reader.skipValue();
return result;
}
@Override public void toJson(JsonWriter writer, T value) throws IOException {
@ -54,7 +58,7 @@ public final class DefaultOnDataMismatchAdapter<T> extends JsonAdapter<T> {
public static <T> Factory newFactory(final Class<T> type, final T defaultValue) {
return new Factory() {
@Override public JsonAdapter<?> create(
@Override public @Nullable JsonAdapter<?> create(
Type requestedType, Set<? extends Annotation> annotations, Moshi moshi) {
if (type != requestedType) return null;
JsonAdapter<T> delegate = moshi.nextAdapter(this, type, annotations);

View file

@ -0,0 +1,131 @@
/*
* Copyright (C) 2018 Square, Inc.
*
* 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 com.squareup.moshi.recipes;
import com.squareup.moshi.Json;
import com.squareup.moshi.JsonAdapter;
import com.squareup.moshi.JsonQualifier;
import com.squareup.moshi.JsonReader;
import com.squareup.moshi.JsonWriter;
import com.squareup.moshi.Moshi;
import com.squareup.moshi.Types;
import java.io.IOException;
import java.lang.annotation.Annotation;
import java.lang.annotation.Retention;
import java.lang.reflect.Type;
import java.util.Set;
import javax.annotation.Nullable;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
final class FallbackEnum {
@Retention(RUNTIME)
@JsonQualifier
public @interface Fallback {
/**
* The enum name.
*/
String value();
}
public static final class FallbackEnumJsonAdapter<T extends Enum<T>> extends JsonAdapter<T> {
public static final Factory FACTORY = new Factory() {
@Nullable @Override @SuppressWarnings("unchecked")
public JsonAdapter<?> create(Type type, Set<? extends Annotation> annotations, Moshi moshi) {
Class<?> rawType = Types.getRawType(type);
if (!rawType.isEnum()) {
return null;
}
if (annotations.size() != 1) {
return null;
}
Annotation annotation = annotations.iterator().next();
if (!(annotation instanceof Fallback)) {
return null;
}
Class<Enum> enumType = (Class<Enum>) rawType;
Enum<?> fallback = Enum.valueOf(enumType, ((Fallback) annotation).value());
return new FallbackEnumJsonAdapter<>(enumType, fallback);
}
};
final Class<T> enumType;
final String[] nameStrings;
final T[] constants;
final JsonReader.Options options;
final T defaultValue;
FallbackEnumJsonAdapter(Class<T> enumType, T defaultValue) {
this.enumType = enumType;
this.defaultValue = defaultValue;
try {
constants = enumType.getEnumConstants();
nameStrings = new String[constants.length];
for (int i = 0; i < constants.length; i++) {
T constant = constants[i];
Json annotation = enumType.getField(constant.name()).getAnnotation(Json.class);
String name = annotation != null ? annotation.name() : constant.name();
nameStrings[i] = name;
}
options = JsonReader.Options.of(nameStrings);
} catch (NoSuchFieldException e) {
throw new AssertionError(e);
}
}
@Override public T fromJson(JsonReader reader) throws IOException {
int index = reader.selectString(options);
if (index != -1) return constants[index];
reader.nextString();
return defaultValue;
}
@Override public void toJson(JsonWriter writer, T value) throws IOException {
writer.value(nameStrings[value.ordinal()]);
}
@Override public String toString() {
return "JsonAdapter(" + enumType.getName() + ").defaultValue( " + defaultValue + ")";
}
}
static final class Example {
enum Transportation {
WALKING, BIKING, TRAINS, PLANES
}
@Fallback("WALKING") final Transportation transportation;
Example(Transportation transportation) {
this.transportation = transportation;
}
@Override public String toString() {
return transportation.toString();
}
}
public static void main(String[] args) throws Exception {
Moshi moshi = new Moshi.Builder()
.add(FallbackEnumJsonAdapter.FACTORY)
.build();
JsonAdapter<Example> adapter = moshi.adapter(Example.class);
System.out.println(adapter.fromJson("{\"transportation\":\"CARS\"}"));
}
private FallbackEnum() {
}
}

View file

@ -44,6 +44,7 @@ public final class FromJsonWithoutStrings {
new FromJsonWithoutStrings().run();
}
@SuppressWarnings("checkstyle:membername")
private static final class EventJson {
String title;
String begin_date;
@ -55,10 +56,10 @@ public final class FromJsonWithoutStrings {
String beginDateAndTime;
@Override public String toString() {
return "Event{" +
"title='" + title + '\'' +
", beginDateAndTime='" + beginDateAndTime + '\'' +
'}';
return "Event{"
+ "title='" + title + '\''
+ ", beginDateAndTime='" + beginDateAndTime + '\''
+ '}';
}
}

View file

@ -0,0 +1,94 @@
/*
* Copyright (C) 2018 Square, Inc.
*
* 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 com.squareup.moshi.recipes;
import com.squareup.moshi.FromJson;
import com.squareup.moshi.JsonAdapter;
import com.squareup.moshi.JsonDataException;
import com.squareup.moshi.JsonQualifier;
import com.squareup.moshi.JsonReader;
import com.squareup.moshi.JsonWriter;
import com.squareup.moshi.Moshi;
import com.squareup.moshi.ToJson;
import com.squareup.moshi.recipes.models.Card;
import com.squareup.moshi.recipes.models.Suit;
import java.io.IOException;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
public final class MultipleFormats {
public void run() throws Exception {
Moshi moshi = new Moshi.Builder()
.add(new MultipleFormatsCardAdapter())
.add(new CardStringAdapter())
.build();
JsonAdapter<Card> cardAdapter = moshi.adapter(Card.class);
// Decode cards from one format or the other.
System.out.println(cardAdapter.fromJson("\"5D\""));
System.out.println(cardAdapter.fromJson("{\"suit\": \"SPADES\", \"rank\": 5}"));
// Cards are always encoded as strings.
System.out.println(cardAdapter.toJson(new Card('5', Suit.CLUBS)));
}
/** Handles cards either as strings "5D" or as objects {"suit": "SPADES", "rank": 5}. */
public final class MultipleFormatsCardAdapter {
@ToJson void toJson(JsonWriter writer, Card value,
@CardString JsonAdapter<Card> stringAdapter) throws IOException {
stringAdapter.toJson(writer, value);
}
@FromJson Card fromJson(JsonReader reader, @CardString JsonAdapter<Card> stringAdapter,
JsonAdapter<Card> defaultAdapter) throws IOException {
if (reader.peek() == JsonReader.Token.STRING) {
return stringAdapter.fromJson(reader);
} else {
return defaultAdapter.fromJson(reader);
}
}
}
/** Handles cards as strings only. */
public final class CardStringAdapter {
@ToJson String toJson(@CardString Card card) {
return card.rank + card.suit.name().substring(0, 1);
}
@FromJson @CardString Card fromJson(String card) {
if (card.length() != 2) throw new JsonDataException("Unknown card: " + card);
char rank = card.charAt(0);
switch (card.charAt(1)) {
case 'C': return new Card(rank, Suit.CLUBS);
case 'D': return new Card(rank, Suit.DIAMONDS);
case 'H': return new Card(rank, Suit.HEARTS);
case 'S': return new Card(rank, Suit.SPADES);
default: throw new JsonDataException("unknown suit: " + card);
}
}
}
@Retention(RetentionPolicy.RUNTIME)
@JsonQualifier
@interface CardString {
}
public static void main(String[] args) throws Exception {
new MultipleFormats().run();
}
}

View file

@ -17,7 +17,7 @@ package com.squareup.moshi.recipes;
import com.squareup.moshi.JsonAdapter;
import com.squareup.moshi.Moshi;
import com.squareup.moshi.Rfc3339DateJsonAdapter;
import com.squareup.moshi.adapters.Rfc3339DateJsonAdapter;
import com.squareup.moshi.recipes.models.Tournament;
import java.util.Calendar;
import java.util.Date;

View file

@ -21,7 +21,7 @@ import com.squareup.moshi.recipes.models.BlackjackHand;
public final class ReadJson {
public void run() throws Exception {
String json = ""
String json = ""
+ "{\n"
+ " \"hidden_card\": {\n"
+ " \"rank\": \"6\",\n"

View file

@ -28,10 +28,14 @@ import java.lang.annotation.Annotation;
import java.lang.annotation.Retention;
import java.lang.reflect.Type;
import java.util.Set;
import javax.annotation.Nullable;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
final class Unwrap {
private Unwrap() {
}
public static void main(String[] args) throws Exception {
String json = ""
+ "{\"data\":"
@ -48,8 +52,8 @@ final class Unwrap {
public static final class EnvelopeJsonAdapter extends JsonAdapter<Object> {
public static final JsonAdapter.Factory FACTORY = new Factory() {
@Override
public JsonAdapter<?> create(Type type, Set<? extends Annotation> annotations, Moshi moshi) {
@Override public @Nullable JsonAdapter<?> create(
Type type, Set<? extends Annotation> annotations, Moshi moshi) {
Set<? extends Annotation> delegateAnnotations =
Types.nextAnnotations(annotations, Enveloped.class);
if (delegateAnnotations == null) {
@ -84,7 +88,7 @@ final class Unwrap {
}
@Override public void toJson(JsonWriter writer, Object value) throws IOException {
delegate.toJson(new Envelope<>(value));
delegate.toJson(writer, new Envelope<>(value));
}
}
}

View file

@ -17,13 +17,14 @@ package com.squareup.moshi.recipes.models;
import java.util.List;
@SuppressWarnings("checkstyle:membername")
public final class BlackjackHand {
public final Card hidden_card;
public final List<Card> visible_cards;
public BlackjackHand(Card hidden_card, List<Card> visible_cards) {
this.hidden_card = hidden_card;
this.visible_cards = visible_cards;
public BlackjackHand(Card hiddenCard, List<Card> visibleCards) {
this.hidden_card = hiddenCard;
this.visible_cards = visibleCards;
}
@Override public String toString() {

View file

@ -0,0 +1,3 @@
/** Moshi code samples. */
@javax.annotation.ParametersAreNonnullByDefault
package com.squareup.moshi.recipes;

205
kotlin/codegen/pom.xml Normal file
View file

@ -0,0 +1,205 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.squareup.moshi</groupId>
<artifactId>moshi-parent</artifactId>
<version>1.9.0-SNAPSHOT</version>
<relativePath>../../pom.xml</relativePath>
</parent>
<artifactId>moshi-kotlin-codegen</artifactId>
<dependencies>
<dependency>
<groupId>com.squareup.moshi</groupId>
<artifactId>moshi</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-stdlib</artifactId>
</dependency>
<dependency>
<groupId>com.squareup</groupId>
<artifactId>kotlinpoet</artifactId>
<version>1.2.0</version>
</dependency>
<dependency>
<groupId>net.ltgt.gradle.incap</groupId>
<artifactId>incap</artifactId>
<version>${incap.version}</version>
<scope>provided</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.google.auto</groupId>
<artifactId>auto-common</artifactId>
<version>0.10</version>
</dependency>
<dependency>
<groupId>com.google.auto.service</groupId>
<artifactId>auto-service-annotations</artifactId>
<version>${autoservice.version}</version>
<scope>provided</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<scope>test</scope>
</dependency>
<!--
The Kotlin compiler must be near the end of the list because its .jar file includes an
obsolete version of Guava!
-->
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-compiler-embeddable</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-annotation-processing-embeddable</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>me.eugeniomarletti.kotlin.metadata</groupId>
<artifactId>kotlin-metadata</artifactId>
</dependency>
<!--
Though we don't use compile-testing, including it is a convenient way to get tools.jar on the
classpath. This dependency is required by kapt3.
-->
<dependency>
<groupId>com.google.testing.compile</groupId>
<artifactId>compile-testing</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<properties>
<maven.javadoc.skip>true</maven.javadoc.skip><!-- We use Dokka instead. -->
</properties>
<build>
<plugins>
<plugin>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-maven-plugin</artifactId>
<version>${kotlin.version}</version>
<executions>
<execution>
<id>kapt</id>
<goals>
<goal>kapt</goal>
</goals>
<configuration>
<sourceDirs>
<sourceDir>src/main/kotlin</sourceDir>
<sourceDir>src/main/java</sourceDir>
</sourceDirs>
<annotationProcessorPaths>
<annotationProcessorPath>
<groupId>com.google.auto.service</groupId>
<artifactId>auto-service</artifactId>
<version>${autoservice.version}</version>
</annotationProcessorPath>
<annotationProcessorPath>
<groupId>net.ltgt.gradle.incap</groupId>
<artifactId>incap-processor</artifactId>
<version>${incap.version}</version>
</annotationProcessorPath>
</annotationProcessorPaths>
</configuration>
</execution>
<execution>
<id>compile</id>
<phase>compile</phase>
<goals>
<goal>compile</goal>
</goals>
</execution>
<execution>
<id>test-compile</id>
<phase>test-compile</phase>
<goals>
<goal>test-compile</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<executions>
<execution>
<id>compile</id>
<phase>compile</phase>
<goals>
<goal>compile</goal>
</goals>
</execution>
<execution>
<id>testCompile</id>
<phase>test-compile</phase>
<goals>
<goal>testCompile</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.21.0</version>
<configuration>
<!--
Suppress the surefire classloader which prevents introspecting the classpath.
http://maven.apache.org/surefire/maven-surefire-plugin/examples/class-loading.html
-->
<useManifestOnlyJar>false</useManifestOnlyJar>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<version>${maven-assembly.version}</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
<configuration>
<descriptors>
<descriptor>src/assembly/dokka.xml</descriptor>
</descriptors>
</configuration>
</plugin>
<plugin>
<groupId>org.jetbrains.dokka</groupId>
<artifactId>dokka-maven-plugin</artifactId>
<version>${dokka.version}</version>
<executions>
<execution>
<phase>prepare-package</phase>
<goals>
<goal>dokka</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

View file

@ -0,0 +1,16 @@
<assembly
xmlns="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.3"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.3 http://maven.apache.org/xsd/assembly-1.1.3.xsd">
<id>javadoc</id>
<formats>
<format>jar</format>
</formats>
<baseDirectory>/</baseDirectory>
<fileSets>
<fileSet>
<directory>target/dokka/moshi-kotlin-codegen</directory>
<outputDirectory>/</outputDirectory>
</fileSet>
</fileSets>
</assembly>

View file

@ -0,0 +1,327 @@
/*
* Copyright (C) 2018 Square, Inc.
*
* 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 com.squareup.moshi.kotlin.codegen
import com.squareup.kotlinpoet.ARRAY
import com.squareup.kotlinpoet.AnnotationSpec
import com.squareup.kotlinpoet.CodeBlock
import com.squareup.kotlinpoet.FileSpec
import com.squareup.kotlinpoet.FunSpec
import com.squareup.kotlinpoet.KModifier
import com.squareup.kotlinpoet.NameAllocator
import com.squareup.kotlinpoet.ParameterSpec
import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy
import com.squareup.kotlinpoet.PropertySpec
import com.squareup.kotlinpoet.TypeSpec
import com.squareup.kotlinpoet.TypeVariableName
import com.squareup.kotlinpoet.asClassName
import com.squareup.kotlinpoet.asTypeName
import com.squareup.moshi.JsonAdapter
import com.squareup.moshi.JsonDataException
import com.squareup.moshi.JsonReader
import com.squareup.moshi.JsonWriter
import com.squareup.moshi.Moshi
import me.eugeniomarletti.kotlin.metadata.isDataClass
import me.eugeniomarletti.kotlin.metadata.shadow.metadata.ProtoBuf.Visibility
import me.eugeniomarletti.kotlin.metadata.visibility
import java.lang.reflect.Type
import javax.lang.model.element.TypeElement
/** Generates a JSON adapter for a target type. */
internal class AdapterGenerator(
target: TargetType,
private val propertyList: List<PropertyGenerator>
) {
private val className = target.name
private val isDataClass = target.proto.isDataClass
private val visibility = target.proto.visibility!!
private val typeVariables = target.typeVariables
private val nameAllocator = NameAllocator()
private val adapterName = "${className.simpleNames.joinToString(separator = "_")}JsonAdapter"
private val originalElement = target.element
private val originalTypeName = target.element.asType().asTypeName()
private val moshiParam = ParameterSpec.builder(
nameAllocator.newName("moshi"),
Moshi::class).build()
private val typesParam = ParameterSpec.builder(
nameAllocator.newName("types"),
ARRAY.parameterizedBy(Type::class.asTypeName()))
.build()
private val readerParam = ParameterSpec.builder(
nameAllocator.newName("reader"),
JsonReader::class)
.build()
private val writerParam = ParameterSpec.builder(
nameAllocator.newName("writer"),
JsonWriter::class)
.build()
private val valueParam = ParameterSpec.builder(
nameAllocator.newName("value"),
originalTypeName.copy(nullable = true))
.build()
private val jsonAdapterTypeName = JsonAdapter::class.asClassName().parameterizedBy(originalTypeName)
// selectName() API setup
private val optionsProperty = PropertySpec.builder(
nameAllocator.newName("options"), JsonReader.Options::class.asTypeName(),
KModifier.PRIVATE)
.initializer("%T.of(${propertyList.joinToString(", ") {
CodeBlock.of("%S", it.jsonName).toString()
}})", JsonReader.Options::class.asTypeName())
.build()
fun generateFile(generatedOption: TypeElement?): FileSpec {
for (property in propertyList) {
property.allocateNames(nameAllocator)
}
val result = FileSpec.builder(className.packageName, adapterName)
result.addComment("Code generated by moshi-kotlin-codegen. Do not edit.")
result.addType(generateType(generatedOption))
return result.build()
}
private fun generateType(generatedOption: TypeElement?): TypeSpec {
val result = TypeSpec.classBuilder(adapterName)
.addOriginatingElement(originalElement)
generatedOption?.let {
result.addAnnotation(AnnotationSpec.builder(it.asClassName())
.addMember("value = [%S]", JsonClassCodegenProcessor::class.java.canonicalName)
.addMember("comments = %S", "https://github.com/square/moshi")
.build())
}
result.superclass(jsonAdapterTypeName)
if (typeVariables.isNotEmpty()) {
result.addTypeVariables(typeVariables)
}
// TODO make this configurable. Right now it just matches the source model
if (visibility == Visibility.INTERNAL) {
result.addModifiers(KModifier.INTERNAL)
}
result.primaryConstructor(generateConstructor())
val typeRenderer: TypeRenderer = object : TypeRenderer() {
override fun renderTypeVariable(typeVariable: TypeVariableName): CodeBlock {
val index = typeVariables.indexOfFirst { it == typeVariable }
check(index != -1) { "Unexpected type variable $typeVariable" }
return CodeBlock.of("%N[%L]", typesParam, index)
}
}
result.addProperty(optionsProperty)
for (uniqueAdapter in propertyList.distinctBy { it.delegateKey }) {
result.addProperty(uniqueAdapter.delegateKey.generateProperty(
nameAllocator, typeRenderer, moshiParam, uniqueAdapter.name))
}
result.addFunction(generateToStringFun())
result.addFunction(generateFromJsonFun())
result.addFunction(generateToJsonFun())
return result.build()
}
private fun generateConstructor(): FunSpec {
val result = FunSpec.constructorBuilder()
result.addParameter(moshiParam)
if (typeVariables.isNotEmpty()) {
result.addParameter(typesParam)
}
return result.build()
}
private fun generateToStringFun(): FunSpec {
return FunSpec.builder("toString")
.addModifiers(KModifier.OVERRIDE)
.returns(String::class)
.addStatement("return %S",
"GeneratedJsonAdapter(${originalTypeName.rawType().simpleNames.joinToString(".")})")
.build()
}
private fun jsonDataException(
description: String,
identifier: String,
condition: String,
reader: ParameterSpec
): CodeBlock {
return CodeBlock.of("%T(%T(%S).append(%S).append(%S).append(%N.path).toString())",
JsonDataException::class, StringBuilder::class, description, identifier, condition, reader)
}
private fun generateFromJsonFun(): FunSpec {
val resultName = nameAllocator.newName("result")
val result = FunSpec.builder("fromJson")
.addModifiers(KModifier.OVERRIDE)
.addParameter(readerParam)
.returns(originalTypeName)
for (property in propertyList) {
result.addCode("%L", property.generateLocalProperty())
if (property.differentiateAbsentFromNull) {
result.addCode("%L", property.generateLocalIsPresentProperty())
}
}
result.addStatement("%N.beginObject()", readerParam)
result.beginControlFlow("while (%N.hasNext())", readerParam)
result.beginControlFlow("when (%N.selectName(%N))", readerParam, optionsProperty)
propertyList.forEachIndexed { index, property ->
if (property.differentiateAbsentFromNull) {
result.beginControlFlow("%L -> ", index)
if (property.delegateKey.nullable) {
result.addStatement("%N = %N.fromJson(%N)",
property.localName, nameAllocator[property.delegateKey], readerParam)
} else {
val exception = jsonDataException(
"Non-null value '", property.localName, "' was null at ", readerParam)
result.addStatement("%N = %N.fromJson(%N) ?: throw·%L",
property.localName, nameAllocator[property.delegateKey], readerParam, exception)
}
result.addStatement("%N = true", property.localIsPresentName)
result.endControlFlow()
} else {
if (property.delegateKey.nullable) {
result.addStatement("%L -> %N = %N.fromJson(%N)",
index, property.localName, nameAllocator[property.delegateKey], readerParam)
} else {
val exception = jsonDataException(
"Non-null value '", property.localName, "' was null at ", readerParam)
result.addStatement("%L -> %N = %N.fromJson(%N) ?: throw·%L",
index, property.localName, nameAllocator[property.delegateKey], readerParam,
exception)
}
}
}
result.beginControlFlow("-1 ->")
result.addComment("Unknown name, skip it.")
result.addStatement("%N.skipName()", readerParam)
result.addStatement("%N.skipValue()", readerParam)
result.endControlFlow()
result.endControlFlow() // when
result.endControlFlow() // while
result.addStatement("%N.endObject()", readerParam)
// Call the constructor providing only required parameters.
var hasOptionalParameters = false
result.addCode("«var %N = %T(", resultName, originalTypeName)
var separator = "\n"
for (property in propertyList) {
if (!property.hasConstructorParameter) {
continue
}
if (property.hasDefault) {
hasOptionalParameters = true
continue
}
result.addCode(separator)
result.addCode("%N = %N", property.name, property.localName)
if (property.isRequired) {
result.addCode(" ?: throw·%L", jsonDataException(
"Required property '", property.localName, "' missing at ", readerParam))
}
separator = ",\n"
}
result.addCode("\n", originalTypeName)
// Call either the constructor again, or the copy() method, this time providing any optional
// parameters that we have.
if (hasOptionalParameters) {
if (isDataClass) {
result.addCode("«%1N = %1N.copy(", resultName)
} else {
result.addCode("«%1N = %2T(", resultName, originalTypeName)
}
separator = "\n"
for (property in propertyList) {
if (!property.hasConstructorParameter) {
continue // No constructor parameter for this property.
}
if (isDataClass && !property.hasDefault) {
continue // Property already assigned.
}
result.addCode(separator)
when {
property.differentiateAbsentFromNull -> {
result.addCode("%2N = if (%3N) %4N else %1N.%2N",
resultName, property.name, property.localIsPresentName, property.localName)
}
property.isRequired -> {
result.addCode("%1N = %2N", property.name, property.localName)
}
else -> {
result.addCode("%2N = %3N ?: %1N.%2N", resultName, property.name, property.localName)
}
}
separator = ",\n"
}
result.addCode("»)\n")
}
// Assign properties not present in the constructor.
for (property in propertyList) {
if (property.hasConstructorParameter) {
continue // Property already handled.
}
if (property.differentiateAbsentFromNull) {
result.addStatement("%1N.%2N = if (%3N) %4N else %1N.%2N",
resultName, property.name, property.localIsPresentName, property.localName)
} else {
result.addStatement("%1N.%2N = %3N ?: %1N.%2N",
resultName, property.name, property.localName)
}
}
result.addStatement("return %1N", resultName)
return result.build()
}
private fun generateToJsonFun(): FunSpec {
val result = FunSpec.builder("toJson")
.addModifiers(KModifier.OVERRIDE)
.addParameter(writerParam)
.addParameter(valueParam)
result.beginControlFlow("if (%N == null)", valueParam)
result.addStatement("throw·%T(%S)", NullPointerException::class,
"${valueParam.name} was null! Wrap in .nullSafe() to write nullable values.")
result.endControlFlow()
result.addStatement("%N.beginObject()", writerParam)
propertyList.forEach { property ->
result.addStatement("%N.name(%S)", writerParam, property.jsonName)
result.addStatement("%N.toJson(%N, %N.%L)",
nameAllocator[property.delegateKey], writerParam, valueParam, property.name)
}
result.addStatement("%N.endObject()", writerParam)
return result.build()
}
}

View file

@ -0,0 +1,71 @@
/*
* Copyright (C) 2018 Square, Inc.
*
* 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 com.squareup.moshi.kotlin.codegen
import com.squareup.kotlinpoet.TypeName
import com.squareup.kotlinpoet.TypeVariableName
import com.squareup.kotlinpoet.asTypeName
import javax.lang.model.element.TypeElement
import javax.lang.model.type.DeclaredType
import javax.lang.model.util.Types
/**
* A concrete type like `List<String>` with enough information to know how to resolve its type
* variables.
*/
internal class AppliedType private constructor(
val element: TypeElement,
val resolver: TypeResolver,
private val mirror: DeclaredType
) {
/** Returns all supertypes of this, recursively. Includes both interface and class supertypes. */
fun supertypes(
types: Types,
result: MutableSet<AppliedType> = mutableSetOf()
): Set<AppliedType> {
result.add(this)
for (supertype in types.directSupertypes(mirror)) {
val supertypeDeclaredType = supertype as DeclaredType
val supertypeElement = supertypeDeclaredType.asElement() as TypeElement
val appliedSupertype = AppliedType(supertypeElement,
resolver(supertypeElement, supertypeDeclaredType), supertypeDeclaredType)
appliedSupertype.supertypes(types, result)
}
return result
}
/** Returns a resolver that uses `element` and `mirror` to resolve type parameters. */
private fun resolver(element: TypeElement, mirror: DeclaredType): TypeResolver {
return object : TypeResolver() {
override fun resolveTypeVariable(typeVariable: TypeVariableName): TypeName {
val index = element.typeParameters.indexOfFirst {
it.simpleName.toString() == typeVariable.name
}
check(index != -1) { "Unexpected type variable $typeVariable in $mirror" }
val argument = mirror.typeArguments[index]
return argument.asTypeName()
}
}
}
override fun toString() = mirror.toString()
companion object {
fun get(typeElement: TypeElement): AppliedType {
return AppliedType(typeElement, TypeResolver(), typeElement.asType() as DeclaredType)
}
}
}

View file

@ -0,0 +1,97 @@
/*
* Copyright (C) 2018 Square, Inc.
*
* 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 com.squareup.moshi.kotlin.codegen
import com.squareup.kotlinpoet.AnnotationSpec
import com.squareup.kotlinpoet.ClassName
import com.squareup.kotlinpoet.CodeBlock
import com.squareup.kotlinpoet.KModifier
import com.squareup.kotlinpoet.MemberName
import com.squareup.kotlinpoet.NameAllocator
import com.squareup.kotlinpoet.ParameterSpec
import com.squareup.kotlinpoet.ParameterizedTypeName
import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy
import com.squareup.kotlinpoet.PropertySpec
import com.squareup.kotlinpoet.TypeName
import com.squareup.kotlinpoet.TypeVariableName
import com.squareup.kotlinpoet.WildcardTypeName
import com.squareup.kotlinpoet.asClassName
import com.squareup.kotlinpoet.asTypeName
import com.squareup.moshi.JsonAdapter
import com.squareup.moshi.Types
/** A JsonAdapter that can be used to encode and decode a particular field. */
internal data class DelegateKey(
private val type: TypeName,
private val jsonQualifiers: List<AnnotationSpec>
) {
val nullable get() = type.isNullable
/** Returns an adapter to use when encoding and decoding this property. */
fun generateProperty(
nameAllocator: NameAllocator,
typeRenderer: TypeRenderer,
moshiParameter: ParameterSpec,
propertyName: String
): PropertySpec {
val qualifierNames = jsonQualifiers.joinToString("") {
"At${it.className.simpleName}"
}
val adapterName = nameAllocator.newName(
"${type.toVariableName().decapitalize()}${qualifierNames}Adapter", this)
val adapterTypeName = JsonAdapter::class.asClassName().parameterizedBy(type)
val standardArgs = arrayOf(moshiParameter,
CodeBlock.of("<%T>", type),
typeRenderer.render(type))
val (initializerString, args) = when {
jsonQualifiers.isEmpty() -> ", %M()" to arrayOf(MemberName("kotlin.collections", "emptySet"))
else -> {
", %T.getFieldJsonQualifierAnnotations(javaClass, " +
"%S)" to arrayOf(Types::class.asTypeName(), adapterName)
}
}
val finalArgs = arrayOf(*standardArgs, *args, propertyName)
return PropertySpec.builder(adapterName, adapterTypeName, KModifier.PRIVATE)
.addAnnotations(jsonQualifiers)
.initializer("%N.adapter%L(%L$initializerString, %S)", *finalArgs)
.build()
}
}
/**
* Returns a suggested variable name derived from a list of type names. This just concatenates,
* yielding types like MapOfStringLong.
*/
private fun List<TypeName>.toVariableNames() = joinToString("") { it.toVariableName() }
/** Returns a suggested variable name derived from a type name, like nullableListOfString. */
private fun TypeName.toVariableName(): String {
val base = when (this) {
is ClassName -> simpleName
is ParameterizedTypeName -> rawType.simpleName + "Of" + typeArguments.toVariableNames()
is WildcardTypeName -> (inTypes + outTypes).toVariableNames()
is TypeVariableName -> name + bounds.toVariableNames()
else -> throw IllegalArgumentException("Unrecognized type! $this")
}
return if (isNullable) {
"Nullable$base"
} else {
base
}
}

View file

@ -0,0 +1,128 @@
/*
* Copyright (C) 2018 Square, Inc.
*
* 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 com.squareup.moshi.kotlin.codegen
import com.google.auto.service.AutoService
import com.squareup.moshi.JsonClass
import me.eugeniomarletti.kotlin.metadata.KotlinMetadataUtils
import me.eugeniomarletti.kotlin.metadata.declaresDefaultValue
import me.eugeniomarletti.kotlin.processing.KotlinAbstractProcessor
import net.ltgt.gradle.incap.IncrementalAnnotationProcessor
import net.ltgt.gradle.incap.IncrementalAnnotationProcessorType.ISOLATING
import javax.annotation.processing.ProcessingEnvironment
import javax.annotation.processing.Processor
import javax.annotation.processing.RoundEnvironment
import javax.lang.model.SourceVersion
import javax.lang.model.element.Element
import javax.lang.model.element.TypeElement
import javax.tools.Diagnostic.Kind.ERROR
/**
* An annotation processor that reads Kotlin data classes and generates Moshi JsonAdapters for them.
* This generates Kotlin code, and understands basic Kotlin language features like default values
* and companion objects.
*
* The generated class will match the visibility of the given data class (i.e. if it's internal, the
* adapter will also be internal).
*
* If you define a companion object, a jsonAdapter() extension function will be generated onto it.
* If you don't want this though, you can use the runtime [JsonClass] factory implementation.
*/
@AutoService(Processor::class)
@IncrementalAnnotationProcessor(ISOLATING)
class JsonClassCodegenProcessor : KotlinAbstractProcessor(), KotlinMetadataUtils {
companion object {
/**
* This annotation processing argument can be specified to have a `@Generated` annotation
* included in the generated code. It is not encouraged unless you need it for static analysis
* reasons and not enabled by default.
*
* Note that this can only be one of the following values:
* * `"javax.annotation.processing.Generated"` (JRE 9+)
* * `"javax.annotation.Generated"` (JRE <9)
*/
const val OPTION_GENERATED = "moshi.generated"
private val POSSIBLE_GENERATED_NAMES = setOf(
"javax.annotation.processing.Generated",
"javax.annotation.Generated"
)
}
private val annotation = JsonClass::class.java
private var generatedType: TypeElement? = null
override fun getSupportedAnnotationTypes() = setOf(annotation.canonicalName)
override fun getSupportedSourceVersion(): SourceVersion = SourceVersion.latest()
override fun getSupportedOptions() = setOf(OPTION_GENERATED)
override fun init(processingEnv: ProcessingEnvironment) {
super.init(processingEnv)
generatedType = processingEnv.options[OPTION_GENERATED]?.let {
if (it !in POSSIBLE_GENERATED_NAMES) {
throw IllegalArgumentException("Invalid option value for $OPTION_GENERATED. Found $it, " +
"allowable values are $POSSIBLE_GENERATED_NAMES.")
}
processingEnv.elementUtils.getTypeElement(it)
}
}
override fun process(annotations: Set<TypeElement>, roundEnv: RoundEnvironment): Boolean {
for (type in roundEnv.getElementsAnnotatedWith(annotation)) {
val jsonClass = type.getAnnotation(annotation)
if (jsonClass.generateAdapter && jsonClass.generator.isEmpty()) {
val generator = adapterGenerator(type) ?: continue
generator.generateFile(generatedType)
.writeTo(filer)
}
}
return false
}
private fun adapterGenerator(element: Element): AdapterGenerator? {
val type = TargetType.get(messager, elementUtils, typeUtils, element) ?: return null
val properties = mutableMapOf<String, PropertyGenerator>()
for (property in type.properties.values) {
val generator = property.generator(messager)
if (generator != null) {
properties[property.name] = generator
}
}
for ((name, parameter) in type.constructor.parameters) {
if (type.properties[parameter.name] == null && !parameter.proto.declaresDefaultValue) {
messager.printMessage(
ERROR, "No property for required constructor parameter $name", parameter.element)
return null
}
}
// Sort properties so that those with constructor parameters come first.
val sortedProperties = properties.values.sortedBy {
if (it.hasConstructorParameter) {
it.target.parameterIndex
} else {
Integer.MAX_VALUE
}
}
return AdapterGenerator(type, sortedProperties)
}
}

View file

@ -0,0 +1,59 @@
/*
* Copyright (C) 2018 Square, Inc.
*
* 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 com.squareup.moshi.kotlin.codegen
import com.squareup.kotlinpoet.BOOLEAN
import com.squareup.kotlinpoet.NameAllocator
import com.squareup.kotlinpoet.PropertySpec
/** Generates functions to encode and decode a property as JSON. */
internal class PropertyGenerator(
val target: TargetProperty,
val delegateKey: DelegateKey
) {
val name = target.name
val jsonName = target.jsonName()
val hasDefault = target.hasDefault
lateinit var localName: String
lateinit var localIsPresentName: String
val isRequired get() = !delegateKey.nullable && !hasDefault
val hasConstructorParameter get() = target.parameterIndex != -1
/** We prefer to use 'null' to mean absent, but for some properties those are distinct. */
val differentiateAbsentFromNull get() = delegateKey.nullable && hasDefault
fun allocateNames(nameAllocator: NameAllocator) {
localName = nameAllocator.newName(name)
localIsPresentName = nameAllocator.newName("${name}Set")
}
fun generateLocalProperty(): PropertySpec {
return PropertySpec.builder(localName, target.type.copy(nullable = true))
.mutable(true)
.initializer("null")
.build()
}
fun generateLocalIsPresentProperty(): PropertySpec {
return PropertySpec.builder(localIsPresentName, BOOLEAN)
.mutable(true)
.initializer("false")
.build()
}
}

View file

@ -0,0 +1,63 @@
/*
* Copyright (C) 2018 Square, Inc.
*
* 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 com.squareup.moshi.kotlin.codegen
import me.eugeniomarletti.kotlin.metadata.KotlinClassMetadata
import me.eugeniomarletti.kotlin.metadata.isPrimary
import me.eugeniomarletti.kotlin.metadata.jvm.getJvmConstructorSignature
import me.eugeniomarletti.kotlin.metadata.shadow.metadata.ProtoBuf.Constructor
import javax.lang.model.element.ElementKind
import javax.lang.model.element.ExecutableElement
import javax.lang.model.util.Elements
/** A constructor in user code that should be called by generated code. */
internal data class TargetConstructor(
val element: ExecutableElement,
val proto: Constructor,
val parameters: Map<String, TargetParameter>
) {
companion object {
fun primary(metadata: KotlinClassMetadata, elements: Elements): TargetConstructor {
val (nameResolver, classProto) = metadata.data
// todo allow custom constructor
val proto = classProto.constructorList
.single { it.isPrimary }
val constructorJvmSignature = proto.getJvmConstructorSignature(
nameResolver, classProto.typeTable)
val element = classProto.fqName
.let(nameResolver::getString)
.replace('/', '.')
.let(elements::getTypeElement)
.enclosedElements
.mapNotNull {
it.takeIf { it.kind == ElementKind.CONSTRUCTOR }?.let { it as ExecutableElement }
}
.first()
// TODO Temporary until JVM method signature matching is better
// .single { it.jvmMethodSignature == constructorJvmSignature }
val parameters = mutableMapOf<String, TargetParameter>()
for (parameter in proto.valueParameterList) {
val name = nameResolver.getString(parameter.name)
val index = proto.valueParameterList.indexOf(parameter)
parameters[name] = TargetParameter(name, parameter, index, element.parameters[index])
}
return TargetConstructor(element, proto, parameters)
}
}
}

View file

@ -0,0 +1,27 @@
/*
* Copyright (C) 2018 Square, Inc.
*
* 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 com.squareup.moshi.kotlin.codegen
import me.eugeniomarletti.kotlin.metadata.shadow.metadata.ProtoBuf.ValueParameter
import javax.lang.model.element.VariableElement
/** A parameter in user code that should be populated by generated code. */
internal data class TargetParameter(
val name: String,
val proto: ValueParameter,
val index: Int,
val element: VariableElement
)

View file

@ -0,0 +1,167 @@
/*
* Copyright (C) 2018 Square, Inc.
*
* 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 com.squareup.moshi.kotlin.codegen
import com.google.auto.common.AnnotationMirrors
import com.google.auto.common.MoreTypes
import com.squareup.kotlinpoet.AnnotationSpec
import com.squareup.kotlinpoet.TypeName
import com.squareup.moshi.Json
import com.squareup.moshi.JsonQualifier
import me.eugeniomarletti.kotlin.metadata.declaresDefaultValue
import me.eugeniomarletti.kotlin.metadata.hasSetter
import me.eugeniomarletti.kotlin.metadata.shadow.metadata.ProtoBuf.Property
import me.eugeniomarletti.kotlin.metadata.shadow.metadata.ProtoBuf.Visibility.INTERNAL
import me.eugeniomarletti.kotlin.metadata.shadow.metadata.ProtoBuf.Visibility.PROTECTED
import me.eugeniomarletti.kotlin.metadata.shadow.metadata.ProtoBuf.Visibility.PUBLIC
import me.eugeniomarletti.kotlin.metadata.visibility
import java.lang.annotation.ElementType
import java.lang.annotation.Retention
import java.lang.annotation.RetentionPolicy
import java.lang.annotation.Target
import javax.annotation.processing.Messager
import javax.lang.model.element.AnnotationMirror
import javax.lang.model.element.Element
import javax.lang.model.element.ExecutableElement
import javax.lang.model.element.Modifier
import javax.lang.model.element.Name
import javax.lang.model.element.VariableElement
import javax.tools.Diagnostic
/** A property in user code that maps to JSON. */
internal data class TargetProperty(
val name: String,
val type: TypeName,
private val proto: Property,
private val parameter: TargetParameter?,
private val annotationHolder: ExecutableElement?,
private val field: VariableElement?,
private val setter: ExecutableElement?,
private val getter: ExecutableElement?
) {
val parameterIndex get() = parameter?.index ?: -1
val hasDefault get() = parameter?.proto?.declaresDefaultValue ?: true
private val isTransient get() = field != null && Modifier.TRANSIENT in field.modifiers
private val element get() = field ?: setter ?: getter!!
private val isSettable get() = proto.hasSetter || parameter != null
private val isVisible: Boolean
get() {
return proto.visibility == INTERNAL
|| proto.visibility == PROTECTED
|| proto.visibility == PUBLIC
}
/**
* Returns a generator for this property, or null if either there is an error and this property
* cannot be used with code gen, or if no codegen is necessary for this property.
*/
fun generator(messager: Messager): PropertyGenerator? {
if (isTransient) {
if (!hasDefault) {
messager.printMessage(
Diagnostic.Kind.ERROR, "No default value for transient property ${this}", element)
return null
}
return null // This property is transient and has a default value. Ignore it.
}
if (!isVisible) {
messager.printMessage(Diagnostic.Kind.ERROR, "property ${this} is not visible", element)
return null
}
if (!isSettable) {
return null // This property is not settable. Ignore it.
}
val jsonQualifierMirrors = jsonQualifiers()
for (jsonQualifier in jsonQualifierMirrors) {
// Check Java types since that covers both Java and Kotlin annotations.
val annotationElement = MoreTypes.asTypeElement(jsonQualifier.annotationType)
annotationElement.getAnnotation(Retention::class.java)?.let {
if (it.value != RetentionPolicy.RUNTIME) {
messager.printMessage(Diagnostic.Kind.ERROR,
"JsonQualifier @${jsonQualifier.simpleName} must have RUNTIME retention")
}
}
annotationElement.getAnnotation(Target::class.java)?.let {
if (ElementType.FIELD !in it.value) {
messager.printMessage(Diagnostic.Kind.ERROR,
"JsonQualifier @${jsonQualifier.simpleName} must support FIELD target")
}
}
}
val jsonQualifierSpecs = jsonQualifierMirrors.map {
AnnotationSpec.get(it).toBuilder()
.useSiteTarget(AnnotationSpec.UseSiteTarget.FIELD)
.build()
}
return PropertyGenerator(this, DelegateKey(type, jsonQualifierSpecs))
}
/** Returns the JsonQualifiers on the field and parameter of this property. */
private fun jsonQualifiers(): Set<AnnotationMirror> {
val elementQualifiers = element.qualifiers
val annotationHolderQualifiers = annotationHolder.qualifiers
val parameterQualifiers = parameter?.element.qualifiers
// TODO(jwilson): union the qualifiers somehow?
return when {
elementQualifiers.isNotEmpty() -> elementQualifiers
annotationHolderQualifiers.isNotEmpty() -> annotationHolderQualifiers
parameterQualifiers.isNotEmpty() -> parameterQualifiers
else -> setOf()
}
}
private val Element?.qualifiers: Set<AnnotationMirror>
get() {
if (this == null) return setOf()
return AnnotationMirrors.getAnnotatedAnnotations(this, JsonQualifier::class.java)
}
/** Returns the @Json name of this property, or this property's name if none is provided. */
fun jsonName(): String {
val fieldJsonName = element.jsonName
val annotationHolderJsonName = annotationHolder.jsonName
val parameterJsonName = parameter?.element.jsonName
return when {
fieldJsonName != null -> fieldJsonName
annotationHolderJsonName != null -> annotationHolderJsonName
parameterJsonName != null -> parameterJsonName
else -> name
}
}
private val Element?.jsonName: String?
get() {
if (this == null) return null
return getAnnotation(Json::class.java)?.name
}
private val AnnotationMirror.simpleName: Name
get() = MoreTypes.asTypeElement(annotationType).simpleName!!
override fun toString() = name
}

View file

@ -0,0 +1,229 @@
/*
* Copyright (C) 2018 Square, Inc.
*
* 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 com.squareup.moshi.kotlin.codegen
import com.squareup.kotlinpoet.ClassName
import com.squareup.kotlinpoet.KModifier
import com.squareup.kotlinpoet.ParameterizedTypeName
import com.squareup.kotlinpoet.TypeVariableName
import com.squareup.kotlinpoet.asClassName
import com.squareup.kotlinpoet.asTypeName
import me.eugeniomarletti.kotlin.metadata.KotlinClassMetadata
import me.eugeniomarletti.kotlin.metadata.KotlinMetadata
import me.eugeniomarletti.kotlin.metadata.classKind
import me.eugeniomarletti.kotlin.metadata.getPropertyOrNull
import me.eugeniomarletti.kotlin.metadata.isInnerClass
import me.eugeniomarletti.kotlin.metadata.kotlinMetadata
import me.eugeniomarletti.kotlin.metadata.modality
import me.eugeniomarletti.kotlin.metadata.shadow.metadata.ProtoBuf.Class
import me.eugeniomarletti.kotlin.metadata.shadow.metadata.ProtoBuf.Modality.ABSTRACT
import me.eugeniomarletti.kotlin.metadata.shadow.metadata.ProtoBuf.TypeParameter
import me.eugeniomarletti.kotlin.metadata.shadow.metadata.ProtoBuf.Visibility.INTERNAL
import me.eugeniomarletti.kotlin.metadata.shadow.metadata.ProtoBuf.Visibility.LOCAL
import me.eugeniomarletti.kotlin.metadata.shadow.metadata.ProtoBuf.Visibility.PUBLIC
import me.eugeniomarletti.kotlin.metadata.shadow.metadata.deserialization.NameResolver
import me.eugeniomarletti.kotlin.metadata.shadow.util.capitalizeDecapitalize.decapitalizeAsciiOnly
import me.eugeniomarletti.kotlin.metadata.visibility
import javax.annotation.processing.Messager
import javax.lang.model.element.Element
import javax.lang.model.element.ElementKind
import javax.lang.model.element.ExecutableElement
import javax.lang.model.element.TypeElement
import javax.lang.model.element.VariableElement
import javax.lang.model.util.Elements
import javax.lang.model.util.Types
import javax.tools.Diagnostic.Kind.ERROR
/** A user type that should be decoded and encoded by generated code. */
internal data class TargetType(
val proto: Class,
val element: TypeElement,
val constructor: TargetConstructor,
val properties: Map<String, TargetProperty>,
val typeVariables: List<TypeVariableName>
) {
val name = element.className
companion object {
private val OBJECT_CLASS = ClassName("java.lang", "Object")
/** Returns a target type for `element`, or null if it cannot be used with code gen. */
fun get(messager: Messager, elements: Elements, types: Types, element: Element): TargetType? {
val typeMetadata: KotlinMetadata? = element.kotlinMetadata
if (element !is TypeElement || typeMetadata !is KotlinClassMetadata) {
messager.printMessage(
ERROR, "@JsonClass can't be applied to $element: must be a Kotlin class", element)
return null
}
val proto = typeMetadata.data.classProto
when {
proto.classKind == Class.Kind.ENUM_CLASS -> {
messager.printMessage(
ERROR, "@JsonClass with 'generateAdapter = \"true\"' can't be applied to $element: code gen for enums is not supported or necessary", element)
return null
}
proto.classKind != Class.Kind.CLASS -> {
messager.printMessage(
ERROR, "@JsonClass can't be applied to $element: must be a Kotlin class", element)
return null
}
proto.isInnerClass -> {
messager.printMessage(
ERROR, "@JsonClass can't be applied to $element: must not be an inner class", element)
return null
}
proto.modality == ABSTRACT -> {
messager.printMessage(
ERROR, "@JsonClass can't be applied to $element: must not be abstract", element)
return null
}
proto.visibility == LOCAL -> {
messager.printMessage(
ERROR, "@JsonClass can't be applied to $element: must not be local", element)
return null
}
}
val typeVariables = genericTypeNames(proto, typeMetadata.data.nameResolver)
val appliedType = AppliedType.get(element)
val constructor = TargetConstructor.primary(typeMetadata, elements)
if (constructor.proto.visibility != INTERNAL && constructor.proto.visibility != PUBLIC) {
messager.printMessage(ERROR, "@JsonClass can't be applied to $element: " +
"primary constructor is not internal or public", element)
return null
}
val properties = mutableMapOf<String, TargetProperty>()
for (supertype in appliedType.supertypes(types)) {
if (supertype.element.asClassName() == OBJECT_CLASS) {
continue // Don't load properties for java.lang.Object.
}
if (supertype.element.kind != ElementKind.CLASS) {
continue // Don't load properties for interface types.
}
if (supertype.element.kotlinMetadata == null) {
messager.printMessage(ERROR,
"@JsonClass can't be applied to $element: supertype $supertype is not a Kotlin type",
element)
return null
}
val supertypeProperties = declaredProperties(
supertype.element, supertype.resolver, constructor)
for ((name, property) in supertypeProperties) {
properties.putIfAbsent(name, property)
}
}
return TargetType(proto, element, constructor, properties, typeVariables)
}
/** Returns the properties declared by `typeElement`. */
private fun declaredProperties(
typeElement: TypeElement,
typeResolver: TypeResolver,
constructor: TargetConstructor
): Map<String, TargetProperty> {
val typeMetadata: KotlinClassMetadata = typeElement.kotlinMetadata as KotlinClassMetadata
val nameResolver = typeMetadata.data.nameResolver
val classProto = typeMetadata.data.classProto
val annotationHolders = mutableMapOf<String, ExecutableElement>()
val fields = mutableMapOf<String, VariableElement>()
val setters = mutableMapOf<String, ExecutableElement>()
val getters = mutableMapOf<String, ExecutableElement>()
for (element in typeElement.enclosedElements) {
if (element is VariableElement) {
fields[element.name] = element
} else if (element is ExecutableElement) {
when {
element.name.startsWith("get") -> {
val name = element.name.substring("get".length).decapitalizeAsciiOnly()
getters[name] = element
}
element.name.startsWith("is") -> {
val name = element.name.substring("is".length).decapitalizeAsciiOnly()
getters[name] = element
}
element.name.startsWith("set") -> {
val name = element.name.substring("set".length).decapitalizeAsciiOnly()
setters[name] = element
}
}
val propertyProto = typeMetadata.data.getPropertyOrNull(element)
if (propertyProto != null) {
val name = nameResolver.getString(propertyProto.name)
annotationHolders[name] = element
}
}
}
val result = mutableMapOf<String, TargetProperty>()
for (property in classProto.propertyList) {
val name = nameResolver.getString(property.name)
val type = typeResolver.resolve(property.returnType.asTypeName(
nameResolver, classProto::getTypeParameter, false))
result[name] = TargetProperty(name, type, property, constructor.parameters[name],
annotationHolders[name], fields[name], setters[name], getters[name])
}
return result
}
private val Element.className: ClassName
get() {
val typeName = asType().asTypeName()
return when (typeName) {
is ClassName -> typeName
is ParameterizedTypeName -> typeName.rawType
else -> throw IllegalStateException("unexpected TypeName: ${typeName::class}")
}
}
private val Element.name get() = simpleName.toString()
private fun genericTypeNames(proto: Class, nameResolver: NameResolver): List<TypeVariableName> {
return proto.typeParameterList.map {
val possibleBounds = it.upperBoundList
.map { it.asTypeName(nameResolver, proto::getTypeParameter, false) }
val typeVar = if (possibleBounds.isEmpty()) {
TypeVariableName(
name = nameResolver.getString(it.name),
variance = it.varianceModifier)
} else {
TypeVariableName(
name = nameResolver.getString(it.name),
bounds = *possibleBounds.toTypedArray(),
variance = it.varianceModifier)
}
return@map typeVar.copy(reified = it.reified)
}
}
private val TypeParameter.varianceModifier: KModifier?
get() {
return variance.asKModifier().let {
// We don't redeclare out variance here
if (it == KModifier.OUT) {
null
} else {
it
}
}
}
}
}

View file

@ -0,0 +1,114 @@
/*
* Copyright (C) 2018 Square, Inc.
*
* 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 com.squareup.moshi.kotlin.codegen
import com.squareup.kotlinpoet.ARRAY
import com.squareup.kotlinpoet.BOOLEAN
import com.squareup.kotlinpoet.BYTE
import com.squareup.kotlinpoet.CHAR
import com.squareup.kotlinpoet.ClassName
import com.squareup.kotlinpoet.CodeBlock
import com.squareup.kotlinpoet.DOUBLE
import com.squareup.kotlinpoet.FLOAT
import com.squareup.kotlinpoet.INT
import com.squareup.kotlinpoet.LONG
import com.squareup.kotlinpoet.ParameterizedTypeName
import com.squareup.kotlinpoet.SHORT
import com.squareup.kotlinpoet.TypeName
import com.squareup.kotlinpoet.TypeVariableName
import com.squareup.kotlinpoet.WildcardTypeName
import com.squareup.moshi.Types
/**
* Renders literals like `Types.newParameterizedType(List::class.java, String::class.java)`.
* Rendering is pluggable so that type variables can either be resolved or emitted as other code
* blocks.
*/
abstract class TypeRenderer {
abstract fun renderTypeVariable(typeVariable: TypeVariableName): CodeBlock
fun render(typeName: TypeName): CodeBlock {
if (typeName.isNullable) {
return renderObjectType(typeName.copy(nullable = false))
}
return when (typeName) {
is ClassName -> CodeBlock.of("%T::class.java", typeName)
is ParameterizedTypeName -> {
// If it's an Array type, we shortcut this to return Types.arrayOf()
if (typeName.rawType == ARRAY) {
CodeBlock.of("%T.arrayOf(%L)",
Types::class,
renderObjectType(typeName.typeArguments[0]))
} else {
val builder = CodeBlock.builder().apply {
add("%T.", Types::class)
val enclosingClassName = typeName.rawType.enclosingClassName()
if (enclosingClassName != null) {
add("newParameterizedTypeWithOwner(%L, ", render(enclosingClassName))
} else {
add("newParameterizedType(")
}
add("%T::class.java", typeName.rawType)
for (typeArgument in typeName.typeArguments) {
add(", %L", renderObjectType(typeArgument))
}
add(")")
}
builder.build()
}
}
is WildcardTypeName -> {
val target: TypeName
val method: String
when {
typeName.inTypes.size == 1 -> {
target = typeName.inTypes[0]
method = "supertypeOf"
}
typeName.outTypes.size == 1 -> {
target = typeName.outTypes[0]
method = "subtypeOf"
}
else -> throw IllegalArgumentException(
"Unrepresentable wildcard type. Cannot have more than one bound: $typeName")
}
CodeBlock.of("%T.%L(%T::class.java)", Types::class, method, target.copy(nullable = false))
}
is TypeVariableName -> renderTypeVariable(typeName)
else -> throw IllegalArgumentException("Unrepresentable type: $typeName")
}
}
private fun renderObjectType(typeName: TypeName): CodeBlock {
return if (typeName.isPrimitive()) {
CodeBlock.of("%T::class.javaObjectType", typeName)
} else {
render(typeName)
}
}
private fun TypeName.isPrimitive(): Boolean {
return when (this) {
BOOLEAN, BYTE, SHORT, INT, LONG, CHAR, FLOAT, DOUBLE -> true
else -> false
}
}
}

View file

@ -0,0 +1,63 @@
/*
* Copyright (C) 2018 Square, Inc.
*
* 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 com.squareup.moshi.kotlin.codegen
import com.squareup.kotlinpoet.ClassName
import com.squareup.kotlinpoet.ParameterizedTypeName
import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy
import com.squareup.kotlinpoet.TypeName
import com.squareup.kotlinpoet.TypeVariableName
import com.squareup.kotlinpoet.WildcardTypeName
/**
* Resolves type parameters against a type declaration. Use this to fill in type variables with
* their actual type parameters.
*/
open class TypeResolver {
open fun resolveTypeVariable(typeVariable: TypeVariableName): TypeName = typeVariable
fun resolve(typeName: TypeName): TypeName {
return when (typeName) {
is ClassName -> typeName
is ParameterizedTypeName -> {
typeName.rawType.parameterizedBy(*(typeName.typeArguments.map { resolve(it) }.toTypedArray()))
.copy(nullable = typeName.isNullable)
}
is WildcardTypeName -> {
when {
typeName.inTypes.size == 1 -> {
WildcardTypeName.consumerOf(resolve(typeName.inTypes[0]))
.copy(nullable = typeName.isNullable)
}
typeName.outTypes.size == 1 -> {
WildcardTypeName.producerOf(resolve(typeName.outTypes[0]))
.copy(nullable = typeName.isNullable)
}
else -> {
throw IllegalArgumentException(
"Unrepresentable wildcard type. Cannot have more than one bound: $typeName")
}
}
}
is TypeVariableName -> resolveTypeVariable(typeName)
else -> throw IllegalArgumentException("Unrepresentable type: $typeName")
}
}
}

View file

@ -0,0 +1,28 @@
/*
* Copyright (C) 2018 Square, Inc.
*
* 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 com.squareup.moshi.kotlin.codegen
import com.squareup.kotlinpoet.ClassName
import com.squareup.kotlinpoet.ParameterizedTypeName
import com.squareup.kotlinpoet.TypeName
internal fun TypeName.rawType(): ClassName {
return when (this) {
is ClassName -> this
is ParameterizedTypeName -> rawType
else -> throw IllegalArgumentException("Cannot get raw type from $this")
}
}

View file

@ -0,0 +1,125 @@
/*
* Copyright (C) 2018 Square, Inc.
*
* 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 com.squareup.moshi.kotlin.codegen
import com.squareup.kotlinpoet.ClassName
import com.squareup.kotlinpoet.KModifier
import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy
import com.squareup.kotlinpoet.STAR
import com.squareup.kotlinpoet.TypeName
import com.squareup.kotlinpoet.TypeVariableName
import com.squareup.kotlinpoet.WildcardTypeName
import me.eugeniomarletti.kotlin.metadata.shadow.metadata.ProtoBuf.Type
import me.eugeniomarletti.kotlin.metadata.shadow.metadata.ProtoBuf.TypeParameter
import me.eugeniomarletti.kotlin.metadata.shadow.metadata.ProtoBuf.TypeParameter.Variance
import me.eugeniomarletti.kotlin.metadata.shadow.metadata.deserialization.NameResolver
internal fun TypeParameter.asTypeName(
nameResolver: NameResolver,
getTypeParameter: (index: Int) -> TypeParameter,
resolveAliases: Boolean = false
): TypeVariableName {
val possibleBounds = upperBoundList.map {
it.asTypeName(nameResolver, getTypeParameter, resolveAliases)
}
return if (possibleBounds.isEmpty()) {
TypeVariableName(
name = nameResolver.getString(name),
variance = variance.asKModifier())
} else {
TypeVariableName(
name = nameResolver.getString(name),
bounds = *possibleBounds.toTypedArray(),
variance = variance.asKModifier())
}
}
internal fun TypeParameter.Variance.asKModifier(): KModifier? {
return when (this) {
Variance.IN -> KModifier.IN
Variance.OUT -> KModifier.OUT
Variance.INV -> null
}
}
/**
* Returns the TypeName of this type as it would be seen in the source code, including nullability
* and generic type parameters.
*
* @param [nameResolver] a [NameResolver] instance from the source proto
* @param [getTypeParameter] a function that returns the type parameter for the given index. **Only
* called if [ProtoBuf.Type.hasTypeParameter] is true!**
*/
internal fun Type.asTypeName(
nameResolver: NameResolver,
getTypeParameter: (index: Int) -> TypeParameter,
useAbbreviatedType: Boolean = true
): TypeName {
val argumentList = when {
useAbbreviatedType && hasAbbreviatedType() -> abbreviatedType.argumentList
else -> argumentList
}
if (hasFlexibleUpperBound()) {
return WildcardTypeName.producerOf(
flexibleUpperBound.asTypeName(nameResolver, getTypeParameter, useAbbreviatedType))
.copy(nullable = nullable)
} else if (hasOuterType()) {
return WildcardTypeName.consumerOf(
outerType.asTypeName(nameResolver, getTypeParameter, useAbbreviatedType))
.copy(nullable = nullable)
}
val realType = when {
hasTypeParameter() -> return getTypeParameter(typeParameter)
.asTypeName(nameResolver, getTypeParameter, useAbbreviatedType)
.copy(nullable = nullable)
hasTypeParameterName() -> typeParameterName
useAbbreviatedType && hasAbbreviatedType() -> abbreviatedType.typeAliasName
else -> className
}
var typeName: TypeName =
ClassName.bestGuess(nameResolver.getString(realType)
.replace("/", "."))
if (argumentList.isNotEmpty()) {
val remappedArgs: Array<TypeName> = argumentList.map { argumentType ->
val nullableProjection = if (argumentType.hasProjection()) {
argumentType.projection
} else null
if (argumentType.hasType()) {
argumentType.type.asTypeName(nameResolver, getTypeParameter, useAbbreviatedType)
.let { argumentTypeName ->
nullableProjection?.let { projection ->
when (projection) {
Type.Argument.Projection.IN -> WildcardTypeName.consumerOf(argumentTypeName)
Type.Argument.Projection.OUT -> WildcardTypeName.producerOf(argumentTypeName)
Type.Argument.Projection.STAR -> STAR
Type.Argument.Projection.INV -> TODO("INV projection is unsupported")
}
} ?: argumentTypeName
}
} else {
STAR
}
}.toTypedArray()
typeName = (typeName as ClassName).parameterizedBy(*remappedArgs)
}
return typeName.copy(nullable = nullable)
}

View file

@ -0,0 +1,21 @@
/*
* Copyright (C) 2018 Square, Inc.
*
* 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 com.squareup.moshi.kotlin.codegen;
/** For {@link JsonClassCodegenProcessorTest#extendJavaType}. */
public class JavaSuperclass {
public int a = 1;
}

View file

@ -0,0 +1,381 @@
/*
* Copyright (C) 2018 Square, Inc.
*
* 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 com.squareup.moshi.kotlin.codegen
import org.assertj.core.api.Assertions.assertThat
import org.jetbrains.kotlin.cli.common.ExitCode
import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TemporaryFolder
import javax.annotation.processing.Processor
/** Execute kotlinc to confirm that either files are generated or errors are printed. */
class JsonClassCodegenProcessorTest {
@Rule @JvmField var temporaryFolder: TemporaryFolder = TemporaryFolder()
@Test fun privateConstructor() {
val call = KotlinCompilerCall(temporaryFolder.root)
call.inheritClasspath = true
call.addService(Processor::class, JsonClassCodegenProcessor::class)
call.addKt("source.kt", """
|import com.squareup.moshi.JsonClass
|
|@JsonClass(generateAdapter = true)
|class PrivateConstructor private constructor(var a: Int, var b: Int) {
| fun a() = a
| fun b() = b
| companion object {
| fun newInstance(a: Int, b: Int) = PrivateConstructor(a, b)
| }
|}
|""".trimMargin())
val result = call.execute()
assertThat(result.exitCode).isEqualTo(ExitCode.COMPILATION_ERROR)
assertThat(result.systemErr).contains("constructor is not internal or public")
}
@Test fun privateConstructorParameter() {
val call = KotlinCompilerCall(temporaryFolder.root)
call.inheritClasspath = true
call.addService(Processor::class, JsonClassCodegenProcessor::class)
call.addKt("source.kt", """
|import com.squareup.moshi.JsonClass
|
|@JsonClass(generateAdapter = true)
|class PrivateConstructorParameter(private var a: Int)
|""".trimMargin())
val result = call.execute()
assertThat(result.exitCode).isEqualTo(ExitCode.COMPILATION_ERROR)
assertThat(result.systemErr).contains("property a is not visible")
}
@Test fun privateProperties() {
val call = KotlinCompilerCall(temporaryFolder.root)
call.inheritClasspath = true
call.addService(Processor::class, JsonClassCodegenProcessor::class)
call.addKt("source.kt", """
|import com.squareup.moshi.JsonClass
|
|@JsonClass(generateAdapter = true)
|class PrivateProperties {
| private var a: Int = -1
| private var b: Int = -1
|}
|""".trimMargin())
val result = call.execute()
assertThat(result.exitCode).isEqualTo(ExitCode.COMPILATION_ERROR)
assertThat(result.systemErr).contains("property a is not visible")
}
@Test fun interfacesNotSupported() {
val call = KotlinCompilerCall(temporaryFolder.root)
call.inheritClasspath = true
call.addService(Processor::class, JsonClassCodegenProcessor::class)
call.addKt("source.kt", """
|import com.squareup.moshi.JsonClass
|
|@JsonClass(generateAdapter = true)
|interface Interface
|""".trimMargin())
val result = call.execute()
assertThat(result.exitCode).isEqualTo(ExitCode.COMPILATION_ERROR)
assertThat(result.systemErr).contains(
"error: @JsonClass can't be applied to Interface: must be a Kotlin class")
}
@Test fun abstractClassesNotSupported() {
val call = KotlinCompilerCall(temporaryFolder.root)
call.inheritClasspath = true
call.addService(Processor::class, JsonClassCodegenProcessor::class)
call.addKt("source.kt", """
|import com.squareup.moshi.JsonClass
|
|@JsonClass(generateAdapter = true)
|abstract class AbstractClass(val a: Int)
|""".trimMargin())
val result = call.execute()
assertThat(result.exitCode).isEqualTo(ExitCode.COMPILATION_ERROR)
assertThat(result.systemErr).contains(
"error: @JsonClass can't be applied to AbstractClass: must not be abstract")
}
@Test fun innerClassesNotSupported() {
val call = KotlinCompilerCall(temporaryFolder.root)
call.inheritClasspath = true
call.addService(Processor::class, JsonClassCodegenProcessor::class)
call.addKt("source.kt", """
|import com.squareup.moshi.JsonClass
|
|class Outer {
| @JsonClass(generateAdapter = true)
| inner class InnerClass(val a: Int)
|}
|""".trimMargin())
val result = call.execute()
assertThat(result.exitCode).isEqualTo(ExitCode.COMPILATION_ERROR)
assertThat(result.systemErr).contains(
"error: @JsonClass can't be applied to Outer.InnerClass: must not be an inner class")
}
@Test fun enumClassesNotSupported() {
val call = KotlinCompilerCall(temporaryFolder.root)
call.inheritClasspath = true
call.addService(Processor::class, JsonClassCodegenProcessor::class)
call.addKt("source.kt", """
|import com.squareup.moshi.JsonClass
|
|@JsonClass(generateAdapter = true)
|enum class KotlinEnum {
| A, B
|}
|""".trimMargin())
val result = call.execute()
assertThat(result.exitCode).isEqualTo(ExitCode.COMPILATION_ERROR)
assertThat(result.systemErr).contains(
"error: @JsonClass with 'generateAdapter = \"true\"' can't be applied to KotlinEnum: code gen for enums is not supported or necessary")
}
// Annotation processors don't get called for local classes, so we don't have the opportunity to
// print an error message. Instead local classes will fail at runtime.
@Ignore
@Test fun localClassesNotSupported() {
val call = KotlinCompilerCall(temporaryFolder.root)
call.inheritClasspath = true
call.addService(Processor::class, JsonClassCodegenProcessor::class)
call.addKt("source.kt", """
|import com.squareup.moshi.JsonClass
|
|fun outer() {
| @JsonClass(generateAdapter = true)
| class LocalClass(val a: Int)
|}
|""".trimMargin())
val result = call.execute()
assertThat(result.exitCode).isEqualTo(ExitCode.COMPILATION_ERROR)
assertThat(result.systemErr).contains(
"error: @JsonClass can't be applied to LocalClass: must not be local")
}
@Test fun objectDeclarationsNotSupported() {
val call = KotlinCompilerCall(temporaryFolder.root)
call.inheritClasspath = true
call.addService(Processor::class, JsonClassCodegenProcessor::class)
call.addKt("source.kt", """
|import com.squareup.moshi.JsonClass
|
|@JsonClass(generateAdapter = true)
|object ObjectDeclaration {
| var a = 5
|}
|""".trimMargin())
val result = call.execute()
assertThat(result.exitCode).isEqualTo(ExitCode.COMPILATION_ERROR)
assertThat(result.systemErr).contains(
"error: @JsonClass can't be applied to ObjectDeclaration: must be a Kotlin class")
}
@Test fun objectExpressionsNotSupported() {
val call = KotlinCompilerCall(temporaryFolder.root)
call.inheritClasspath = true
call.addService(Processor::class, JsonClassCodegenProcessor::class)
call.addKt("source.kt", """
|import com.squareup.moshi.JsonClass
|
|@JsonClass(generateAdapter = true)
|val expression = object : Any() {
| var a = 5
|}
|""".trimMargin())
val result = call.execute()
assertThat(result.exitCode).isEqualTo(ExitCode.COMPILATION_ERROR)
assertThat(result.systemErr).contains(
"error: @JsonClass can't be applied to expression\$annotations(): must be a Kotlin class")
}
@Test fun requiredTransientConstructorParameterFails() {
val call = KotlinCompilerCall(temporaryFolder.root)
call.inheritClasspath = true
call.addService(Processor::class, JsonClassCodegenProcessor::class)
call.addKt("source.kt", """
|import com.squareup.moshi.JsonClass
|
|@JsonClass(generateAdapter = true)
|class RequiredTransientConstructorParameter(@Transient var a: Int)
|""".trimMargin())
val result = call.execute()
assertThat(result.exitCode).isEqualTo(ExitCode.COMPILATION_ERROR)
assertThat(result.systemErr).contains(
"error: No default value for transient property a")
}
@Test fun nonPropertyConstructorParameter() {
val call = KotlinCompilerCall(temporaryFolder.root)
call.inheritClasspath = true
call.addService(Processor::class, JsonClassCodegenProcessor::class)
call.addKt("source.kt", """
|import com.squareup.moshi.JsonClass
|
|@JsonClass(generateAdapter = true)
|class NonPropertyConstructorParameter(a: Int, val b: Int)
|""".trimMargin())
val result = call.execute()
assertThat(result.exitCode).isEqualTo(ExitCode.COMPILATION_ERROR)
assertThat(result.systemErr).contains(
"error: No property for required constructor parameter a")
}
@Test fun badGeneratedAnnotation() {
val call = KotlinCompilerCall(temporaryFolder.root)
call.inheritClasspath = true
call.addService(Processor::class, JsonClassCodegenProcessor::class)
call.kaptArgs[JsonClassCodegenProcessor.OPTION_GENERATED] = "javax.annotation.GeneratedBlerg"
call.addKt("source.kt", """
|import com.squareup.moshi.JsonClass
|
|@JsonClass(generateAdapter = true)
|data class Foo(val a: Int)
|""".trimMargin())
val result = call.execute()
assertThat(result.exitCode).isEqualTo(ExitCode.COMPILATION_ERROR)
assertThat(result.systemErr).contains(
"Invalid option value for ${JsonClassCodegenProcessor.OPTION_GENERATED}")
}
@Test fun multipleErrors() {
val call = KotlinCompilerCall(temporaryFolder.root)
call.inheritClasspath = true
call.addService(Processor::class, JsonClassCodegenProcessor::class)
call.addKt("source.kt", """
|import com.squareup.moshi.JsonClass
|
|@JsonClass(generateAdapter = true)
|class Class1(private var a: Int, private var b: Int)
|
|@JsonClass(generateAdapter = true)
|class Class2(private var c: Int)
|""".trimMargin())
val result = call.execute()
assertThat(result.exitCode).isEqualTo(ExitCode.COMPILATION_ERROR)
assertThat(result.systemErr).contains("property a is not visible")
assertThat(result.systemErr).contains("property b is not visible")
assertThat(result.systemErr).contains("property c is not visible")
}
@Test fun extendPlatformType() {
val call = KotlinCompilerCall(temporaryFolder.root)
call.inheritClasspath = true
call.addService(Processor::class, JsonClassCodegenProcessor::class)
call.addKt("source.kt", """
|import com.squareup.moshi.JsonClass
|import java.util.Date
|
|@JsonClass(generateAdapter = true)
|class ExtendsPlatformClass(var a: Int) : Date()
|""".trimMargin())
val result = call.execute()
assertThat(result.exitCode).isEqualTo(ExitCode.COMPILATION_ERROR)
assertThat(result.systemErr).contains("supertype java.util.Date is not a Kotlin type")
}
@Test fun extendJavaType() {
val call = KotlinCompilerCall(temporaryFolder.root)
call.inheritClasspath = true
call.addService(Processor::class, JsonClassCodegenProcessor::class)
call.addKt("source.kt", """
|import com.squareup.moshi.JsonClass
|import com.squareup.moshi.kotlin.codegen.JavaSuperclass
|
|@JsonClass(generateAdapter = true)
|class ExtendsJavaType(var b: Int) : JavaSuperclass()
|""".trimMargin())
val result = call.execute()
assertThat(result.exitCode).isEqualTo(ExitCode.COMPILATION_ERROR)
assertThat(result.systemErr)
.contains("supertype com.squareup.moshi.kotlin.codegen.JavaSuperclass is not a Kotlin type")
}
@Test
fun nonFieldApplicableQualifier() {
val call = KotlinCompilerCall(temporaryFolder.root)
call.inheritClasspath = true
call.addService(Processor::class, JsonClassCodegenProcessor::class)
call.addKt("source.kt", """
|import com.squareup.moshi.JsonClass
|import com.squareup.moshi.JsonQualifier
|import kotlin.annotation.AnnotationRetention.RUNTIME
|import kotlin.annotation.AnnotationTarget.PROPERTY
|import kotlin.annotation.Retention
|import kotlin.annotation.Target
|
|@Retention(RUNTIME)
|@Target(PROPERTY)
|@JsonQualifier
|annotation class UpperCase
|
|@JsonClass(generateAdapter = true)
|class ClassWithQualifier(@UpperCase val a: Int)
|""".trimMargin())
val result = call.execute()
println(result.systemErr)
assertThat(result.exitCode).isEqualTo(ExitCode.COMPILATION_ERROR)
assertThat(result.systemErr).contains("JsonQualifier @UpperCase must support FIELD target")
}
@Test
fun nonRuntimeQualifier() {
val call = KotlinCompilerCall(temporaryFolder.root)
call.inheritClasspath = true
call.addService(Processor::class, JsonClassCodegenProcessor::class)
call.addKt("source.kt", """
|import com.squareup.moshi.JsonClass
|import com.squareup.moshi.JsonQualifier
|import kotlin.annotation.AnnotationRetention.BINARY
|import kotlin.annotation.AnnotationTarget.FIELD
|import kotlin.annotation.AnnotationTarget.PROPERTY
|import kotlin.annotation.Retention
|import kotlin.annotation.Target
|
|@Retention(BINARY)
|@Target(PROPERTY, FIELD)
|@JsonQualifier
|annotation class UpperCase
|
|@JsonClass(generateAdapter = true)
|class ClassWithQualifier(@UpperCase val a: Int)
|""".trimMargin())
val result = call.execute()
assertThat(result.exitCode).isEqualTo(ExitCode.COMPILATION_ERROR)
assertThat(result.systemErr).contains("JsonQualifier @UpperCase must have RUNTIME retention")
}
}

View file

@ -0,0 +1,193 @@
/*
* Copyright (C) 2018 Square, Inc.
*
* 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 com.squareup.moshi.kotlin.codegen
import com.google.common.collect.LinkedHashMultimap
import okio.Buffer
import okio.Okio
import org.jetbrains.kotlin.cli.common.CLITool
import org.jetbrains.kotlin.cli.jvm.K2JVMCompiler
import java.io.File
import java.io.FileOutputStream
import java.io.ObjectOutputStream
import java.io.PrintStream
import java.net.URLClassLoader
import java.net.URLDecoder
import java.util.zip.ZipEntry
import java.util.zip.ZipOutputStream
import kotlin.reflect.KClass
/** Prepares an invocation of the Kotlin compiler. */
class KotlinCompilerCall(var scratchDir: File) {
val sourcesDir = File(scratchDir, "sources")
val classesDir = File(scratchDir, "classes")
val servicesJar = File(scratchDir, "services.jar")
var inheritClasspath = false
val args = mutableListOf<String>()
val kaptArgs = mutableMapOf<String, String>()
val classpath = mutableListOf<String>()
val services = LinkedHashMultimap.create<KClass<*>, KClass<*>>()
/** Adds a source file to be compiled. */
fun addKt(path: String, source: String) {
val sourceFile = File(sourcesDir, path)
sourceFile.parentFile.mkdirs()
Okio.buffer(Okio.sink(sourceFile)).use {
it.writeUtf8(source)
}
}
/** Adds a service like an annotation processor to make available to the compiler. */
fun addService(serviceClass: KClass<*>, implementation: KClass<*>) {
services.put(serviceClass, implementation)
}
fun execute(): KotlinCompilerResult {
val fullArgs = mutableListOf<String>()
fullArgs.addAll(args)
fullArgs.add("-d")
fullArgs.add(classesDir.toString())
val fullClasspath = fullClasspath()
if (fullClasspath.isNotEmpty()) {
fullArgs.add("-classpath")
fullArgs.add(fullClasspath.joinToString(separator = ":"))
}
for (source in sourcesDir.listFiles()) {
fullArgs.add(source.toString())
}
fullArgs.addAll(annotationProcessorArgs())
if (kaptArgs.isNotEmpty()) {
fullArgs.apply {
add("-P")
add("plugin:org.jetbrains.kotlin.kapt3:apoptions=${encodeOptions(kaptArgs)}")
}
}
val systemErrBuffer = Buffer()
val oldSystemErr = System.err
System.setErr(PrintStream(systemErrBuffer.outputStream()))
try {
val exitCode = CLITool.doMainNoExit(K2JVMCompiler(), fullArgs.toTypedArray())
val systemErr = systemErrBuffer.readUtf8()
return KotlinCompilerResult(systemErr, exitCode)
} finally {
System.setErr(oldSystemErr)
}
}
/** Returns arguments necessary to enable and configure kapt3. */
private fun annotationProcessorArgs(): List<String> {
val kaptSourceDir = File(scratchDir, "kapt/sources")
val kaptStubsDir = File(scratchDir, "kapt/stubs")
return listOf(
"-Xplugin=${kapt3Jar()}",
"-P", "plugin:org.jetbrains.kotlin.kapt3:sources=$kaptSourceDir",
"-P", "plugin:org.jetbrains.kotlin.kapt3:classes=$classesDir",
"-P", "plugin:org.jetbrains.kotlin.kapt3:stubs=$kaptStubsDir",
"-P", "plugin:org.jetbrains.kotlin.kapt3:apclasspath=$servicesJar",
"-P", "plugin:org.jetbrains.kotlin.kapt3:correctErrorTypes=true"
)
}
/** Returns the classpath to use when compiling code. */
private fun fullClasspath(): List<String> {
val result = mutableListOf<String>()
result.addAll(classpath)
// Copy over the classpath of the running application.
if (inheritClasspath) {
for (classpathFile in classpathFiles()) {
result.add(classpathFile.toString())
}
}
if (!services.isEmpty) {
writeServicesJar()
result.add(servicesJar.toString())
}
return result.toList()
}
/**
* Generate a .jar file that holds ServiceManager registrations. Necessary because AutoService's
* results might not be visible to this test.
*/
private fun writeServicesJar() {
ZipOutputStream(FileOutputStream(servicesJar)).use { zipOutputStream ->
for (entry in services.asMap()) {
zipOutputStream.putNextEntry(
ZipEntry("META-INF/services/${entry.key.qualifiedName}"))
val serviceFile = Okio.buffer(Okio.sink(zipOutputStream))
for (implementation in entry.value) {
serviceFile.writeUtf8(implementation.qualifiedName!!)
serviceFile.writeUtf8("\n")
}
serviceFile.emit() // Don't close the entry; that closes the file.
zipOutputStream.closeEntry()
}
}
}
/** Returns the files on the host process' classpath. */
private fun classpathFiles(): List<File> {
val classLoader = JsonClassCodegenProcessorTest::class.java.classLoader
if (classLoader !is URLClassLoader) {
throw UnsupportedOperationException("unable to extract classpath from $classLoader")
}
val result = mutableListOf<File>()
for (url in classLoader.urLs) {
if (url.protocol != "file") {
throw UnsupportedOperationException("unable to handle classpath element $url")
}
result.add(File(URLDecoder.decode(url.path, "UTF-8")))
}
return result.toList()
}
/** Returns the path to the kotlin-annotation-processing .jar file. */
private fun kapt3Jar(): File {
for (file in classpathFiles()) {
if (file.name.startsWith("kotlin-annotation-processing-embeddable")) return file
}
throw IllegalStateException("no kotlin-annotation-processing-embeddable jar on classpath:\n " +
"${classpathFiles().joinToString(separator = "\n ")}}")
}
/**
* Base64 encodes a mapping of annotation processor args for kapt, as specified by
* https://kotlinlang.org/docs/reference/kapt.html#apjavac-options-encoding
*/
private fun encodeOptions(options: Map<String, String>): String {
val buffer = Buffer()
ObjectOutputStream(buffer.outputStream()).use { oos ->
oos.writeInt(options.size)
for ((key, value) in options.entries) {
oos.writeUTF(key)
oos.writeUTF(value)
}
}
return buffer.readByteString().base64()
}
}

View file

@ -0,0 +1,23 @@
/*
* Copyright (C) 2018 Square, Inc.
*
* 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 com.squareup.moshi.kotlin.codegen
import org.jetbrains.kotlin.cli.common.ExitCode
class KotlinCompilerResult(
val systemErr: String,
var exitCode: ExitCode
)

View file

@ -0,0 +1,46 @@
/*
* Copyright (C) 2018 Square, Inc.
*
* 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 com.squareup.moshi.kotlin.codegen
import com.google.common.truth.Truth.assertThat
import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.plusParameter
import com.squareup.kotlinpoet.WildcardTypeName
import com.squareup.kotlinpoet.asClassName
import org.junit.Test
class TypeResolverTest {
private val resolver = TypeResolver()
@Test
fun ensureClassNameNullabilityIsPreserved() {
assertThat(resolver.resolve(Int::class.asClassName().copy(nullable = true)).isNullable).isTrue()
}
@Test
fun ensureParameterizedNullabilityIsPreserved() {
val nullableTypeName = List::class.plusParameter(String::class).copy(nullable = true)
assertThat(resolver.resolve(nullableTypeName).isNullable).isTrue()
}
@Test
fun ensureWildcardNullabilityIsPreserved() {
val nullableTypeName = WildcardTypeName.producerOf(List::class.asClassName())
.copy(nullable = true)
assertThat(resolver.resolve(nullableTypeName).isNullable).isTrue()
}
}

137
kotlin/reflect/pom.xml Normal file
View file

@ -0,0 +1,137 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.squareup.moshi</groupId>
<artifactId>moshi-parent</artifactId>
<version>1.9.0-SNAPSHOT</version>
<relativePath>../../pom.xml</relativePath>
</parent>
<artifactId>moshi-kotlin</artifactId>
<dependencies>
<dependency>
<groupId>com.squareup.moshi</groupId>
<artifactId>moshi</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-stdlib</artifactId>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-reflect</artifactId>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<properties>
<maven.javadoc.skip>true</maven.javadoc.skip><!-- We use Dokka instead. -->
</properties>
<build>
<plugins>
<plugin>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-maven-plugin</artifactId>
<version>${kotlin.version}</version>
<executions>
<execution>
<id>compile</id>
<phase>compile</phase>
<goals>
<goal>compile</goal>
</goals>
</execution>
<execution>
<id>test-compile</id>
<phase>test-compile</phase>
<goals>
<goal>test-compile</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<executions>
<execution>
<id>compile</id>
<phase>compile</phase>
<goals>
<goal>compile</goal>
</goals>
</execution>
<execution>
<id>testCompile</id>
<phase>test-compile</phase>
<goals>
<goal>testCompile</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<configuration>
<archive>
<manifestEntries>
<Automatic-Module-Name>com.squareup.moshi.kotlin</Automatic-Module-Name>
</manifestEntries>
</archive>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<version>${maven-assembly.version}</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
<configuration>
<descriptors>
<descriptor>src/assembly/dokka.xml</descriptor>
</descriptors>
</configuration>
</plugin>
<plugin>
<groupId>org.jetbrains.dokka</groupId>
<artifactId>dokka-maven-plugin</artifactId>
<version>${dokka.version}</version>
<executions>
<execution>
<phase>prepare-package</phase>
<goals>
<goal>dokka</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

View file

@ -0,0 +1,16 @@
<assembly
xmlns="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.3"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.3 http://maven.apache.org/xsd/assembly-1.1.3.xsd">
<id>javadoc</id>
<formats>
<format>jar</format>
</formats>
<baseDirectory>/</baseDirectory>
<fileSets>
<fileSet>
<directory>target/dokka/moshi-kotlin</directory>
<outputDirectory>/</outputDirectory>
</fileSet>
</fileSets>
</assembly>

View file

@ -0,0 +1,25 @@
/*
* Copyright (C) 2017 Square, Inc.
*
* 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 com.squareup.moshi
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
@Deprecated(
message = "this moved to avoid a package name conflict in the Java Platform Module System.",
replaceWith = ReplaceWith("com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory")
)
class KotlinJsonAdapterFactory
: JsonAdapter.Factory by KotlinJsonAdapterFactory()

View file

@ -0,0 +1,262 @@
/*
* Copyright (C) 2017 Square, Inc.
*
* 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 com.squareup.moshi.kotlin.reflect
import com.squareup.moshi.Json
import com.squareup.moshi.JsonAdapter
import com.squareup.moshi.JsonDataException
import com.squareup.moshi.JsonReader
import com.squareup.moshi.JsonWriter
import com.squareup.moshi.Moshi
import com.squareup.moshi.Types
import com.squareup.moshi.internal.Util
import com.squareup.moshi.internal.Util.generatedAdapter
import com.squareup.moshi.internal.Util.resolve
import java.lang.reflect.Modifier
import java.lang.reflect.Type
import java.util.AbstractMap.SimpleEntry
import kotlin.collections.Map.Entry
import kotlin.reflect.KFunction
import kotlin.reflect.KMutableProperty1
import kotlin.reflect.KParameter
import kotlin.reflect.KProperty1
import kotlin.reflect.full.findAnnotation
import kotlin.reflect.full.memberProperties
import kotlin.reflect.full.primaryConstructor
import kotlin.reflect.jvm.isAccessible
import kotlin.reflect.jvm.javaField
import kotlin.reflect.jvm.javaType
/** Classes annotated with this are eligible for this adapter. */
private val KOTLIN_METADATA = Class.forName("kotlin.Metadata") as Class<out Annotation>
/**
* Placeholder value used when a field is absent from the JSON. Note that this code
* distinguishes between absent values and present-but-null values.
*/
private val ABSENT_VALUE = Any()
/**
* This class encodes Kotlin classes using their properties. It decodes them by first invoking the
* constructor, and then by setting any additional properties that exist, if any.
*/
internal class KotlinJsonAdapter<T>(
val constructor: KFunction<T>,
val bindings: List<Binding<T, Any?>?>,
val options: JsonReader.Options) : JsonAdapter<T>() {
override fun fromJson(reader: JsonReader): T {
val constructorSize = constructor.parameters.size
// Read each value into its slot in the array.
val values = Array<Any?>(bindings.size) { ABSENT_VALUE }
reader.beginObject()
while (reader.hasNext()) {
val index = reader.selectName(options)
val binding = if (index != -1) bindings[index] else null
if (binding == null) {
reader.skipName()
reader.skipValue()
continue
}
if (values[index] !== ABSENT_VALUE) {
throw JsonDataException(
"Multiple values for '${binding.property.name}' at ${reader.path}")
}
values[index] = binding.adapter.fromJson(reader)
if (values[index] == null && !binding.property.returnType.isMarkedNullable) {
throw JsonDataException(
"Non-null value '${binding.property.name}' was null at ${reader.path}")
}
}
reader.endObject()
// Confirm all parameters are present, optional, or nullable.
for (i in 0 until constructorSize) {
if (values[i] === ABSENT_VALUE && !constructor.parameters[i].isOptional) {
if (!constructor.parameters[i].type.isMarkedNullable) {
throw JsonDataException(
"Required value '${constructor.parameters[i].name}' missing at ${reader.path}")
}
values[i] = null // Replace absent with null.
}
}
// Call the constructor using a Map so that absent optionals get defaults.
val result = constructor.callBy(IndexedParameterMap(constructor.parameters, values))
// Set remaining properties.
for (i in constructorSize until bindings.size) {
val binding = bindings[i]!!
val value = values[i]
binding.set(result, value)
}
return result
}
override fun toJson(writer: JsonWriter, value: T?) {
if (value == null) throw NullPointerException("value == null")
writer.beginObject()
for (binding in bindings) {
if (binding == null) continue // Skip constructor parameters that aren't properties.
writer.name(binding.name)
binding.adapter.toJson(writer, binding.get(value))
}
writer.endObject()
}
override fun toString() = "KotlinJsonAdapter(${constructor.returnType})"
data class Binding<K, P>(
val name: String,
val adapter: JsonAdapter<P>,
val property: KProperty1<K, P>,
val parameter: KParameter?) {
fun get(value: K) = property.get(value)
fun set(result: K, value: P) {
if (value !== ABSENT_VALUE) {
(property as KMutableProperty1<K, P>).set(result, value)
}
}
}
/** A simple [Map] that uses parameter indexes instead of sorting or hashing. */
class IndexedParameterMap(val parameterKeys: List<KParameter>, val parameterValues: Array<Any?>)
: AbstractMap<KParameter, Any?>() {
override val entries: Set<Entry<KParameter, Any?>>
get() {
val allPossibleEntries = parameterKeys.mapIndexed { index, value ->
SimpleEntry<KParameter, Any?>(value, parameterValues[index])
}
return allPossibleEntries.filterTo(LinkedHashSet<Entry<KParameter, Any?>>()) {
it.value !== ABSENT_VALUE
}
}
override fun containsKey(key: KParameter) = parameterValues[key.index] !== ABSENT_VALUE
override fun get(key: KParameter): Any? {
val value = parameterValues[key.index]
return if (value !== ABSENT_VALUE) value else null
}
}
}
class KotlinJsonAdapterFactory : JsonAdapter.Factory {
override fun create(type: Type, annotations: MutableSet<out Annotation>, moshi: Moshi)
: JsonAdapter<*>? {
if (!annotations.isEmpty()) return null
val rawType = Types.getRawType(type)
if (rawType.isInterface) return null
if (rawType.isEnum) return null
if (!rawType.isAnnotationPresent(KOTLIN_METADATA)) return null
if (Util.isPlatformType(rawType)) return null
try {
val generatedAdapter = generatedAdapter(moshi, type, rawType)
if (generatedAdapter != null) {
return generatedAdapter
}
} catch (e: RuntimeException) {
if (e.cause !is ClassNotFoundException) {
throw e
}
// Fall back to a reflective adapter when the generated adapter is not found.
}
if (rawType.isLocalClass) {
throw IllegalArgumentException("Cannot serialize local class or object expression ${rawType.name}")
}
val rawTypeKotlin = rawType.kotlin
if (rawTypeKotlin.isAbstract) {
throw IllegalArgumentException("Cannot serialize abstract class ${rawType.name}")
}
if (rawTypeKotlin.isInner) {
throw IllegalArgumentException("Cannot serialize inner class ${rawType.name}")
}
if (rawTypeKotlin.objectInstance != null) {
throw IllegalArgumentException("Cannot serialize object declaration ${rawType.name}")
}
val constructor = rawTypeKotlin.primaryConstructor ?: return null
val parametersByName = constructor.parameters.associateBy { it.name }
constructor.isAccessible = true
val bindingsByName = LinkedHashMap<String, KotlinJsonAdapter.Binding<Any, Any?>>()
for (property in rawTypeKotlin.memberProperties) {
val parameter = parametersByName[property.name]
if (Modifier.isTransient(property.javaField?.modifiers ?: 0)) {
if (parameter != null && !parameter.isOptional) {
throw IllegalArgumentException(
"No default value for transient constructor $parameter")
}
continue
}
if (parameter != null && parameter.type != property.returnType) {
throw IllegalArgumentException("'${property.name}' has a constructor parameter of type " +
"${parameter.type} but a property of type ${property.returnType}.")
}
if (property !is KMutableProperty1 && parameter == null) continue
property.isAccessible = true
var allAnnotations = property.annotations
var jsonAnnotation = property.findAnnotation<Json>()
if (parameter != null) {
allAnnotations += parameter.annotations
if (jsonAnnotation == null) {
jsonAnnotation = parameter.findAnnotation<Json>()
}
}
val name = jsonAnnotation?.name ?: property.name
val resolvedPropertyType = resolve(type, rawType, property.returnType.javaType)
val adapter = moshi.adapter<Any>(
resolvedPropertyType, Util.jsonAnnotations(allAnnotations.toTypedArray()), property.name)
bindingsByName[property.name] = KotlinJsonAdapter.Binding(name, adapter,
property as KProperty1<Any, Any?>, parameter)
}
val bindings = ArrayList<KotlinJsonAdapter.Binding<Any, Any?>?>()
for (parameter in constructor.parameters) {
val binding = bindingsByName.remove(parameter.name)
if (binding == null && !parameter.isOptional) {
throw IllegalArgumentException("No property for required constructor ${parameter}")
}
bindings += binding
}
bindings += bindingsByName.values
val options = JsonReader.Options.of(*bindings.map { it?.name ?: "\u0000" }.toTypedArray())
return KotlinJsonAdapter(constructor, bindings, options).nullSafe()
}
}

View file

@ -0,0 +1,5 @@
-keep class kotlin.reflect.jvm.internal.impl.builtins.BuiltInsLoaderImpl
-keepclassmembers class kotlin.Metadata {
public <methods>;
}

View file

@ -0,0 +1,21 @@
package com.squareup.moshi.kotlin.reflect
import com.squareup.moshi.JsonClass
import com.squareup.moshi.Moshi
import org.assertj.core.api.Assertions.assertThat
import org.junit.Test
class KotlinJsonAdapterTest {
@JsonClass(generateAdapter = true)
class Data
@Test fun fallsBackToReflectiveAdapterWithoutCodegen() {
val moshi = Moshi.Builder()
.add(KotlinJsonAdapterFactory())
.build()
val adapter = moshi.adapter(Data::class.java)
assertThat(adapter.toString()).isEqualTo(
"KotlinJsonAdapter(com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterTest.Data).nullSafe()"
)
}
}

191
kotlin/tests/pom.xml Normal file
View file

@ -0,0 +1,191 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.squareup.moshi</groupId>
<artifactId>moshi-parent</artifactId>
<version>1.9.0-SNAPSHOT</version>
<relativePath>../../pom.xml</relativePath>
</parent>
<artifactId>moshi-kotlin-tests</artifactId>
<dependencies>
<dependency>
<groupId>com.squareup.moshi</groupId>
<artifactId>moshi</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>com.squareup.moshi</groupId>
<artifactId>moshi-kotlin</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-stdlib</artifactId>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<properties>
<maven.javadoc.skip>true</maven.javadoc.skip><!-- We use Dokka instead. -->
</properties>
<build>
<plugins>
<plugin>
<artifactId>kotlin-maven-plugin</artifactId>
<groupId>org.jetbrains.kotlin</groupId>
<version>${kotlin.version}</version>
<executions>
<execution>
<id>kapt</id>
<goals>
<goal>kapt</goal>
</goals>
<configuration>
<sourceDirs>
<sourceDir>src/main/kotlin</sourceDir>
</sourceDirs>
<annotationProcessorPaths>
<annotationProcessorPath>
<groupId>com.squareup.moshi</groupId>
<artifactId>moshi-kotlin-codegen</artifactId>
<version>${project.version}</version>
</annotationProcessorPath>
</annotationProcessorPaths>
</configuration>
</execution>
<execution>
<id>compile</id>
<goals>
<goal>compile</goal>
</goals>
<configuration>
<sourceDirs>
<sourceDir>src/main/kotlin</sourceDir>
<sourceDir>src/main/java</sourceDir>
</sourceDirs>
</configuration>
</execution>
<execution>
<id>test-kapt</id>
<goals>
<goal>test-kapt</goal>
</goals>
<configuration>
<sourceDirs>
<sourceDir>src/test/kotlin</sourceDir>
<sourceDir>src/test/java</sourceDir>
</sourceDirs>
<annotationProcessorPaths>
<annotationProcessorPath>
<groupId>com.squareup.moshi</groupId>
<artifactId>moshi-kotlin-codegen</artifactId>
<version>${project.version}</version>
</annotationProcessorPath>
</annotationProcessorPaths>
</configuration>
</execution>
<execution>
<id>test-compile</id>
<goals>
<goal>test-compile</goal>
</goals>
<configuration>
<sourceDirs>
<sourceDir>src/test/kotlin</sourceDir>
<sourceDir>src/test/java</sourceDir>
<sourceDir>target/generated-sources/kapt/test</sourceDir>
</sourceDirs>
</configuration>
</execution>
</executions>
<configuration>
<args>
<arg>-Werror</arg>
</args>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.5.1</version>
<configuration>
<proc>none</proc>
<source>1.6</source>
<target>1.6</target>
</configuration>
<executions>
<!-- Replacing default-compile as it is treated specially by maven -->
<execution>
<id>default-compile</id>
<phase>none</phase>
</execution>
<!-- Replacing default-testCompile as it is treated specially by maven -->
<execution>
<id>default-testCompile</id>
<phase>none</phase>
</execution>
<execution>
<id>java-compile</id>
<phase>compile</phase>
<goals>
<goal>compile</goal>
</goals>
</execution>
<execution>
<id>java-test-compile</id>
<phase>test-compile</phase>
<goals>
<goal>testCompile</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<version>${maven-assembly.version}</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
<configuration>
<descriptors>
<descriptor>src/assembly/dokka.xml</descriptor>
</descriptors>
</configuration>
</plugin>
<plugin>
<groupId>org.jetbrains.dokka</groupId>
<artifactId>dokka-maven-plugin</artifactId>
<version>${dokka.version}</version>
<executions>
<execution>
<phase>prepare-package</phase>
<goals>
<goal>dokka</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

View file

@ -0,0 +1,16 @@
<assembly
xmlns="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.3"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.3 http://maven.apache.org/xsd/assembly-1.1.3.xsd">
<id>javadoc</id>
<formats>
<format>jar</format>
</formats>
<baseDirectory>/</baseDirectory>
<fileSets>
<fileSet>
<directory>target/dokka/moshi-kotlin-tests</directory>
<outputDirectory>/</outputDirectory>
</fileSet>
</fileSets>
</assembly>

View file

@ -0,0 +1,32 @@
/*
* Copyright (C) 2019 Square, Inc.
*
* 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 com.squareup.moshi.kotlin.codgen
import com.squareup.moshi.JsonAdapter
import com.squareup.moshi.JsonReader
import com.squareup.moshi.JsonWriter
import com.squareup.moshi.kotlin.codgen.GeneratedAdaptersTest.CustomGeneratedClass
// This also tests custom generated types with no moshi constructor
class GeneratedAdaptersTest_CustomGeneratedClassJsonAdapter : JsonAdapter<CustomGeneratedClass>() {
override fun fromJson(reader: JsonReader): CustomGeneratedClass? {
TODO()
}
override fun toJson(writer: JsonWriter, value: CustomGeneratedClass?) {
TODO()
}
}

View file

@ -0,0 +1,9 @@
package com.squareup.moshi.kotlin.codgen.LooksLikeAClass
import com.squareup.moshi.JsonClass
/**
* https://github.com/square/moshi/issues/783
*/
@JsonClass(generateAdapter = true)
data class ClassInPackageThatLooksLikeAClass(val foo: String)

View file

@ -6,7 +6,7 @@
<parent>
<groupId>com.squareup.moshi</groupId>
<artifactId>moshi-parent</artifactId>
<version>1.4.0</version>
<version>1.9.0-SNAPSHOT</version>
</parent>
<artifactId>moshi</artifactId>
@ -17,6 +17,11 @@
<groupId>com.squareup.okio</groupId>
<artifactId>okio</artifactId>
</dependency>
<dependency>
<groupId>com.google.code.findbugs</groupId>
<artifactId>jsr305</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
@ -28,4 +33,31 @@
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<configuration>
<archive>
<manifestEntries>
<Automatic-Module-Name>com.squareup.moshi</Automatic-Module-Name>
</manifestEntries>
</archive>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-javadoc-plugin</artifactId>
<version>2.10.4</version>
<configuration>
<excludePackageNames>com.squareup.moshi.internal:com.squareup.moshi.internal.*</excludePackageNames>
<links>
<link>https://square.github.io/okio/</link>
</links>
</configuration>
</plugin>
</plugins>
</build>
</project>

View file

@ -15,6 +15,7 @@
*/
package com.squareup.moshi;
import com.squareup.moshi.internal.Util;
import java.io.IOException;
import java.lang.annotation.Annotation;
import java.lang.reflect.InvocationTargetException;
@ -24,19 +25,22 @@ import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import javax.annotation.Nullable;
import static com.squareup.moshi.Util.jsonAnnotations;
import static com.squareup.moshi.internal.Util.canonicalize;
import static com.squareup.moshi.internal.Util.jsonAnnotations;
import static com.squareup.moshi.internal.Util.typeAnnotatedWithAnnotations;
final class AdapterMethodsFactory implements JsonAdapter.Factory {
private final List<AdapterMethod> toAdapters;
private final List<AdapterMethod> fromAdapters;
public AdapterMethodsFactory(List<AdapterMethod> toAdapters, List<AdapterMethod> fromAdapters) {
AdapterMethodsFactory(List<AdapterMethod> toAdapters, List<AdapterMethod> fromAdapters) {
this.toAdapters = toAdapters;
this.fromAdapters = fromAdapters;
}
@Override public JsonAdapter<?> create(
@Override public @Nullable JsonAdapter<?> create(
final Type type, final Set<? extends Annotation> annotations, final Moshi moshi) {
final AdapterMethod toAdapter = get(toAdapters, type, annotations);
final AdapterMethod fromAdapter = get(fromAdapters, type, annotations);
@ -49,17 +53,17 @@ final class AdapterMethodsFactory implements JsonAdapter.Factory {
} catch (IllegalArgumentException e) {
String missingAnnotation = toAdapter == null ? "@ToJson" : "@FromJson";
throw new IllegalArgumentException("No " + missingAnnotation + " adapter for "
+ type + " annotated " + annotations);
+ typeAnnotatedWithAnnotations(type, annotations), e);
}
} else {
delegate = null;
}
if (toAdapter != null) toAdapter.bind(moshi);
if (fromAdapter != null) fromAdapter.bind(moshi);
if (toAdapter != null) toAdapter.bind(moshi, this);
if (fromAdapter != null) fromAdapter.bind(moshi, this);
return new JsonAdapter<Object>() {
@Override public void toJson(JsonWriter writer, Object value) throws IOException {
@Override public void toJson(JsonWriter writer, @Nullable Object value) throws IOException {
if (toAdapter == null) {
delegate.toJson(writer, value);
} else if (!toAdapter.nullable && value == null) {
@ -75,7 +79,7 @@ final class AdapterMethodsFactory implements JsonAdapter.Factory {
}
}
@Override public Object fromJson(JsonReader reader) throws IOException {
@Override public @Nullable Object fromJson(JsonReader reader) throws IOException {
if (fromAdapter == null) {
return delegate.fromJson(reader);
} else if (!fromAdapter.nullable && reader.peek() == JsonReader.Token.NULL) {
@ -155,7 +159,7 @@ final class AdapterMethodsFactory implements JsonAdapter.Factory {
Set<? extends Annotation> qualifierAnnotations = jsonAnnotations(parameterAnnotations[1]);
return new AdapterMethod(parameterTypes[1], qualifierAnnotations, adapter, method,
parameterTypes.length, 2, true) {
@Override public void toJson(Moshi moshi, JsonWriter writer, Object value)
@Override public void toJson(Moshi moshi, JsonWriter writer, @Nullable Object value)
throws IOException, InvocationTargetException {
invoke(writer, value);
}
@ -164,18 +168,22 @@ final class AdapterMethodsFactory implements JsonAdapter.Factory {
} else if (parameterTypes.length == 1 && returnType != void.class) {
// List<Integer> pointToJson(Point point) {
final Set<? extends Annotation> returnTypeAnnotations = jsonAnnotations(method);
Set<? extends Annotation> qualifierAnnotations = jsonAnnotations(parameterAnnotations[0]);
final Set<? extends Annotation> qualifierAnnotations =
jsonAnnotations(parameterAnnotations[0]);
boolean nullable = Util.hasNullable(parameterAnnotations[0]);
return new AdapterMethod(parameterTypes[0], qualifierAnnotations, adapter, method,
parameterTypes.length, 1, nullable) {
private JsonAdapter<Object> delegate;
@Override public void bind(Moshi moshi) {
super.bind(moshi);
delegate = moshi.adapter(returnType, returnTypeAnnotations);
@Override public void bind(Moshi moshi, JsonAdapter.Factory factory) {
super.bind(moshi, factory);
delegate = Types.equals(parameterTypes[0], returnType)
&& qualifierAnnotations.equals(returnTypeAnnotations)
? moshi.nextAdapter(factory, returnType, returnTypeAnnotations)
: moshi.adapter(returnType, returnTypeAnnotations);
}
@Override public void toJson(Moshi moshi, JsonWriter writer, Object value)
@Override public void toJson(Moshi moshi, JsonWriter writer, @Nullable Object value)
throws IOException, InvocationTargetException {
Object intermediate = invoke(value);
delegate.toJson(writer, intermediate);
@ -186,6 +194,8 @@ final class AdapterMethodsFactory implements JsonAdapter.Factory {
throw new IllegalArgumentException("Unexpected signature for " + method + ".\n"
+ "@ToJson method signatures may have one of the following structures:\n"
+ " <any access modifier> void toJson(JsonWriter writer, T value) throws <any>;\n"
+ " <any access modifier> void toJson(JsonWriter writer, T value,"
+ " JsonAdapter<any> delegate, <any more delegates>) throws <any>;\n"
+ " <any access modifier> R toJson(T value) throws <any>;\n");
}
}
@ -206,7 +216,7 @@ final class AdapterMethodsFactory implements JsonAdapter.Factory {
static AdapterMethod fromAdapter(Object adapter, Method method) {
method.setAccessible(true);
final Type returnType = method.getGenericReturnType();
Set<? extends Annotation> returnTypeAnnotations = jsonAnnotations(method);
final Set<? extends Annotation> returnTypeAnnotations = jsonAnnotations(method);
final Type[] parameterTypes = method.getGenericParameterTypes();
Annotation[][] parameterAnnotations = method.getParameterAnnotations();
@ -233,9 +243,12 @@ final class AdapterMethodsFactory implements JsonAdapter.Factory {
parameterTypes.length, 1, nullable) {
JsonAdapter<Object> delegate;
@Override public void bind(Moshi moshi) {
super.bind(moshi);
delegate = moshi.adapter(parameterTypes[0], qualifierAnnotations);
@Override public void bind(Moshi moshi, JsonAdapter.Factory factory) {
super.bind(moshi, factory);
delegate = Types.equals(parameterTypes[0], returnType)
&& qualifierAnnotations.equals(returnTypeAnnotations)
? moshi.nextAdapter(factory, parameterTypes[0], qualifierAnnotations)
: moshi.adapter(parameterTypes[0], qualifierAnnotations);
}
@Override public Object fromJson(Moshi moshi, JsonReader reader)
@ -248,24 +261,26 @@ final class AdapterMethodsFactory implements JsonAdapter.Factory {
} else {
throw new IllegalArgumentException("Unexpected signature for " + method + ".\n"
+ "@FromJson method signatures may have one of the following structures:\n"
+ " <any access modifier> void fromJson(JsonReader jsonReader) throws <any>;\n"
+ " <any access modifier> R fromJson(JsonReader jsonReader) throws <any>;\n"
+ " <any access modifier> R fromJson(JsonReader jsonReader,"
+ " JsonAdapter<any> delegate, <any more delegates>) throws <any>;\n"
+ " <any access modifier> R fromJson(T value) throws <any>;\n");
}
}
/** Returns the matching adapter method from the list. */
private static AdapterMethod get(
private static @Nullable AdapterMethod get(
List<AdapterMethod> adapterMethods, Type type, Set<? extends Annotation> annotations) {
for (int i = 0, size = adapterMethods.size(); i < size; i++) {
AdapterMethod adapterMethod = adapterMethods.get(i);
if (adapterMethod.type.equals(type) && adapterMethod.annotations.equals(annotations)) {
if (Types.equals(adapterMethod.type, type) && adapterMethod.annotations.equals(annotations)) {
return adapterMethod;
}
}
return null;
}
static abstract class AdapterMethod {
abstract static class AdapterMethod {
final Type type;
final Set<? extends Annotation> annotations;
final Object adapter;
@ -274,9 +289,9 @@ final class AdapterMethodsFactory implements JsonAdapter.Factory {
final JsonAdapter<?>[] jsonAdapters;
final boolean nullable;
public AdapterMethod(Type type, Set<? extends Annotation> annotations, Object adapter,
AdapterMethod(Type type, Set<? extends Annotation> annotations, Object adapter,
Method method, int parameterCount, int adaptersOffset, boolean nullable) {
this.type = Types.canonicalize(type);
this.type = canonicalize(type);
this.annotations = annotations;
this.adapter = adapter;
this.method = method;
@ -285,30 +300,33 @@ final class AdapterMethodsFactory implements JsonAdapter.Factory {
this.nullable = nullable;
}
public void bind(Moshi moshi) {
public void bind(Moshi moshi, JsonAdapter.Factory factory) {
if (jsonAdapters.length > 0) {
Type[] parameterTypes = method.getGenericParameterTypes();
Annotation[][] parameterAnnotations = method.getParameterAnnotations();
for (int i = adaptersOffset, size = parameterTypes.length; i < size; i++) {
jsonAdapters[i - adaptersOffset] = moshi.adapter(
((ParameterizedType) parameterTypes[i]).getActualTypeArguments()[0],
jsonAnnotations(parameterAnnotations[i]));
Type type = ((ParameterizedType) parameterTypes[i]).getActualTypeArguments()[0];
Set<? extends Annotation> jsonAnnotations = jsonAnnotations(parameterAnnotations[i]);
jsonAdapters[i - adaptersOffset] =
Types.equals(this.type, type) && annotations.equals(jsonAnnotations)
? moshi.nextAdapter(factory, type, jsonAnnotations)
: moshi.adapter(type, jsonAnnotations);
}
}
}
public void toJson(Moshi moshi, JsonWriter writer, Object value)
public void toJson(Moshi moshi, JsonWriter writer, @Nullable Object value)
throws IOException, InvocationTargetException {
throw new AssertionError();
}
public Object fromJson(Moshi moshi, JsonReader reader)
public @Nullable Object fromJson(Moshi moshi, JsonReader reader)
throws IOException, InvocationTargetException {
throw new AssertionError();
}
/** Invoke the method with one fixed argument, plus any number of JSON adapter arguments. */
protected Object invoke(Object a1) throws InvocationTargetException {
protected @Nullable Object invoke(@Nullable Object a1) throws InvocationTargetException {
Object[] args = new Object[1 + jsonAdapters.length];
args[0] = a1;
System.arraycopy(jsonAdapters, 0, args, 1, jsonAdapters.length);
@ -321,7 +339,8 @@ final class AdapterMethodsFactory implements JsonAdapter.Factory {
}
/** Invoke the method with two fixed arguments, plus any number of JSON adapter arguments. */
protected Object invoke(Object a1, Object a2) throws InvocationTargetException {
protected Object invoke(@Nullable Object a1, @Nullable Object a2)
throws InvocationTargetException {
Object[] args = new Object[2 + jsonAdapters.length];
args[0] = a1;
args[1] = a2;

View file

@ -22,6 +22,7 @@ import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import javax.annotation.Nullable;
/**
* Converts arrays to JSON arrays containing their converted contents. This
@ -29,7 +30,7 @@ import java.util.Set;
*/
final class ArrayJsonAdapter extends JsonAdapter<Object> {
public static final Factory FACTORY = new Factory() {
@Override public JsonAdapter<?> create(
@Override public @Nullable JsonAdapter<?> create(
Type type, Set<? extends Annotation> annotations, Moshi moshi) {
Type elementType = Types.arrayComponentType(type);
if (elementType == null) return null;

View file

@ -15,6 +15,7 @@
*/
package com.squareup.moshi;
import com.squareup.moshi.internal.Util;
import java.io.ObjectInputStream;
import java.io.ObjectStreamClass;
import java.lang.reflect.Constructor;
@ -103,7 +104,7 @@ abstract class ClassFactory<T> {
} catch (IllegalAccessException e) {
throw new AssertionError();
} catch (InvocationTargetException e) {
throw new RuntimeException(e);
throw Util.rethrowCause(e);
} catch (NoSuchMethodException ignored) {
// Not the expected version of Dalvik/libcore!
}

View file

@ -15,15 +15,20 @@
*/
package com.squareup.moshi;
import com.squareup.moshi.internal.Util;
import java.io.IOException;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Modifier;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import javax.annotation.Nullable;
import static com.squareup.moshi.internal.Util.resolve;
/**
* Emits a regular class as a JSON object by mapping Java fields to JSON object properties.
@ -42,31 +47,45 @@ import java.util.TreeMap;
*/
final class ClassJsonAdapter<T> extends JsonAdapter<T> {
public static final JsonAdapter.Factory FACTORY = new JsonAdapter.Factory() {
@Override public JsonAdapter<?> create(
@Override public @Nullable JsonAdapter<?> create(
Type type, Set<? extends Annotation> annotations, Moshi moshi) {
if (!(type instanceof Class) && !(type instanceof ParameterizedType)) {
return null;
}
Class<?> rawType = Types.getRawType(type);
if (rawType.isInterface() || rawType.isEnum()) return null;
if (isPlatformType(rawType) && !Types.isAllowedPlatformType(rawType)) {
throw new IllegalArgumentException("Platform "
+ type
+ " annotated "
+ annotations
+ " requires explicit JsonAdapter to be registered");
}
if (!annotations.isEmpty()) return null;
if (Util.isPlatformType(rawType)) {
throw new IllegalArgumentException(
"Platform " + type + " requires explicit JsonAdapter to be registered");
}
if (rawType.isAnonymousClass()) {
throw new IllegalArgumentException("Cannot serialize anonymous class " + rawType.getName());
}
if (rawType.isLocalClass()) {
throw new IllegalArgumentException("Cannot serialize local class " + rawType.getName());
}
if (rawType.getEnclosingClass() != null && !Modifier.isStatic(rawType.getModifiers())) {
if (rawType.getSimpleName().isEmpty()) {
throw new IllegalArgumentException(
"Cannot serialize anonymous class " + rawType.getName());
} else {
throw new IllegalArgumentException(
"Cannot serialize non-static nested class " + rawType.getName());
}
throw new IllegalArgumentException(
"Cannot serialize non-static nested class " + rawType.getName());
}
if (Modifier.isAbstract(rawType.getModifiers())) {
throw new IllegalArgumentException("Cannot serialize abstract class " + rawType.getName());
}
try {
//noinspection unchecked if the Class.forName works, the cast will work.
Class<? extends Annotation> metadataClass =
(Class<? extends Annotation>) Class.forName("kotlin.Metadata");
if (rawType.isAnnotationPresent(metadataClass)) {
throw new IllegalArgumentException("Cannot serialize Kotlin type " + rawType.getName()
+ ". Reflective serialization of Kotlin classes without using kotlin-reflect has "
+ "undefined and unexpected behavior. Please use KotlinJsonAdapter from the "
+ "moshi-kotlin artifact or use code gen from the moshi-kotlin-codegen artifact.");
}
} catch (ClassNotFoundException ignored) {
}
ClassFactory<Object> classFactory = ClassFactory.get(rawType);
Map<String, FieldBinding<?>> fields = new TreeMap<>();
@ -80,21 +99,22 @@ final class ClassJsonAdapter<T> extends JsonAdapter<T> {
private void createFieldBindings(
Moshi moshi, Type type, Map<String, FieldBinding<?>> fieldBindings) {
Class<?> rawType = Types.getRawType(type);
boolean platformType = isPlatformType(rawType);
boolean platformType = Util.isPlatformType(rawType);
for (Field field : rawType.getDeclaredFields()) {
if (!includeField(platformType, field.getModifiers())) continue;
// Look up a type adapter for this type.
Type fieldType = Types.resolve(type, rawType, field.getGenericType());
Type fieldType = resolve(type, rawType, field.getGenericType());
Set<? extends Annotation> annotations = Util.jsonAnnotations(field);
JsonAdapter<Object> adapter = moshi.adapter(fieldType, annotations);
String fieldName = field.getName();
JsonAdapter<Object> adapter = moshi.adapter(fieldType, annotations, fieldName);
// Create the binding between field and JSON.
field.setAccessible(true);
// Store it using the field's name. If there was already a field with this name, fail!
Json jsonAnnotation = field.getAnnotation(Json.class);
String name = jsonAnnotation != null ? jsonAnnotation.name() : field.getName();
String name = jsonAnnotation != null ? jsonAnnotation.name() : fieldName;
FieldBinding<Object> fieldBinding = new FieldBinding<>(name, field, adapter);
FieldBinding<?> replaced = fieldBindings.put(name, fieldBinding);
if (replaced != null) {
@ -105,19 +125,6 @@ final class ClassJsonAdapter<T> extends JsonAdapter<T> {
}
}
/**
* Returns true if {@code rawType} is built in. We don't reflect on private fields of platform
* types because they're unspecified and likely to be different on Java vs. Android.
*/
private boolean isPlatformType(Class<?> rawType) {
String name = rawType.getName();
return name.startsWith("android.")
|| name.startsWith("java.")
|| name.startsWith("javax.")
|| name.startsWith("kotlin.")
|| name.startsWith("scala.");
}
/** Returns true if fields with {@code modifiers} are included in the emitted JSON. */
private boolean includeField(boolean platformType, int modifiers) {
if (Modifier.isStatic(modifiers) || Modifier.isTransient(modifiers)) return false;
@ -143,10 +150,7 @@ final class ClassJsonAdapter<T> extends JsonAdapter<T> {
} catch (InstantiationException e) {
throw new RuntimeException(e);
} catch (InvocationTargetException e) {
Throwable targetException = e.getTargetException();
if (targetException instanceof RuntimeException) throw (RuntimeException) targetException;
if (targetException instanceof Error) throw (Error) targetException;
throw new RuntimeException(targetException);
throw Util.rethrowCause(e);
} catch (IllegalAccessException e) {
throw new AssertionError();
}
@ -155,15 +159,12 @@ final class ClassJsonAdapter<T> extends JsonAdapter<T> {
reader.beginObject();
while (reader.hasNext()) {
int index = reader.selectName(options);
FieldBinding<?> fieldBinding;
if (index != -1) {
fieldBinding = fieldsArray[index];
} else {
reader.nextName();
if (index == -1) {
reader.skipName();
reader.skipValue();
continue;
}
fieldBinding.read(reader, result);
fieldsArray[index].read(reader, result);
}
reader.endObject();
return result;
@ -194,7 +195,7 @@ final class ClassJsonAdapter<T> extends JsonAdapter<T> {
final Field field;
final JsonAdapter<T> adapter;
public FieldBinding(String name, Field field, JsonAdapter<T> adapter) {
FieldBinding(String name, Field field, JsonAdapter<T> adapter) {
this.name = name;
this.field = field;
this.adapter = adapter;

View file

@ -23,11 +23,12 @@ import java.util.Collection;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import javax.annotation.Nullable;
/** Converts collection types to JSON arrays containing their converted contents. */
abstract class CollectionJsonAdapter<C extends Collection<T>, T> extends JsonAdapter<C> {
public static final JsonAdapter.Factory FACTORY = new JsonAdapter.Factory() {
@Override public JsonAdapter<?> create(
@Override public @Nullable JsonAdapter<?> create(
Type type, Set<? extends Annotation> annotations, Moshi moshi) {
Class<?> rawType = Types.getRawType(type);
if (!annotations.isEmpty()) return null;

View file

@ -19,12 +19,23 @@ import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
/** Customizes how a field is encoded as JSON. */
@Target({FIELD, METHOD})
/**
* Customizes how a field is encoded as JSON.
*
* <p>Although this annotation doesn't declare a {@link Target}, it is only honored in the following
* elements:
*
* <ul>
* <li><strong>Java class fields</strong>
* <li><strong>Kotlin properties</strong> for use with {@code moshi-kotlin}. This includes both
* properties declared in the constructor and properties declared as members.
* </ul>
*
* <p>Users of the <a href="https://github.com/rharter/auto-value-moshi">AutoValue: Moshi
* Extension</a> may also use this annotation on abstract getters.
*/
@Retention(RUNTIME)
@Documented
public @interface Json {

View file

@ -15,11 +15,14 @@
*/
package com.squareup.moshi;
import com.squareup.moshi.internal.NullSafeJsonAdapter;
import java.io.IOException;
import java.lang.annotation.Annotation;
import java.lang.reflect.Type;
import java.math.BigDecimal;
import java.util.Set;
import javax.annotation.CheckReturnValue;
import javax.annotation.Nullable;
import okio.Buffer;
import okio.BufferedSink;
import okio.BufferedSource;
@ -28,24 +31,29 @@ import okio.BufferedSource;
* Converts Java values to JSON, and JSON values to Java.
*/
public abstract class JsonAdapter<T> {
public abstract T fromJson(JsonReader reader) throws IOException;
@CheckReturnValue public abstract @Nullable T fromJson(JsonReader reader) throws IOException;
public final T fromJson(BufferedSource source) throws IOException {
@CheckReturnValue public final @Nullable T fromJson(BufferedSource source) throws IOException {
return fromJson(JsonReader.of(source));
}
public final T fromJson(String string) throws IOException {
return fromJson(new Buffer().writeUtf8(string));
@CheckReturnValue public final @Nullable T fromJson(String string) throws IOException {
JsonReader reader = JsonReader.of(new Buffer().writeUtf8(string));
T result = fromJson(reader);
if (!isLenient() && reader.peek() != JsonReader.Token.END_DOCUMENT) {
throw new JsonDataException("JSON document was not fully consumed.");
}
return result;
}
public abstract void toJson(JsonWriter writer, T value) throws IOException;
public abstract void toJson(JsonWriter writer, @Nullable T value) throws IOException;
public final void toJson(BufferedSink sink, T value) throws IOException {
public final void toJson(BufferedSink sink, @Nullable T value) throws IOException {
JsonWriter writer = JsonWriter.of(sink);
toJson(writer, value);
}
public final String toJson(T value) {
@CheckReturnValue public final String toJson(@Nullable T value) {
Buffer buffer = new Buffer();
try {
toJson(buffer, value);
@ -58,14 +66,14 @@ public abstract class JsonAdapter<T> {
/**
* Encodes {@code value} as a Java value object comprised of maps, lists, strings, numbers,
* booleans, and nulls.
*
*
* <p>Values encoded using {@code value(double)} or {@code value(long)} are modeled with the
* corresponding boxed type. Values encoded using {@code value(Number)} are modeled as a
* {@link Long} for boxed integer types ({@link Byte}, {@link Short}, {@link Integer}, and {@link
* Long}), as a {@link Double} for boxed floating point types ({@link Float} and {@link Double}),
* and as a {@link BigDecimal} for all other types.
*/
public final Object toJsonValue(T value) {
@CheckReturnValue public final @Nullable Object toJsonValue(@Nullable T value) {
JsonValueWriter writer = new JsonValueWriter();
try {
toJson(writer, value);
@ -79,7 +87,7 @@ public abstract class JsonAdapter<T> {
* Decodes a Java value object from {@code value}, which must be comprised of maps, lists,
* strings, numbers, booleans and nulls.
*/
public final T fromJsonValue(Object value) {
@CheckReturnValue public final @Nullable T fromJsonValue(@Nullable Object value) {
JsonValueReader reader = new JsonValueReader(value);
try {
return fromJson(reader);
@ -92,13 +100,13 @@ public abstract class JsonAdapter<T> {
* Returns a JSON adapter equal to this JSON adapter, but that serializes nulls when encoding
* JSON.
*/
public final JsonAdapter<T> serializeNulls() {
@CheckReturnValue public final JsonAdapter<T> serializeNulls() {
final JsonAdapter<T> delegate = this;
return new JsonAdapter<T>() {
@Override public T fromJson(JsonReader reader) throws IOException {
@Override public @Nullable T fromJson(JsonReader reader) throws IOException {
return delegate.fromJson(reader);
}
@Override public void toJson(JsonWriter writer, T value) throws IOException {
@Override public void toJson(JsonWriter writer, @Nullable T value) throws IOException {
boolean serializeNulls = writer.getSerializeNulls();
writer.setSerializeNulls(true);
try {
@ -107,6 +115,9 @@ public abstract class JsonAdapter<T> {
writer.setSerializeNulls(serializeNulls);
}
}
@Override boolean isLenient() {
return delegate.isLenient();
}
@Override public String toString() {
return delegate + ".serializeNulls()";
}
@ -117,34 +128,48 @@ public abstract class JsonAdapter<T> {
* Returns a JSON adapter equal to this JSON adapter, but with support for reading and writing
* nulls.
*/
public final JsonAdapter<T> nullSafe() {
@CheckReturnValue public final JsonAdapter<T> nullSafe() {
return new NullSafeJsonAdapter<>(this);
}
/**
* Returns a JSON adapter equal to this JSON adapter, but that refuses null values. If null is
* read or written this will throw a {@link JsonDataException}.
*
* <p>Note that this adapter will not usually be invoked for absent values and so those must be
* handled elsewhere. This should only be used to fail on explicit nulls.
*/
@CheckReturnValue public final JsonAdapter<T> nonNull() {
final JsonAdapter<T> delegate = this;
return new JsonAdapter<T>() {
@Override public T fromJson(JsonReader reader) throws IOException {
@Override public @Nullable T fromJson(JsonReader reader) throws IOException {
if (reader.peek() == JsonReader.Token.NULL) {
return reader.nextNull();
throw new JsonDataException("Unexpected null at " + reader.getPath());
} else {
return delegate.fromJson(reader);
}
}
@Override public void toJson(JsonWriter writer, T value) throws IOException {
@Override public void toJson(JsonWriter writer, @Nullable T value) throws IOException {
if (value == null) {
writer.nullValue();
throw new JsonDataException("Unexpected null at " + writer.getPath());
} else {
delegate.toJson(writer, value);
}
}
@Override boolean isLenient() {
return delegate.isLenient();
}
@Override public String toString() {
return delegate + ".nullSafe()";
return delegate + ".nonNull()";
}
};
}
/** Returns a JSON adapter equal to this, but is lenient when reading and writing. */
public final JsonAdapter<T> lenient() {
@CheckReturnValue public final JsonAdapter<T> lenient() {
final JsonAdapter<T> delegate = this;
return new JsonAdapter<T>() {
@Override public T fromJson(JsonReader reader) throws IOException {
@Override public @Nullable T fromJson(JsonReader reader) throws IOException {
boolean lenient = reader.isLenient();
reader.setLenient(true);
try {
@ -153,7 +178,7 @@ public abstract class JsonAdapter<T> {
reader.setLenient(lenient);
}
}
@Override public void toJson(JsonWriter writer, T value) throws IOException {
@Override public void toJson(JsonWriter writer, @Nullable T value) throws IOException {
boolean lenient = writer.isLenient();
writer.setLenient(true);
try {
@ -162,6 +187,9 @@ public abstract class JsonAdapter<T> {
writer.setLenient(lenient);
}
}
@Override boolean isLenient() {
return true;
}
@Override public String toString() {
return delegate + ".lenient()";
}
@ -170,14 +198,14 @@ public abstract class JsonAdapter<T> {
/**
* Returns a JSON adapter equal to this, but that throws a {@link JsonDataException} when
* {@linkplain JsonReader#setFailOnUnknown(boolean) unknown values} are encountered. This
* constraint applies to both the top-level message handled by this type adapter as well as to
* nested messages.
* {@linkplain JsonReader#setFailOnUnknown(boolean) unknown names and values} are encountered.
* This constraint applies to both the top-level message handled by this type adapter as well as
* to nested messages.
*/
public final JsonAdapter<T> failOnUnknown() {
@CheckReturnValue public final JsonAdapter<T> failOnUnknown() {
final JsonAdapter<T> delegate = this;
return new JsonAdapter<T>() {
@Override public T fromJson(JsonReader reader) throws IOException {
@Override public @Nullable T fromJson(JsonReader reader) throws IOException {
boolean skipForbidden = reader.failOnUnknown();
reader.setFailOnUnknown(true);
try {
@ -186,9 +214,12 @@ public abstract class JsonAdapter<T> {
reader.setFailOnUnknown(skipForbidden);
}
}
@Override public void toJson(JsonWriter writer, T value) throws IOException {
@Override public void toJson(JsonWriter writer, @Nullable T value) throws IOException {
delegate.toJson(writer, value);
}
@Override boolean isLenient() {
return delegate.isLenient();
}
@Override public String toString() {
return delegate + ".failOnUnknown()";
}
@ -203,13 +234,16 @@ public abstract class JsonAdapter<T> {
*
* @param indent a string containing only whitespace.
*/
public JsonAdapter<T> indent(final String indent) {
@CheckReturnValue public JsonAdapter<T> indent(final String indent) {
if (indent == null) {
throw new NullPointerException("indent == null");
}
final JsonAdapter<T> delegate = this;
return new JsonAdapter<T>() {
@Override public T fromJson(JsonReader reader) throws IOException {
@Override public @Nullable T fromJson(JsonReader reader) throws IOException {
return delegate.fromJson(reader);
}
@Override public void toJson(JsonWriter writer, T value) throws IOException {
@Override public void toJson(JsonWriter writer, @Nullable T value) throws IOException {
String originalIndent = writer.getIndent();
writer.setIndent(indent);
try {
@ -218,21 +252,29 @@ public abstract class JsonAdapter<T> {
writer.setIndent(originalIndent);
}
}
@Override boolean isLenient() {
return delegate.isLenient();
}
@Override public String toString() {
return delegate + ".indent(\"" + indent + "\")";
}
};
}
boolean isLenient() {
return false;
}
public interface Factory {
/**
* Attempts to create an adapter for {@code type} annotated with {@code annotations}. This
* returns the adapter if one was created, or null if this factory isn't capable of creating
* such an adapter.
*
* <p>Implementations may use to {@link Moshi#adapter} to compose adapters of other types, or
* <p>Implementations may use {@link Moshi#adapter} to compose adapters of other types, or
* {@link Moshi#nextAdapter} to delegate to the underlying adapter of the same type.
*/
JsonAdapter<?> create(Type type, Set<? extends Annotation> annotations, Moshi moshi);
@CheckReturnValue
@Nullable JsonAdapter<?> create(Type type, Set<? extends Annotation> annotations, Moshi moshi);
}
}

View file

@ -0,0 +1,81 @@
/*
* Copyright (C) 2018 Square, Inc.
*
* 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 com.squareup.moshi;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.reflect.Type;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
/**
* Customizes how a type is encoded as JSON.
*/
@Retention(RUNTIME)
@Documented
public @interface JsonClass {
/**
* True to trigger the annotation processor to generate an adapter for this type.
*
* <p>There are currently some restrictions on which types that can be used with generated
* adapters:
* <ul>
* <li>
* The class must be implemented in Kotlin (unless using a custom generator, see
* {@link #generator()}).
* </li>
* <li>The class may not be an abstract class, an inner class, or a local class.</li>
* <li>All superclasses must be implemented in Kotlin.</li>
* <li>All properties must be public, protected, or internal.</li>
* <li>All properties must be either non-transient or have a default value.</li>
* </ul>
*/
boolean generateAdapter();
/**
* An optional custom generator tag used to indicate which generator should be used. If empty,
* Moshi's annotation processor will generate an adapter for the annotated type. If not empty,
* Moshi's processor will skip it and defer to a custom generator. This can be used to allow
* other custom code generation tools to run and still allow Moshi to read their generated
* JsonAdapter outputs.
*
* <p>Requirements for generated adapter class signatures:
* <ul>
* <li>
* The generated adapter must subclass {@link JsonAdapter} and be parameterized by this type.
* </li>
* <li>
* {@link Types#generatedJsonAdapterName} should be used for the fully qualified class name in
* order for Moshi to correctly resolve and load the generated JsonAdapter.
* </li>
* <li>The first parameter must be a {@link Moshi} instance.</li>
* <li>
* If generic, a second {@link Type[]} parameter should be declared to accept type arguments.
* </li>
* </ul>
*
* <p>Example for a class "CustomType":<pre>{@code
* class CustomTypeJsonAdapter(moshi: Moshi, types: Array<Type>) : JsonAdapter<CustomType>() {
* // ...
* }
* }</pre>
*
* <p>To help ensure your own generator meets requirements above, you can use Moshis built-in
* generator to create the API signature to get started, then make your own generator match that
* expected signature.
*/
String generator() default "";
}

View file

@ -15,6 +15,8 @@
*/
package com.squareup.moshi;
import javax.annotation.Nullable;
/**
* Thrown when the data in a JSON document doesn't match the data expected by the caller. For
* example, suppose the application expects a boolean but the JSON document contains a string. When
@ -31,15 +33,15 @@ public final class JsonDataException extends RuntimeException {
public JsonDataException() {
}
public JsonDataException(String message) {
public JsonDataException(@Nullable String message) {
super(message);
}
public JsonDataException(Throwable cause) {
public JsonDataException(@Nullable Throwable cause) {
super(cause);
}
public JsonDataException(String message, Throwable cause) {
public JsonDataException(@Nullable String message, @Nullable Throwable cause) {
super(message, cause);
}
}

View file

@ -16,10 +16,11 @@
package com.squareup.moshi;
import java.io.IOException;
import javax.annotation.Nullable;
/** Thrown when the data being parsed is not encoded as valid JSON. */
public final class JsonEncodingException extends IOException {
public JsonEncodingException(String message) {
public JsonEncodingException(@Nullable String message) {
super(message);
}
}

View file

@ -18,8 +18,11 @@ package com.squareup.moshi;
import java.io.Closeable;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import javax.annotation.CheckReturnValue;
import javax.annotation.Nullable;
import okio.Buffer;
import okio.BufferedSource;
import okio.ByteString;
@ -118,7 +121,7 @@ import okio.ByteString;
* id = reader.nextLong();
* } else if (name.equals("text")) {
* text = reader.nextString();
* } else if (name.equals("geo") && reader.peek() != JsonToken.NULL) {
* } else if (name.equals("geo") && reader.peek() != Token.NULL) {
* geo = readDoublesArray(reader);
* } else if (name.equals("user")) {
* user = readUser(reader);
@ -174,32 +177,50 @@ import okio.ByteString;
* of this class are not thread safe.
*/
public abstract class JsonReader implements Closeable {
// The nesting stack. Using a manual array rather than an ArrayList saves 20%. This stack permits
// up to 32 levels of nesting including the top-level document. Deeper nesting is prone to trigger
// StackOverflowErrors.
int stackSize = 0;
final int[] scopes = new int[32];
final String[] pathNames = new String[32];
final int[] pathIndices = new int[32];
// The nesting stack. Using a manual array rather than an ArrayList saves 20%. This stack will
// grow itself up to 256 levels of nesting including the top-level document. Deeper nesting is
// prone to trigger StackOverflowErrors.
int stackSize;
int[] scopes;
String[] pathNames;
int[] pathIndices;
/** True to accept non-spec compliant JSON */
/** True to accept non-spec compliant JSON. */
boolean lenient;
/** True to throw a {@link JsonDataException} on any attempt to call {@link #skipValue()}. */
boolean failOnUnknown;
/** Returns a new instance that reads UTF-8 encoded JSON from {@code source}. */
public static JsonReader of(BufferedSource source) {
@CheckReturnValue public static JsonReader of(BufferedSource source) {
return new JsonUtf8Reader(source);
}
// Package-private to control subclasses.
JsonReader() {
// Package-private to control subclasses.
scopes = new int[32];
pathNames = new String[32];
pathIndices = new int[32];
}
// Package-private to control subclasses.
JsonReader(JsonReader copyFrom) {
this.stackSize = copyFrom.stackSize;
this.scopes = copyFrom.scopes.clone();
this.pathNames = copyFrom.pathNames.clone();
this.pathIndices = copyFrom.pathIndices.clone();
this.lenient = copyFrom.lenient;
this.failOnUnknown = copyFrom.failOnUnknown;
}
final void pushScope(int newTop) {
if (stackSize == scopes.length) {
throw new JsonDataException("Nesting too deep at " + getPath());
if (stackSize == 256) {
throw new JsonDataException("Nesting too deep at " + getPath());
}
scopes = Arrays.copyOf(scopes, scopes.length * 2);
pathNames = Arrays.copyOf(pathNames, pathNames.length * 2);
pathIndices = Arrays.copyOf(pathIndices, pathIndices.length * 2);
}
scopes[stackSize++] = newTop;
}
@ -212,7 +233,7 @@ public abstract class JsonReader implements Closeable {
throw new JsonEncodingException(message + " at path " + getPath());
}
final JsonDataException typeMismatch(Object value, Object expected) {
final JsonDataException typeMismatch(@Nullable Object value, Object expected) {
if (value == null) {
return new JsonDataException(
"Expected " + expected + " but was null at path " + getPath());
@ -254,7 +275,7 @@ public abstract class JsonReader implements Closeable {
/**
* Returns true if this parser is liberal in what it accepts.
*/
public final boolean isLenient() {
@CheckReturnValue public final boolean isLenient() {
return lenient;
}
@ -271,9 +292,9 @@ public abstract class JsonReader implements Closeable {
}
/**
* Returns true if this parser forbids skipping values.
* Returns true if this parser forbids skipping names and values.
*/
public final boolean failOnUnknown() {
@CheckReturnValue public final boolean failOnUnknown() {
return failOnUnknown;
}
@ -304,25 +325,34 @@ public abstract class JsonReader implements Closeable {
/**
* Returns true if the current array or object has another element.
*/
public abstract boolean hasNext() throws IOException;
@CheckReturnValue public abstract boolean hasNext() throws IOException;
/**
* Returns the type of the next token without consuming it.
*/
public abstract Token peek() throws IOException;
@CheckReturnValue public abstract Token peek() throws IOException;
/**
* Returns the next token, a {@linkplain Token#NAME property name}, and consumes it.
*
* @throws JsonDataException if the next token in the stream is not a property name.
*/
public abstract String nextName() throws IOException;
@CheckReturnValue public abstract String nextName() throws IOException;
/**
* If the next token is a {@linkplain Token#NAME property name} that's in {@code options}, this
* consumes it and returns its index. Otherwise this returns -1 and no name is consumed.
*/
public abstract int selectName(Options options) throws IOException;
@CheckReturnValue public abstract int selectName(Options options) throws IOException;
/**
* Skips the next token, consuming it. This method is intended for use when the JSON token stream
* contains unrecognized or unhandled names.
*
* <p>This throws a {@link JsonDataException} if this parser has been configured to {@linkplain
* #failOnUnknown fail on unknown} names.
*/
public abstract void skipName() throws IOException;
/**
* Returns the {@linkplain Token#STRING string} value of the next token, consuming it. If the next
@ -336,7 +366,7 @@ public abstract class JsonReader implements Closeable {
* If the next token is a {@linkplain Token#STRING string} that's in {@code options}, this
* consumes it and returns its index. Otherwise this returns -1 and no string is consumed.
*/
public abstract int selectString(Options options) throws IOException;
@CheckReturnValue public abstract int selectString(Options options) throws IOException;
/**
* Returns the {@linkplain Token#BOOLEAN boolean} value of the next token, consuming it.
@ -351,7 +381,7 @@ public abstract class JsonReader implements Closeable {
*
* @throws JsonDataException if the next token is not null or if this reader is closed.
*/
public abstract <T> T nextNull() throws IOException;
public abstract @Nullable <T> T nextNull() throws IOException;
/**
* Returns the {@linkplain Token#NUMBER double} value of the next token, consuming it. If the next
@ -400,7 +430,7 @@ public abstract class JsonReader implements Closeable {
* @throws JsonDataException if the next token is not a literal value, if a JSON object has a
* duplicate key.
*/
public final Object readJsonValue() throws IOException {
public final @Nullable Object readJsonValue() throws IOException {
switch (peek()) {
case BEGIN_ARRAY:
List<Object> list = new ArrayList<>();
@ -444,11 +474,37 @@ public abstract class JsonReader implements Closeable {
}
}
/**
* Returns a new {@code JsonReader} that can read data from this {@code JsonReader} without
* consuming it. The returned reader becomes invalid once this one is next read or closed.
*
* <p>For example, we can use {@code peekJson()} to lookahead and read the same data multiple
* times.
*
* <pre> {@code
*
* Buffer buffer = new Buffer();
* buffer.writeUtf8("[123, 456, 789]")
*
* JsonReader jsonReader = JsonReader.of(buffer);
* jsonReader.beginArray();
* jsonReader.nextInt(); // Returns 123, reader contains 456, 789 and ].
*
* JsonReader peek = reader.peekJson();
* peek.nextInt() // Returns 456.
* peek.nextInt() // Returns 789.
* peek.endArray()
*
* jsonReader.nextInt() // Returns 456, reader contains 789 and ].
* }</pre>
*/
@CheckReturnValue public abstract JsonReader peekJson();
/**
* Returns a <a href="http://goessner.net/articles/JsonPath/">JsonPath</a> to
* the current location in the JSON value.
*/
public final String getPath() {
@CheckReturnValue public final String getPath() {
return JsonScope.getPath(stackSize, scopes, pathNames, pathIndices);
}
@ -471,7 +527,7 @@ public abstract class JsonReader implements Closeable {
this.doubleQuoteSuffix = doubleQuoteSuffix;
}
public static Options of(String... strings) {
@CheckReturnValue public static Options of(String... strings) {
try {
ByteString[] result = new ByteString[strings.length];
Buffer buffer = new Buffer();

View file

@ -17,6 +17,8 @@ package com.squareup.moshi;
/** Lexical scoping elements within a JSON reader or writer. */
final class JsonScope {
private JsonScope() {
}
/** An array with no elements requires no separators or newlines before it is closed. */
static final int EMPTY_ARRAY = 1;

View file

@ -18,6 +18,7 @@ package com.squareup.moshi;
import java.io.EOFException;
import java.io.IOException;
import java.math.BigDecimal;
import javax.annotation.Nullable;
import okio.Buffer;
import okio.BufferedSource;
import okio.ByteString;
@ -30,6 +31,7 @@ final class JsonUtf8Reader extends JsonReader {
private static final ByteString UNQUOTED_STRING_TERMINALS
= ByteString.encodeUtf8("{}[]:, \n\t\r\f/\\;#=");
private static final ByteString LINEFEED_OR_CARRIAGE_RETURN = ByteString.encodeUtf8("\n\r");
private static final ByteString CLOSING_BLOCK_COMMENT = ByteString.encodeUtf8("*/");
private static final int PEEKED_NONE = 0;
private static final int PEEKED_BEGIN_OBJECT = 1;
@ -76,8 +78,7 @@ final class JsonUtf8Reader extends JsonReader {
private long peekedLong;
/**
* The number of characters in a peeked number literal. Increment 'pos' by
* this after reading a number.
* The number of characters in a peeked number literal.
*/
private int peekedNumberLength;
@ -86,17 +87,38 @@ final class JsonUtf8Reader extends JsonReader {
* This is populated before a numeric value is parsed and used if that parsing
* fails.
*/
private String peekedString;
private @Nullable String peekedString;
JsonUtf8Reader(BufferedSource source) {
if (source == null) {
throw new NullPointerException("source == null");
}
this.source = source;
this.buffer = source.buffer();
this.buffer = source.getBuffer();
pushScope(JsonScope.EMPTY_DOCUMENT);
}
/** Copy-constructor makes a deep copy for peeking. */
JsonUtf8Reader(JsonUtf8Reader copyFrom) {
super(copyFrom);
BufferedSource sourcePeek = copyFrom.source.peek();
this.source = sourcePeek;
this.buffer = sourcePeek.getBuffer();
this.peeked = copyFrom.peeked;
this.peekedLong = copyFrom.peekedLong;
this.peekedNumberLength = copyFrom.peekedNumberLength;
this.peekedString = copyFrom.peekedString;
// Make sure our buffer has as many bytes as the source's buffer. This is necessary because
// JsonUtf8Reader assumes any data it has peeked (like the peekedNumberLength) are buffered.
try {
sourcePeek.require(copyFrom.buffer.size());
} catch (IOException e) {
throw new AssertionError();
}
}
@Override public void beginArray() throws IOException {
int p = peeked;
if (p == PEEKED_NONE) {
@ -162,7 +184,7 @@ final class JsonUtf8Reader extends JsonReader {
if (p == PEEKED_NONE) {
p = doPeek();
}
return p != PEEKED_END_OBJECT && p != PEEKED_END_ARRAY;
return p != PEEKED_END_OBJECT && p != PEEKED_END_ARRAY && p != PEEKED_EOF;
}
@Override public Token peek() throws IOException {
@ -464,7 +486,8 @@ final class JsonUtf8Reader extends JsonReader {
}
// We've read a complete number. Decide if it's a PEEKED_LONG or a PEEKED_NUMBER.
if (last == NUMBER_CHAR_DIGIT && fitsInLong && (value != Long.MIN_VALUE || negative)) {
if (last == NUMBER_CHAR_DIGIT && fitsInLong && (value != Long.MIN_VALUE || negative)
&& (value != 0 || !negative)) {
peekedLong = negative ? value : -value;
buffer.skip(i);
return peeked = PEEKED_LONG;
@ -561,8 +584,29 @@ final class JsonUtf8Reader extends JsonReader {
return result;
}
@Override public void skipName() throws IOException {
if (failOnUnknown) {
throw new JsonDataException("Cannot skip unexpected " + peek() + " at " + getPath());
}
int p = peeked;
if (p == PEEKED_NONE) {
p = doPeek();
}
if (p == PEEKED_UNQUOTED_NAME) {
skipUnquotedValue();
} else if (p == PEEKED_DOUBLE_QUOTED_NAME) {
skipQuotedValue(DOUBLE_QUOTE_OR_SLASH);
} else if (p == PEEKED_SINGLE_QUOTED_NAME) {
skipQuotedValue(SINGLE_QUOTE_OR_SLASH);
} else if (p != PEEKED_BUFFERED_NAME) {
throw new JsonDataException("Expected a name but was " + peek() + " at path " + getPath());
}
peeked = PEEKED_NONE;
pathNames[stackSize - 1] = "null";
}
/**
* If {@code name} is in {@code options} this consumes it and returns it's index.
* If {@code name} is in {@code options} this consumes it and returns its index.
* Otherwise this returns -1 and no name is consumed.
*/
private int findName(String name, Options options) {
@ -637,7 +681,7 @@ final class JsonUtf8Reader extends JsonReader {
}
/**
* If {@code string} is in {@code options} this consumes it and returns it's index.
* If {@code string} is in {@code options} this consumes it and returns its index.
* Otherwise this returns -1 and no string is consumed.
*/
private int findString(String string, Options options) {
@ -669,7 +713,7 @@ final class JsonUtf8Reader extends JsonReader {
throw new JsonDataException("Expected a boolean but was " + peek() + " at path " + getPath());
}
@Override public <T> T nextNull() throws IOException {
@Override public @Nullable <T> T nextNull() throws IOException {
int p = peeked;
if (p == PEEKED_NONE) {
p = doPeek();
@ -749,7 +793,7 @@ final class JsonUtf8Reader extends JsonReader {
pathIndices[stackSize - 1]++;
return result;
} catch (NumberFormatException ignored) {
// Fall back to parse as a double below.
// Fall back to parse as a BigDecimal below.
}
} else if (p != PEEKED_BUFFERED) {
throw new JsonDataException("Expected a long but was " + peek()
@ -913,11 +957,19 @@ final class JsonUtf8Reader extends JsonReader {
pushScope(JsonScope.EMPTY_OBJECT);
count++;
} else if (p == PEEKED_END_ARRAY) {
stackSize--;
count--;
if (count < 0) {
throw new JsonDataException(
"Expected a value but was " + peek() + " at path " + getPath());
}
stackSize--;
} else if (p == PEEKED_END_OBJECT) {
stackSize--;
count--;
if (count < 0) {
throw new JsonDataException(
"Expected a value but was " + peek() + " at path " + getPath());
}
stackSize--;
} else if (p == PEEKED_UNQUOTED_NAME || p == PEEKED_UNQUOTED) {
skipUnquotedValue();
} else if (p == PEEKED_DOUBLE_QUOTED || p == PEEKED_DOUBLE_QUOTED_NAME) {
@ -926,6 +978,9 @@ final class JsonUtf8Reader extends JsonReader {
skipQuotedValue(SINGLE_QUOTE_OR_SLASH);
} else if (p == PEEKED_NUMBER) {
buffer.skip(peekedNumberLength);
} else if (p == PEEKED_EOF) {
throw new JsonDataException(
"Expected a value but was " + peek() + " at path " + getPath());
}
peeked = PEEKED_NONE;
} while (count != 0);
@ -937,8 +992,7 @@ final class JsonUtf8Reader extends JsonReader {
/**
* Returns the next character in the stream that is neither whitespace nor a
* part of a comment. When this returns, the returned character is always at
* {@code buffer[pos-1]}; this means the caller can always push back the
* returned character by decrementing {@code pos}.
* {@code buffer.getByte(0)}.
*/
private int nextNonWhitespace(boolean throwOnEof) throws IOException {
/*
@ -969,11 +1023,9 @@ final class JsonUtf8Reader extends JsonReader {
// skip a /* c-style comment */
buffer.readByte(); // '/'
buffer.readByte(); // '*'
if (!skipTo("*/")) {
if (!skipToEndOfBlockComment()) {
throw syntaxError("Unterminated comment");
}
buffer.readByte(); // '*'
buffer.readByte(); // '/'
p = 0;
continue;
@ -1022,20 +1074,17 @@ final class JsonUtf8Reader extends JsonReader {
}
/**
* @param toFind a string to search for. Must not contain a newline.
* Skips through the next closing block comment.
*/
private boolean skipTo(String toFind) throws IOException {
outer:
for (; source.request(toFind.length()); ) {
for (int c = 0; c < toFind.length(); c++) {
if (buffer.getByte(c) != toFind.charAt(c)) {
buffer.readByte();
continue outer;
}
}
return true;
}
return false;
private boolean skipToEndOfBlockComment() throws IOException {
long index = source.indexOf(CLOSING_BLOCK_COMMENT);
boolean found = index != -1;
buffer.skip(found ? index + CLOSING_BLOCK_COMMENT.size() : buffer.size());
return found;
}
@Override public JsonReader peekJson() {
return new JsonUtf8Reader(this);
}
@Override public String toString() {

View file

@ -16,7 +16,9 @@
package com.squareup.moshi;
import java.io.IOException;
import javax.annotation.Nullable;
import okio.BufferedSink;
import okio.BufferedSource;
import okio.Sink;
import static com.squareup.moshi.JsonScope.DANGLING_NAME;
@ -76,8 +78,12 @@ final class JsonUtf8Writer extends JsonWriter {
}
@Override public JsonWriter beginArray() throws IOException {
if (promoteValueToName) {
throw new IllegalStateException(
"Array cannot be used as a map key in JSON at path " + getPath());
}
writeDeferredName();
return open(EMPTY_ARRAY, "[");
return open(EMPTY_ARRAY, NONEMPTY_ARRAY, "[");
}
@Override public JsonWriter endArray() throws IOException {
@ -85,8 +91,12 @@ final class JsonUtf8Writer extends JsonWriter {
}
@Override public JsonWriter beginObject() throws IOException {
if (promoteValueToName) {
throw new IllegalStateException(
"Object cannot be used as a map key in JSON at path " + getPath());
}
writeDeferredName();
return open(EMPTY_OBJECT, "{");
return open(EMPTY_OBJECT, NONEMPTY_OBJECT, "{");
}
@Override public JsonWriter endObject() throws IOException {
@ -98,8 +108,15 @@ final class JsonUtf8Writer extends JsonWriter {
* Enters a new scope by appending any necessary whitespace and the given
* bracket.
*/
private JsonWriter open(int empty, String openBracket) throws IOException {
private JsonWriter open(int empty, int nonempty, String openBracket) throws IOException {
if (stackSize == flattenStackSize
&& (scopes[stackSize - 1] == empty || scopes[stackSize - 1] == nonempty)) {
// Cancel this open. Invert the flatten stack size until this is closed.
flattenStackSize = ~flattenStackSize;
return this;
}
beforeValue();
checkStack();
pushScope(empty);
pathIndices[stackSize - 1] = 0;
sink.writeUtf8(openBracket);
@ -118,6 +135,11 @@ final class JsonUtf8Writer extends JsonWriter {
if (deferredName != null) {
throw new IllegalStateException("Dangling name: " + deferredName);
}
if (stackSize == ~flattenStackSize) {
// Cancel this close. Restore the flattenStackSize so we're ready to flatten again!
flattenStackSize = ~flattenStackSize;
return this;
}
stackSize--;
pathNames[stackSize] = null; // Free the last path name so that it can be garbage collected!
@ -136,7 +158,8 @@ final class JsonUtf8Writer extends JsonWriter {
if (stackSize == 0) {
throw new IllegalStateException("JsonWriter is closed.");
}
if (deferredName != null) {
int context = peekScope();
if ((context != EMPTY_OBJECT && context != NONEMPTY_OBJECT) || deferredName != null) {
throw new IllegalStateException("Nesting problem.");
}
deferredName = name;
@ -168,6 +191,10 @@ final class JsonUtf8Writer extends JsonWriter {
}
@Override public JsonWriter nullValue() throws IOException {
if (promoteValueToName) {
throw new IllegalStateException(
"null cannot be used as a map key in JSON at path " + getPath());
}
if (deferredName != null) {
if (serializeNulls) {
writeDeferredName();
@ -183,6 +210,10 @@ final class JsonUtf8Writer extends JsonWriter {
}
@Override public JsonWriter value(boolean value) throws IOException {
if (promoteValueToName) {
throw new IllegalStateException(
"Boolean cannot be used as a map key in JSON at path " + getPath());
}
writeDeferredName();
beforeValue();
sink.writeUtf8(value ? "true" : "false");
@ -222,7 +253,7 @@ final class JsonUtf8Writer extends JsonWriter {
return this;
}
@Override public JsonWriter value(Number value) throws IOException {
@Override public JsonWriter value(@Nullable Number value) throws IOException {
if (value == null) {
return nullValue();
}
@ -242,6 +273,18 @@ final class JsonUtf8Writer extends JsonWriter {
return this;
}
@Override public JsonWriter value(BufferedSource source) throws IOException {
if (promoteValueToName) {
throw new IllegalStateException(
"BufferedSource cannot be used as a map key in JSON at path " + getPath());
}
writeDeferredName();
beforeValue();
sink.writeAll(source);
pathIndices[stackSize - 1]++;
return this;
}
/**
* Ensures all buffered data is written to the underlying {@link Sink}
* and flushes that writer.

View file

@ -20,8 +20,10 @@ import java.math.BigDecimal;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import javax.annotation.Nullable;
import static com.squareup.moshi.JsonScope.CLOSED;
/**
* This class reads a JSON document by traversing a Java object comprising maps, lists, and JSON
@ -31,11 +33,11 @@ import java.util.Map;
* <ul>
* <li>The next element to act upon is on the top of the stack.
* <li>When the top of the stack is a {@link List}, calling {@link #beginArray()} replaces the
* list with a {@link ListIterator}. The first element of the iterator is pushed on top of the
* list with a {@link JsonIterator}. The first element of the iterator is pushed on top of the
* iterator.
* <li>Similarly, when the top of the stack is a {@link Map}, calling {@link #beginObject()}
* replaces the map with an {@link Iterator} of its entries. The first element of the iterator
* is pushed on top of the iterator.
* replaces the map with an {@link JsonIterator} of its entries. The first element of the
* iterator is pushed on top of the iterator.
* <li>When the top of the stack is a {@link Map.Entry}, calling {@link #nextName()} returns the
* entry's key and replaces the entry with its value on the stack.
* <li>When an element is consumed it is popped. If the new top of the stack has a non-exhausted
@ -48,17 +50,31 @@ final class JsonValueReader extends JsonReader {
/** Sentinel object pushed on {@link #stack} when the reader is closed. */
private static final Object JSON_READER_CLOSED = new Object();
private final Object[] stack = new Object[32];
private Object[] stack;
public JsonValueReader(Object root) {
JsonValueReader(Object root) {
scopes[stackSize] = JsonScope.NONEMPTY_DOCUMENT;
stack = new Object[32];
stack[stackSize++] = root;
}
/** Copy-constructor makes a deep copy for peeking. */
JsonValueReader(JsonValueReader copyFrom) {
super(copyFrom);
stack = copyFrom.stack.clone();
for (int i = 0; i < stackSize; i++) {
if (stack[i] instanceof JsonIterator) {
stack[i] = ((JsonIterator) stack[i]).clone();
}
}
}
@Override public void beginArray() throws IOException {
List<?> peeked = require(List.class, Token.BEGIN_ARRAY);
ListIterator<?> iterator = peeked.listIterator();
JsonIterator iterator = new JsonIterator(
Token.END_ARRAY, peeked.toArray(new Object[peeked.size()]), 0);
stack[stackSize - 1] = iterator;
scopes[stackSize - 1] = JsonScope.EMPTY_ARRAY;
pathIndices[stackSize - 1] = 0;
@ -70,8 +86,8 @@ final class JsonValueReader extends JsonReader {
}
@Override public void endArray() throws IOException {
ListIterator<?> peeked = require(ListIterator.class, Token.END_ARRAY);
if (peeked.hasNext()) {
JsonIterator peeked = require(JsonIterator.class, Token.END_ARRAY);
if (peeked.endToken != Token.END_ARRAY || peeked.hasNext()) {
throw typeMismatch(peeked, Token.END_ARRAY);
}
remove();
@ -80,7 +96,8 @@ final class JsonValueReader extends JsonReader {
@Override public void beginObject() throws IOException {
Map<?, ?> peeked = require(Map.class, Token.BEGIN_OBJECT);
Iterator<?> iterator = peeked.entrySet().iterator();
JsonIterator iterator = new JsonIterator(
Token.END_OBJECT, peeked.entrySet().toArray(new Object[peeked.size()]), 0);
stack[stackSize - 1] = iterator;
scopes[stackSize - 1] = JsonScope.EMPTY_OBJECT;
@ -91,8 +108,8 @@ final class JsonValueReader extends JsonReader {
}
@Override public void endObject() throws IOException {
Iterator<?> peeked = require(Iterator.class, Token.END_OBJECT);
if (peeked instanceof ListIterator || peeked.hasNext()) {
JsonIterator peeked = require(JsonIterator.class, Token.END_OBJECT);
if (peeked.endToken != Token.END_OBJECT || peeked.hasNext()) {
throw typeMismatch(peeked, Token.END_OBJECT);
}
pathNames[stackSize - 1] = null;
@ -100,8 +117,7 @@ final class JsonValueReader extends JsonReader {
}
@Override public boolean hasNext() throws IOException {
// TODO(jwilson): this is consistent with BufferedSourceJsonReader but it doesn't make sense.
if (stackSize == 0) return true;
if (stackSize == 0) return false;
Object peeked = stack[stackSize - 1];
return !(peeked instanceof Iterator) || ((Iterator) peeked).hasNext();
@ -112,8 +128,7 @@ final class JsonValueReader extends JsonReader {
// If the top of the stack is an iterator, take its first element and push it on the stack.
Object peeked = stack[stackSize - 1];
if (peeked instanceof ListIterator) return Token.END_ARRAY;
if (peeked instanceof Iterator) return Token.END_OBJECT;
if (peeked instanceof JsonIterator) return ((JsonIterator) peeked).endToken;
if (peeked instanceof List) return Token.BEGIN_ARRAY;
if (peeked instanceof Map) return Token.BEGIN_OBJECT;
if (peeked instanceof Map.Entry) return Token.NAME;
@ -150,16 +165,47 @@ final class JsonValueReader extends JsonReader {
return -1;
}
@Override public void skipName() throws IOException {
if (failOnUnknown) {
throw new JsonDataException("Cannot skip unexpected " + peek() + " at " + getPath());
}
Map.Entry<?, ?> peeked = require(Map.Entry.class, Token.NAME);
// Swap the Map.Entry for its value on the stack.
stack[stackSize - 1] = peeked.getValue();
pathNames[stackSize - 2] = "null";
}
@Override public String nextString() throws IOException {
String peeked = require(String.class, Token.STRING);
remove();
return peeked;
Object peeked = (stackSize != 0 ? stack[stackSize - 1] : null);
if (peeked instanceof String) {
remove();
return (String) peeked;
}
if (peeked instanceof Number) {
remove();
return peeked.toString();
}
if (peeked == JSON_READER_CLOSED) {
throw new IllegalStateException("JsonReader is closed");
}
throw typeMismatch(peeked, Token.STRING);
}
@Override public int selectString(Options options) throws IOException {
String peeked = require(String.class, Token.STRING);
Object peeked = (stackSize != 0 ? stack[stackSize - 1] : null);
if (!(peeked instanceof String)) {
if (peeked == JSON_READER_CLOSED) {
throw new IllegalStateException("JsonReader is closed");
}
return -1;
}
String peekedString = (String) peeked;
for (int i = 0, length = options.strings.length; i < length; i++) {
if (options.strings[i].equals(peeked)) {
if (options.strings[i].equals(peekedString)) {
remove();
return i;
}
@ -173,7 +219,7 @@ final class JsonValueReader extends JsonReader {
return peeked;
}
@Override public <T> T nextNull() throws IOException {
@Override public @Nullable <T> T nextNull() throws IOException {
require(Void.class, Token.NULL);
remove();
return null;
@ -262,6 +308,9 @@ final class JsonValueReader extends JsonReader {
Object skipped = stackSize != 0 ? stack[stackSize - 1] : null;
if (skipped instanceof JsonIterator) {
throw new JsonDataException("Expected a value but was " + peek() + " at path " + getPath());
}
if (skipped instanceof Map.Entry) {
// We're skipping a name. Promote the map entry's value.
Map.Entry<?, ?> entry = (Map.Entry<?, ?>) stack[stackSize - 1];
@ -269,9 +318,15 @@ final class JsonValueReader extends JsonReader {
} else if (stackSize > 0) {
// We're skipping a value.
remove();
} else {
throw new JsonDataException("Expected a value but was " + peek() + " at path " + getPath());
}
}
@Override public JsonReader peekJson() {
return new JsonValueReader(this);
}
@Override void promoteNameToValue() throws IOException {
if (hasNext()) {
String name = nextName();
@ -282,13 +337,19 @@ final class JsonValueReader extends JsonReader {
@Override public void close() throws IOException {
Arrays.fill(stack, 0, stackSize, null);
stack[0] = JSON_READER_CLOSED;
scopes[0] = JsonScope.CLOSED;
scopes[0] = CLOSED;
stackSize = 1;
}
private void push(Object newTop) {
if (stackSize == stack.length) {
throw new JsonDataException("Nesting too deep at " + getPath());
if (stackSize == 256) {
throw new JsonDataException("Nesting too deep at " + getPath());
}
scopes = Arrays.copyOf(scopes, scopes.length * 2);
pathNames = Arrays.copyOf(pathNames, pathNames.length * 2);
pathIndices = Arrays.copyOf(pathIndices, pathIndices.length * 2);
stack = Arrays.copyOf(stack, stack.length * 2);
}
stack[stackSize++] = newTop;
}
@ -297,7 +358,7 @@ final class JsonValueReader extends JsonReader {
* Returns the top of the stack which is required to be a {@code type}. Throws if this reader is
* closed, or if the type isn't what was expected.
*/
private <T> T require(Class<T> type, Token expected) throws IOException {
private @Nullable <T> T require(Class<T> type, Token expected) throws IOException {
Object peeked = (stackSize != 0 ? stack[stackSize - 1] : null);
if (type.isInstance(peeked)) {
@ -337,4 +398,33 @@ final class JsonValueReader extends JsonReader {
}
}
}
static final class JsonIterator implements Iterator<Object>, Cloneable {
final Token endToken;
final Object[] array;
int next;
JsonIterator(Token endToken, Object[] array, int next) {
this.endToken = endToken;
this.array = array;
this.next = next;
}
@Override public boolean hasNext() {
return next < array.length;
}
@Override public Object next() {
return array[next++];
}
@Override public void remove() {
throw new UnsupportedOperationException();
}
@Override protected JsonIterator clone() {
// No need to copy the array; it's read-only.
return new JsonIterator(endToken, array, next);
}
}
}

View file

@ -20,6 +20,8 @@ import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import javax.annotation.Nullable;
import okio.BufferedSource;
import static com.squareup.moshi.JsonScope.EMPTY_ARRAY;
import static com.squareup.moshi.JsonScope.EMPTY_DOCUMENT;
@ -30,8 +32,8 @@ import static java.lang.Double.POSITIVE_INFINITY;
/** Writes JSON by building a Java object comprising maps, lists, and JSON primitives. */
final class JsonValueWriter extends JsonWriter {
private final Object[] stack = new Object[32];
private String deferredName;
Object[] stack = new Object[32];
private @Nullable String deferredName;
JsonValueWriter() {
pushScope(EMPTY_DOCUMENT);
@ -46,9 +48,16 @@ final class JsonValueWriter extends JsonWriter {
}
@Override public JsonWriter beginArray() throws IOException {
if (stackSize == stack.length) {
throw new JsonDataException("Nesting too deep at " + getPath() + ": circular reference?");
if (promoteValueToName) {
throw new IllegalStateException(
"Array cannot be used as a map key in JSON at path " + getPath());
}
if (stackSize == flattenStackSize && scopes[stackSize - 1] == EMPTY_ARRAY) {
// Cancel this open. Invert the flatten stack size until this is closed.
flattenStackSize = ~flattenStackSize;
return this;
}
checkStack();
List<Object> list = new ArrayList<>();
add(list);
stack[stackSize] = list;
@ -61,6 +70,11 @@ final class JsonValueWriter extends JsonWriter {
if (peekScope() != EMPTY_ARRAY) {
throw new IllegalStateException("Nesting problem.");
}
if (stackSize == ~flattenStackSize) {
// Cancel this close. Restore the flattenStackSize so we're ready to flatten again!
flattenStackSize = ~flattenStackSize;
return this;
}
stackSize--;
stack[stackSize] = null;
pathIndices[stackSize - 1]++;
@ -68,9 +82,16 @@ final class JsonValueWriter extends JsonWriter {
}
@Override public JsonWriter beginObject() throws IOException {
if (stackSize == stack.length) {
throw new JsonDataException("Nesting too deep at " + getPath() + ": circular reference?");
if (promoteValueToName) {
throw new IllegalStateException(
"Object cannot be used as a map key in JSON at path " + getPath());
}
if (stackSize == flattenStackSize && scopes[stackSize - 1] == EMPTY_OBJECT) {
// Cancel this open. Invert the flatten stack size until this is closed.
flattenStackSize = ~flattenStackSize;
return this;
}
checkStack();
Map<String, Object> map = new LinkedHashTreeMap<>();
add(map);
stack[stackSize] = map;
@ -79,9 +100,17 @@ final class JsonValueWriter extends JsonWriter {
}
@Override public JsonWriter endObject() throws IOException {
if (peekScope() != EMPTY_OBJECT || deferredName != null) {
if (peekScope() != EMPTY_OBJECT) {
throw new IllegalStateException("Nesting problem.");
}
if (deferredName != null) {
throw new IllegalStateException("Dangling name: " + deferredName);
}
if (stackSize == ~flattenStackSize) {
// Cancel this close. Restore the flattenStackSize so we're ready to flatten again!
flattenStackSize = ~flattenStackSize;
return this;
}
promoteValueToName = false;
stackSize--;
stack[stackSize] = null;
@ -106,7 +135,7 @@ final class JsonValueWriter extends JsonWriter {
return this;
}
@Override public JsonWriter value(String value) throws IOException {
@Override public JsonWriter value(@Nullable String value) throws IOException {
if (promoteValueToName) {
return name(value);
}
@ -116,18 +145,30 @@ final class JsonValueWriter extends JsonWriter {
}
@Override public JsonWriter nullValue() throws IOException {
if (promoteValueToName) {
throw new IllegalStateException(
"null cannot be used as a map key in JSON at path " + getPath());
}
add(null);
pathIndices[stackSize - 1]++;
return this;
}
@Override public JsonWriter value(boolean value) throws IOException {
if (promoteValueToName) {
throw new IllegalStateException(
"Boolean cannot be used as a map key in JSON at path " + getPath());
}
add(value);
pathIndices[stackSize - 1]++;
return this;
}
@Override public JsonWriter value(Boolean value) throws IOException {
@Override public JsonWriter value(@Nullable Boolean value) throws IOException {
if (promoteValueToName) {
throw new IllegalStateException(
"Boolean cannot be used as a map key in JSON at path " + getPath());
}
add(value);
pathIndices[stackSize - 1]++;
return this;
@ -155,7 +196,7 @@ final class JsonValueWriter extends JsonWriter {
return this;
}
@Override public JsonWriter value(Number value) throws IOException {
@Override public JsonWriter value(@Nullable Number value) throws IOException {
// If it's trivially converted to a long, do that.
if (value instanceof Byte
|| value instanceof Short
@ -169,6 +210,10 @@ final class JsonValueWriter extends JsonWriter {
return value(value.doubleValue());
}
if (value == null) {
return nullValue();
}
// Everything else gets converted to a BigDecimal.
BigDecimal bigDecimalValue = value instanceof BigDecimal
? ((BigDecimal) value)
@ -181,6 +226,23 @@ final class JsonValueWriter extends JsonWriter {
return this;
}
@Override public JsonWriter value(BufferedSource source) throws IOException {
if (promoteValueToName) {
throw new IllegalStateException(
"BufferedSource cannot be used as a map key in JSON at path " + getPath());
}
Object value = JsonReader.of(source).readJsonValue();
boolean serializeNulls = this.serializeNulls;
this.serializeNulls = true;
try {
add(value);
} finally {
this.serializeNulls = serializeNulls;
}
pathIndices[stackSize - 1]++;
return this;
}
@Override public void close() throws IOException {
int size = stackSize;
if (size > 1 || size == 1 && scopes[size - 1] != NONEMPTY_DOCUMENT) {
@ -195,7 +257,7 @@ final class JsonValueWriter extends JsonWriter {
}
}
private JsonValueWriter add(Object newTop) {
private JsonValueWriter add(@Nullable Object newTop) {
int scope = peekScope();
if (stackSize == 1) {

View file

@ -18,9 +18,15 @@ package com.squareup.moshi;
import java.io.Closeable;
import java.io.Flushable;
import java.io.IOException;
import java.util.Arrays;
import javax.annotation.CheckReturnValue;
import javax.annotation.Nullable;
import okio.BufferedSink;
import okio.BufferedSource;
import static com.squareup.moshi.JsonScope.EMPTY_ARRAY;
import static com.squareup.moshi.JsonScope.EMPTY_OBJECT;
import static com.squareup.moshi.JsonScope.NONEMPTY_ARRAY;
import static com.squareup.moshi.JsonScope.NONEMPTY_OBJECT;
/**
@ -119,13 +125,13 @@ import static com.squareup.moshi.JsonScope.NONEMPTY_OBJECT;
* malformed JSON string will fail with an {@link IllegalStateException}.
*/
public abstract class JsonWriter implements Closeable, Flushable {
// The nesting stack. Using a manual array rather than an ArrayList saves 20%. This stack permits
// up to 32 levels of nesting including the top-level document. Deeper nesting is prone to trigger
// StackOverflowErrors.
// The nesting stack. Using a manual array rather than an ArrayList saves 20%. This stack will
// grow itself up to 256 levels of nesting including the top-level document. Deeper nesting is
// prone to trigger StackOverflowErrors.
int stackSize = 0;
final int[] scopes = new int[32];
final String[] pathNames = new String[32];
final int[] pathIndices = new int[32];
int[] scopes = new int[32];
String[] pathNames = new String[32];
int[] pathIndices = new int[32];
/**
* A string containing a full set of spaces for a single level of indentation, or null for no
@ -136,8 +142,28 @@ public abstract class JsonWriter implements Closeable, Flushable {
boolean serializeNulls;
boolean promoteValueToName;
/**
* Controls the deepest stack size that has begin/end pairs flattened:
*
* <ul>
* <li>If -1, no begin/end pairs are being suppressed.
* <li>If positive, this is the deepest stack size whose begin/end pairs are eligible to be
* flattened.
* <li>If negative, it is the bitwise inverse (~) of the deepest stack size whose begin/end
* pairs have been flattened.
* </ul>
*
* <p>We differentiate between what layer would be flattened (positive) from what layer is being
* flattened (negative) so that we don't double-flatten.
*
* <p>To accommodate nested flattening we require callers to track the previous state when they
* provide a new state. The previous state is returned from {@link #beginFlatten} and restored
* with {@link #endFlatten}.
*/
int flattenStackSize = -1;
/** Returns a new instance that writes UTF-8 encoded JSON to {@code sink}. */
public static JsonWriter of(BufferedSink sink) {
@CheckReturnValue public static JsonWriter of(BufferedSink sink) {
return new JsonUtf8Writer(sink);
}
@ -153,10 +179,26 @@ public abstract class JsonWriter implements Closeable, Flushable {
return scopes[stackSize - 1];
}
final void pushScope(int newTop) {
if (stackSize == scopes.length) {
/** Before pushing a value on the stack this confirms that the stack has capacity. */
final boolean checkStack() {
if (stackSize != scopes.length) return false;
if (stackSize == 256) {
throw new JsonDataException("Nesting too deep at " + getPath() + ": circular reference?");
}
scopes = Arrays.copyOf(scopes, scopes.length * 2);
pathNames = Arrays.copyOf(pathNames, pathNames.length * 2);
pathIndices = Arrays.copyOf(pathIndices, pathIndices.length * 2);
if (this instanceof JsonValueWriter) {
((JsonValueWriter) this).stack =
Arrays.copyOf(((JsonValueWriter) this).stack, ((JsonValueWriter) this).stack.length * 2);
}
return true;
}
final void pushScope(int newTop) {
scopes[stackSize++] = newTop;
}
@ -181,7 +223,7 @@ public abstract class JsonWriter implements Closeable, Flushable {
* Returns a string containing only whitespace, used for each level of
* indentation. If empty, the encoded document will be compact.
*/
public final String getIndent() {
@CheckReturnValue public final String getIndent() {
return indent != null ? indent : "";
}
@ -204,7 +246,7 @@ public abstract class JsonWriter implements Closeable, Flushable {
/**
* Returns true if this writer has relaxed syntax rules.
*/
public final boolean isLenient() {
@CheckReturnValue public final boolean isLenient() {
return lenient;
}
@ -220,7 +262,7 @@ public abstract class JsonWriter implements Closeable, Flushable {
* Returns true if object members are serialized when their value is null.
* This has no impact on array elements. The default is false.
*/
public final boolean getSerializeNulls() {
@CheckReturnValue public final boolean getSerializeNulls() {
return serializeNulls;
}
@ -257,7 +299,7 @@ public abstract class JsonWriter implements Closeable, Flushable {
/**
* Encodes the property name.
*
* @param name the name of the forthcoming value. May not be null.
* @param name the name of the forthcoming value. Must not be null.
* @return this writer.
*/
public abstract JsonWriter name(String name) throws IOException;
@ -268,7 +310,7 @@ public abstract class JsonWriter implements Closeable, Flushable {
* @param value the literal string value, or null to encode a null literal.
* @return this writer.
*/
public abstract JsonWriter value(String value) throws IOException;
public abstract JsonWriter value(@Nullable String value) throws IOException;
/**
* Encodes {@code null}.
@ -289,7 +331,7 @@ public abstract class JsonWriter implements Closeable, Flushable {
*
* @return this writer.
*/
public abstract JsonWriter value(Boolean value) throws IOException;
public abstract JsonWriter value(@Nullable Boolean value) throws IOException;
/**
* Encodes {@code value}.
@ -314,7 +356,16 @@ public abstract class JsonWriter implements Closeable, Flushable {
* {@linkplain Double#isInfinite() infinities}.
* @return this writer.
*/
public abstract JsonWriter value(Number value) throws IOException;
public abstract JsonWriter value(@Nullable Number value) throws IOException;
/**
* Writes {@code source} directly without encoding its contents.
* Since no validation is performed, {@link #setSerializeNulls} and other writer configurations
* are not respected.
*
* @return this writer.
*/
public abstract JsonWriter value(BufferedSource source) throws IOException;
/**
* Changes the writer to treat the next value as a string name. This is useful for map adapters so
@ -328,11 +379,93 @@ public abstract class JsonWriter implements Closeable, Flushable {
promoteValueToName = true;
}
/**
* Cancels immediately-nested calls to {@link #beginArray()} or {@link #beginObject()} and their
* matching calls to {@link #endArray} or {@link #endObject()}. Use this to compose JSON adapters
* without nesting.
*
* <p>For example, the following creates JSON with nested arrays: {@code [1,[2,3,4],5]}.
*
* <pre>{@code
*
* JsonAdapter<List<Integer>> integersAdapter = ...
*
* public void writeNumbers(JsonWriter writer) {
* writer.beginArray();
* writer.value(1);
* integersAdapter.toJson(writer, Arrays.asList(2, 3, 4));
* writer.value(5);
* writer.endArray();
* }
* }</pre>
*
* <p>With flattening we can create JSON with a single array {@code [1,2,3,4,5]}:
*
* <pre>{@code
*
* JsonAdapter<List<Integer>> integersAdapter = ...
*
* public void writeNumbers(JsonWriter writer) {
* writer.beginArray();
* int token = writer.beginFlatten();
* writer.value(1);
* integersAdapter.toJson(writer, Arrays.asList(2, 3, 4));
* writer.value(5);
* writer.endFlatten(token);
* writer.endArray();
* }
* }</pre>
*
* <p>This method flattens arrays within arrays:
*
* <pre>{@code
*
* Emit: [1, [2, 3, 4], 5]
* To produce: [1, 2, 3, 4, 5]
* }</pre>
*
* It also flattens objects within objects. Do not call {@link #name} before writing a flattened
* object.
*
* <pre>{@code
*
* Emit: {"a": 1, {"b": 2}, "c": 3}
* To Produce: {"a": 1, "b": 2, "c": 3}
* }</pre>
*
* Other combinations are permitted but do not perform flattening. For example, objects inside of
* arrays are not flattened:
*
* <pre>{@code
*
* Emit: [1, {"b": 2}, 3, [4, 5], 6]
* To Produce: [1, {"b": 2}, 3, 4, 5, 6]
* }</pre>
*
* <p>This method returns an opaque token. Callers must match all calls to this method with a call
* to {@link #endFlatten} with the matching token.
*/
@CheckReturnValue public final int beginFlatten() {
int context = peekScope();
if (context != NONEMPTY_OBJECT && context != EMPTY_OBJECT
&& context != NONEMPTY_ARRAY && context != EMPTY_ARRAY) {
throw new IllegalStateException("Nesting problem.");
}
int token = flattenStackSize;
flattenStackSize = stackSize;
return token;
}
/** Ends nested call flattening created by {@link #beginFlatten}. */
public final void endFlatten(int token) {
flattenStackSize = token;
}
/**
* Returns a <a href="http://goessner.net/articles/JsonPath/">JsonPath</a> to
* the current location in the JSON value.
*/
public final String getPath() {
@CheckReturnValue public final String getPath() {
return JsonScope.getPath(stackSize, scopes, pathNames, pathIndices);
}
}

View file

@ -55,7 +55,7 @@ final class LinkedHashTreeMap<K, V> extends AbstractMap<K, V> implements Seriali
* Create a natural order, empty tree map whose keys must be mutually
* comparable and non-null.
*/
public LinkedHashTreeMap() {
LinkedHashTreeMap() {
this(null);
}
@ -66,8 +66,10 @@ final class LinkedHashTreeMap<K, V> extends AbstractMap<K, V> implements Seriali
* @param comparator the comparator to order elements with, or {@code null} to
* use the natural ordering.
*/
@SuppressWarnings({ "unchecked", "rawtypes" }) // unsafe! if comparator is null, this assumes K is comparable
public LinkedHashTreeMap(Comparator<? super K> comparator) {
@SuppressWarnings({
"unchecked", "rawtypes" // Unsafe! if comparator is null, this assumes K is comparable.
})
LinkedHashTreeMap(Comparator<? super K> comparator) {
this.comparator = comparator != null
? comparator
: (Comparator) NATURAL_ORDER;
@ -473,14 +475,14 @@ final class LinkedHashTreeMap<K, V> extends AbstractMap<K, V> implements Seriali
V value;
int height;
/** Create the header entry */
/** Create the header entry. */
Node() {
key = null;
hash = -1;
next = prev = this;
}
/** Create a regular entry */
/** Create a regular entry. */
Node(Node<K, V> parent, K key, int hash, Node<K, V> next, Node<K, V> prev) {
this.parent = parent;
this.key = key;
@ -665,7 +667,7 @@ final class LinkedHashTreeMap<K, V> extends AbstractMap<K, V> implements Seriali
* comparisons. Using this class to create a tree of size <i>S</i> is
* {@code O(S)}.
*/
final static class AvlBuilder<K, V> {
static final class AvlBuilder<K, V> {
/** This stack is a singly linked list, linked by the 'parent' field. */
private Node<K, V> stack;
private int leavesToSkip;

View file

@ -20,6 +20,7 @@ import java.lang.annotation.Annotation;
import java.lang.reflect.Type;
import java.util.Map;
import java.util.Set;
import javax.annotation.Nullable;
/**
* Converts maps with string keys to JSON objects.
@ -28,7 +29,7 @@ import java.util.Set;
*/
final class MapJsonAdapter<K, V> extends JsonAdapter<Map<K, V>> {
public static final Factory FACTORY = new Factory() {
@Override public JsonAdapter<?> create(
@Override public @Nullable JsonAdapter<?> create(
Type type, Set<? extends Annotation> annotations, Moshi moshi) {
if (!annotations.isEmpty()) return null;
Class<?> rawType = Types.getRawType(type);
@ -41,7 +42,7 @@ final class MapJsonAdapter<K, V> extends JsonAdapter<Map<K, V>> {
private final JsonAdapter<K> keyAdapter;
private final JsonAdapter<V> valueAdapter;
public MapJsonAdapter(Moshi moshi, Type keyType, Type valueType) {
MapJsonAdapter(Moshi moshi, Type keyType, Type valueType) {
this.keyAdapter = moshi.adapter(keyType);
this.valueAdapter = moshi.adapter(valueType);
}

View file

@ -15,16 +15,27 @@
*/
package com.squareup.moshi;
import com.squareup.moshi.internal.Util;
import java.io.IOException;
import java.lang.annotation.Annotation;
import java.lang.reflect.Type;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Deque;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.annotation.CheckReturnValue;
import javax.annotation.Nullable;
import static com.squareup.moshi.internal.Util.canonicalize;
import static com.squareup.moshi.internal.Util.removeSubtypeWildcard;
import static com.squareup.moshi.internal.Util.typeAnnotatedWithAnnotations;
/**
* Coordinates binding between JSON values and Java objects.
@ -41,7 +52,7 @@ public final class Moshi {
}
private final List<JsonAdapter.Factory> factories;
private final ThreadLocal<List<DeferredAdapter<?>>> reentrantCalls = new ThreadLocal<>();
private final ThreadLocal<LookupChain> lookupChainThreadLocal = new ThreadLocal<>();
private final Map<Object, JsonAdapter<?>> adapterCache = new LinkedHashMap<>();
Moshi(Builder builder) {
@ -53,22 +64,56 @@ public final class Moshi {
}
/** Returns a JSON adapter for {@code type}, creating it if necessary. */
public <T> JsonAdapter<T> adapter(Type type) {
@CheckReturnValue public <T> JsonAdapter<T> adapter(Type type) {
return adapter(type, Util.NO_ANNOTATIONS);
}
public <T> JsonAdapter<T> adapter(Class<T> type) {
@CheckReturnValue public <T> JsonAdapter<T> adapter(Class<T> type) {
return adapter(type, Util.NO_ANNOTATIONS);
}
@CheckReturnValue
public <T> JsonAdapter<T> adapter(Type type, Class<? extends Annotation> annotationType) {
if (annotationType == null) {
throw new NullPointerException("annotationType == null");
}
return adapter(type,
Collections.singleton(Types.createJsonQualifierImplementation(annotationType)));
}
@SuppressWarnings("unchecked") // Factories are required to return only matching JsonAdapters.
@CheckReturnValue
public <T> JsonAdapter<T> adapter(Type type, Class<? extends Annotation>... annotationTypes) {
if (annotationTypes.length == 1) {
return adapter(type, annotationTypes[0]);
}
Set<Annotation> annotations = new LinkedHashSet<>(annotationTypes.length);
for (Class<? extends Annotation> annotationType : annotationTypes) {
annotations.add(Types.createJsonQualifierImplementation(annotationType));
}
return adapter(type, Collections.unmodifiableSet(annotations));
}
@CheckReturnValue
public <T> JsonAdapter<T> adapter(Type type, Set<? extends Annotation> annotations) {
type = Types.canonicalize(type);
return adapter(type, annotations, null);
}
/**
* @param fieldName An optional field name associated with this type. The field name is used as a
* hint for better adapter lookup error messages for nested structures.
*/
@CheckReturnValue
@SuppressWarnings("unchecked") // Factories are required to return only matching JsonAdapters.
public <T> JsonAdapter<T> adapter(Type type, Set<? extends Annotation> annotations,
@Nullable String fieldName) {
if (type == null) {
throw new NullPointerException("type == null");
}
if (annotations == null) {
throw new NullPointerException("annotations == null");
}
type = removeSubtypeWildcard(canonicalize(type));
// If there's an equivalent adapter in the cache, we're done!
Object cacheKey = cacheKey(type, annotations);
@ -77,48 +122,44 @@ public final class Moshi {
if (result != null) return (JsonAdapter<T>) result;
}
// Short-circuit if this is a reentrant call.
List<DeferredAdapter<?>> deferredAdapters = reentrantCalls.get();
if (deferredAdapters != null) {
for (int i = 0, size = deferredAdapters.size(); i < size; i++) {
DeferredAdapter<?> deferredAdapter = deferredAdapters.get(i);
if (deferredAdapter.cacheKey.equals(cacheKey)) {
return (JsonAdapter<T>) deferredAdapter;
}
}
} else {
deferredAdapters = new ArrayList<>();
reentrantCalls.set(deferredAdapters);
LookupChain lookupChain = lookupChainThreadLocal.get();
if (lookupChain == null) {
lookupChain = new LookupChain();
lookupChainThreadLocal.set(lookupChain);
}
// Prepare for re-entrant calls, then ask each factory to create a type adapter.
DeferredAdapter<T> deferredAdapter = new DeferredAdapter<>(cacheKey);
deferredAdapters.add(deferredAdapter);
boolean success = false;
JsonAdapter<T> adapterFromCall = lookupChain.push(type, fieldName, cacheKey);
try {
if (adapterFromCall != null) return adapterFromCall;
// Ask each factory to create the JSON adapter.
for (int i = 0, size = factories.size(); i < size; i++) {
JsonAdapter<T> result = (JsonAdapter<T>) factories.get(i).create(type, annotations, this);
if (result != null) {
deferredAdapter.ready(result);
synchronized (adapterCache) {
adapterCache.put(cacheKey, result);
}
return result;
}
}
} finally {
deferredAdapters.remove(deferredAdapters.size() - 1);
if (deferredAdapters.isEmpty()) {
reentrantCalls.remove();
}
}
if (result == null) continue;
throw new IllegalArgumentException("No JsonAdapter for " + type + " annotated " + annotations);
// Success! Notify the LookupChain so it is cached and can be used by re-entrant calls.
lookupChain.adapterFound(result);
success = true;
return result;
}
throw new IllegalArgumentException(
"No JsonAdapter for " + typeAnnotatedWithAnnotations(type, annotations));
} catch (IllegalArgumentException e) {
throw lookupChain.exceptionWithLookupStack(e);
} finally {
lookupChain.pop(success);
}
}
@CheckReturnValue
@SuppressWarnings("unchecked") // Factories are required to return only matching JsonAdapters.
public <T> JsonAdapter<T> nextAdapter(JsonAdapter.Factory skipPast, Type type,
Set<? extends Annotation> annotations) {
type = Types.canonicalize(type);
if (annotations == null) throw new NullPointerException("annotations == null");
type = removeSubtypeWildcard(canonicalize(type));
int skipPastIndex = factories.indexOf(skipPast);
if (skipPastIndex == -1) {
@ -129,11 +170,11 @@ public final class Moshi {
if (result != null) return result;
}
throw new IllegalArgumentException("No next JsonAdapter for "
+ type + " annotated " + annotations);
+ typeAnnotatedWithAnnotations(type, annotations));
}
/** Returns a new builder containing all custom factories used by the current instance. */
public Moshi.Builder newBuilder() {
@CheckReturnValue public Moshi.Builder newBuilder() {
int fullSize = factories.size();
int tailSize = BUILT_IN_FACTORIES.size();
List<JsonAdapter.Factory> customFactories = factories.subList(0, fullSize - tailSize);
@ -154,7 +195,7 @@ public final class Moshi {
if (jsonAdapter == null) throw new IllegalArgumentException("jsonAdapter == null");
return add(new JsonAdapter.Factory() {
@Override public JsonAdapter<?> create(
@Override public @Nullable JsonAdapter<?> create(
Type targetType, Set<? extends Annotation> annotations, Moshi moshi) {
return annotations.isEmpty() && Util.typesMatch(type, targetType) ? jsonAdapter : null;
}
@ -174,7 +215,7 @@ public final class Moshi {
}
return add(new JsonAdapter.Factory() {
@Override public JsonAdapter<?> create(
@Override public @Nullable JsonAdapter<?> create(
Type targetType, Set<? extends Annotation> annotations, Moshi moshi) {
if (Util.typesMatch(type, targetType)
&& annotations.size() == 1
@ -202,40 +243,138 @@ public final class Moshi {
return this;
}
public Moshi build() {
@CheckReturnValue public Moshi build() {
return new Moshi(this);
}
}
/**
* Sometimes a type adapter factory depends on its own product; either directly or indirectly.
* To make this work, we offer this type adapter stub while the final adapter is being computed.
* When it is ready, we wire this to delegate to that finished adapter.
* A possibly-reentrant chain of lookups for JSON adapters.
*
* <p>Typically this is necessary in self-referential object models, such as an {@code Employee}
* class that has a {@code List<Employee>} field for an organization's management hierarchy.
* <p>We keep track of the current stack of lookups: we may start by looking up the JSON adapter
* for Employee, re-enter looking for the JSON adapter of HomeAddress, and re-enter again looking
* up the JSON adapter of PostalCode. If any of these lookups fail we can provide a stack trace
* with all of the lookups.
*
* <p>Sometimes a JSON adapter factory depends on its own product; either directly or indirectly.
* To make this work, we offer a JSON adapter stub while the final adapter is being computed.
* When it is ready, we wire the stub to that finished adapter. This is necessary in
* self-referential object models, such as an {@code Employee} class that has a {@code
* List<Employee>} field for an organization's management hierarchy.
*
* <p>This class defers putting any JSON adapters in the cache until the topmost JSON adapter has
* successfully been computed. That way we don't pollute the cache with incomplete stubs, or
* adapters that may transitively depend on incomplete stubs.
*/
private static class DeferredAdapter<T> extends JsonAdapter<T> {
Object cacheKey;
private JsonAdapter<T> delegate;
final class LookupChain {
final List<Lookup<?>> callLookups = new ArrayList<>();
final Deque<Lookup<?>> stack = new ArrayDeque<>();
boolean exceptionAnnotated;
DeferredAdapter(Object cacheKey) {
/**
* Returns a JSON adapter that was already created for this call, or null if this is the first
* time in this call that the cache key has been requested in this call. This may return a
* lookup that isn't yet ready if this lookup is reentrant.
*/
<T> JsonAdapter<T> push(Type type, @Nullable String fieldName, Object cacheKey) {
// Try to find a lookup with the same key for the same call.
for (int i = 0, size = callLookups.size(); i < size; i++) {
Lookup<?> lookup = callLookups.get(i);
if (lookup.cacheKey.equals(cacheKey)) {
Lookup<T> hit = (Lookup<T>) lookup;
stack.add(hit);
return hit.adapter != null ? hit.adapter : hit;
}
}
// We might need to know about this cache key later in this call. Prepare for that.
Lookup<Object> lookup = new Lookup<>(type, fieldName, cacheKey);
callLookups.add(lookup);
stack.add(lookup);
return null;
}
/** Sets the adapter result of the current lookup. */
<T> void adapterFound(JsonAdapter<T> result) {
Lookup<T> currentLookup = (Lookup<T>) stack.getLast();
currentLookup.adapter = result;
}
/**
* Completes the current lookup by removing a stack frame.
*
* @param success true if the adapter cache should be populated if this is the topmost lookup.
*/
void pop(boolean success) {
stack.removeLast();
if (!stack.isEmpty()) return;
lookupChainThreadLocal.remove();
if (success) {
synchronized (adapterCache) {
for (int i = 0, size = callLookups.size(); i < size; i++) {
Lookup<?> lookup = callLookups.get(i);
JsonAdapter<?> replaced = adapterCache.put(lookup.cacheKey, lookup.adapter);
if (replaced != null) {
((Lookup<Object>) lookup).adapter = (JsonAdapter<Object>) replaced;
adapterCache.put(lookup.cacheKey, replaced);
}
}
}
}
}
IllegalArgumentException exceptionWithLookupStack(IllegalArgumentException e) {
// Don't add the lookup stack to more than one exception; the deepest is sufficient.
if (exceptionAnnotated) return e;
exceptionAnnotated = true;
int size = stack.size();
if (size == 1 && stack.getFirst().fieldName == null) return e;
StringBuilder errorMessageBuilder = new StringBuilder(e.getMessage());
for (Iterator<Lookup<?>> i = stack.descendingIterator(); i.hasNext(); ) {
Lookup<?> lookup = i.next();
errorMessageBuilder
.append("\nfor ")
.append(lookup.type);
if (lookup.fieldName != null) {
errorMessageBuilder
.append(' ')
.append(lookup.fieldName);
}
}
return new IllegalArgumentException(errorMessageBuilder.toString(), e);
}
}
/** This class implements {@code JsonAdapter} so it can be used as a stub for re-entrant calls. */
static final class Lookup<T> extends JsonAdapter<T> {
final Type type;
final @Nullable String fieldName;
final Object cacheKey;
@Nullable JsonAdapter<T> adapter;
Lookup(Type type, @Nullable String fieldName, Object cacheKey) {
this.type = type;
this.fieldName = fieldName;
this.cacheKey = cacheKey;
}
void ready(JsonAdapter<T> delegate) {
this.delegate = delegate;
this.cacheKey = null;
}
@Override public T fromJson(JsonReader reader) throws IOException {
if (delegate == null) throw new IllegalStateException("Type adapter isn't ready");
return delegate.fromJson(reader);
if (adapter == null) throw new IllegalStateException("JsonAdapter isn't ready");
return adapter.fromJson(reader);
}
@Override public void toJson(JsonWriter writer, T value) throws IOException {
if (delegate == null) throw new IllegalStateException("Type adapter isn't ready");
delegate.toJson(writer, value);
if (adapter == null) throw new IllegalStateException("JsonAdapter isn't ready");
adapter.toJson(writer, value);
}
@Override public String toString() {
return adapter != null ? adapter.toString() : super.toString();
}
}
}

View file

@ -15,15 +15,23 @@
*/
package com.squareup.moshi;
import com.squareup.moshi.internal.Util;
import java.io.IOException;
import java.lang.annotation.Annotation;
import java.lang.reflect.Type;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.annotation.Nullable;
import static com.squareup.moshi.internal.Util.generatedAdapter;
final class StandardJsonAdapters {
private StandardJsonAdapters() {
}
public static final JsonAdapter.Factory FACTORY = new JsonAdapter.Factory() {
@Override public JsonAdapter<?> create(
Type type, Set<? extends Annotation> annotations, Moshi moshi) {
@ -48,6 +56,12 @@ final class StandardJsonAdapters {
if (type == Object.class) return new ObjectJsonAdapter(moshi).nullSafe();
Class<?> rawType = Types.getRawType(type);
@Nullable JsonAdapter<?> generatedAdapter = generatedAdapter(moshi, type, rawType);
if (generatedAdapter != null) {
return generatedAdapter;
}
if (rawType.isEnum()) {
//noinspection unchecked
return new EnumJsonAdapter<>((Class<? extends Enum>) rawType).nullSafe();
@ -216,7 +230,7 @@ final class StandardJsonAdapters {
private final T[] constants;
private final JsonReader.Options options;
public EnumJsonAdapter(Class<T> enumType) {
EnumJsonAdapter(Class<T> enumType) {
this.enumType = enumType;
try {
constants = enumType.getEnumConstants();
@ -238,10 +252,10 @@ final class StandardJsonAdapters {
if (index != -1) return constants[index];
// We can consume the string safely, we are terminating anyway.
String path = reader.getPath();
String name = reader.nextString();
throw new JsonDataException("Expected one of "
+ Arrays.asList(nameStrings) + " but was " + name + " at path "
+ reader.getPath());
+ Arrays.asList(nameStrings) + " but was " + name + " at path " + path);
}
@Override public void toJson(JsonWriter writer, T value) throws IOException {
@ -263,13 +277,45 @@ final class StandardJsonAdapters {
*/
static final class ObjectJsonAdapter extends JsonAdapter<Object> {
private final Moshi moshi;
private final JsonAdapter<List> listJsonAdapter;
private final JsonAdapter<Map> mapAdapter;
private final JsonAdapter<String> stringAdapter;
private final JsonAdapter<Double> doubleAdapter;
private final JsonAdapter<Boolean> booleanAdapter;
public ObjectJsonAdapter(Moshi moshi) {
ObjectJsonAdapter(Moshi moshi) {
this.moshi = moshi;
this.listJsonAdapter = moshi.adapter(List.class);
this.mapAdapter = moshi.adapter(Map.class);
this.stringAdapter = moshi.adapter(String.class);
this.doubleAdapter = moshi.adapter(Double.class);
this.booleanAdapter = moshi.adapter(Boolean.class);
}
@Override public Object fromJson(JsonReader reader) throws IOException {
return reader.readJsonValue();
switch (reader.peek()) {
case BEGIN_ARRAY:
return listJsonAdapter.fromJson(reader);
case BEGIN_OBJECT:
return mapAdapter.fromJson(reader);
case STRING:
return stringAdapter.fromJson(reader);
case NUMBER:
return doubleAdapter.fromJson(reader);
case BOOLEAN:
return booleanAdapter.fromJson(reader);
case NULL:
return reader.nextNull();
default:
throw new IllegalStateException(
"Expected a value but was " + reader.peek() + " at path " + reader.getPath());
}
}
@Override public void toJson(JsonWriter writer, Object value) throws IOException {

View file

@ -15,10 +15,13 @@
*/
package com.squareup.moshi;
import com.squareup.moshi.internal.Util.GenericArrayTypeImpl;
import com.squareup.moshi.internal.Util.ParameterizedTypeImpl;
import com.squareup.moshi.internal.Util.WildcardTypeImpl;
import java.lang.annotation.Annotation;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.GenericArrayType;
import java.lang.reflect.GenericDeclaration;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
@ -31,23 +34,59 @@ import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Properties;
import java.util.Set;
import javax.annotation.CheckReturnValue;
import javax.annotation.Nullable;
import static com.squareup.moshi.internal.Util.EMPTY_TYPE_ARRAY;
import static com.squareup.moshi.internal.Util.getGenericSupertype;
import static com.squareup.moshi.internal.Util.resolve;
/** Factory methods for types. */
@CheckReturnValue
public final class Types {
static final Type[] EMPTY_TYPE_ARRAY = new Type[] {};
private Types() {
}
/**
* Resolves the generated {@link JsonAdapter} fully qualified class name for a given
* {@link JsonClass JsonClass-annotated} {@code clazz}. This is the same lookup logic used by
* both the Moshi code generation as well as lookup for any JsonClass-annotated classes. This can
* be useful if generating your own JsonAdapters without using Moshi's first party code gen.
*
* @param clazz the class to calculate a generated JsonAdapter name for.
* @return the resolved fully qualified class name to the expected generated JsonAdapter class.
* Note that this name will always be a top-level class name and not a nested class.
*/
public static String generatedJsonAdapterName(Class<?> clazz) {
if (clazz.getAnnotation(JsonClass.class) == null) {
throw new IllegalArgumentException("Class does not have a JsonClass annotation: " + clazz);
}
return generatedJsonAdapterName(clazz.getName());
}
/**
* Resolves the generated {@link JsonAdapter} fully qualified class name for a given
* {@link JsonClass JsonClass-annotated} {@code className}. This is the same lookup logic used by
* both the Moshi code generation as well as lookup for any JsonClass-annotated classes. This can
* be useful if generating your own JsonAdapters without using Moshi's first party code gen.
*
* @param className the fully qualified class to calculate a generated JsonAdapter name for.
* @return the resolved fully qualified class name to the expected generated JsonAdapter class.
* Note that this name will always be a top-level class name and not a nested class.
*/
public static String generatedJsonAdapterName(String className) {
return className.replace("$", "_") + "JsonAdapter";
}
/**
* Checks if {@code annotations} contains {@code jsonQualifier}.
* Returns the subset of {@code annotations} without {@code jsonQualifier}, or null if {@code
* annotations} does not contain {@code jsonQualifier}.
*/
public static Set<? extends Annotation> nextAnnotations(Set<? extends Annotation> annotations,
public static @Nullable Set<? extends Annotation> nextAnnotations(
Set<? extends Annotation> annotations,
Class<? extends Annotation> jsonQualifier) {
if (!jsonQualifier.isAnnotationPresent(JsonQualifier.class)) {
throw new IllegalArgumentException(jsonQualifier + " is not a JsonQualifier.");
@ -57,7 +96,7 @@ public final class Types {
}
for (Annotation annotation : annotations) {
if (jsonQualifier.equals(annotation.annotationType())) {
Set<Annotation> delegateAnnotations = new LinkedHashSet<>(annotations);
Set<? extends Annotation> delegateAnnotations = new LinkedHashSet<>(annotations);
delegateAnnotations.remove(annotation);
return Collections.unmodifiableSet(delegateAnnotations);
}
@ -105,36 +144,6 @@ public final class Types {
return new WildcardTypeImpl(new Type[] { Object.class }, new Type[] { bound });
}
/**
* Returns a type that is functionally equal but not necessarily equal according to {@link
* Object#equals(Object) Object.equals()}.
*/
static Type canonicalize(Type type) {
if (type instanceof Class) {
Class<?> c = (Class<?>) type;
return c.isArray() ? new GenericArrayTypeImpl(canonicalize(c.getComponentType())) : c;
} else if (type instanceof ParameterizedType) {
if (type instanceof ParameterizedTypeImpl) return type;
ParameterizedType p = (ParameterizedType) type;
return new ParameterizedTypeImpl(p.getOwnerType(),
p.getRawType(), p.getActualTypeArguments());
} else if (type instanceof GenericArrayType) {
if (type instanceof GenericArrayTypeImpl) return type;
GenericArrayType g = (GenericArrayType) type;
return new GenericArrayTypeImpl(g.getGenericComponentType());
} else if (type instanceof WildcardType) {
if (type instanceof WildcardTypeImpl) return type;
WildcardType w = (WildcardType) type;
return new WildcardTypeImpl(w.getUpperBounds(), w.getLowerBounds());
} else {
return type; // This type is unsupported!
}
}
public static Class<?> getRawType(Type type) {
if (type instanceof Class<?>) {
// type is a normal class.
@ -149,7 +158,7 @@ public final class Types {
return (Class<?>) rawType;
} else if (type instanceof GenericArrayType) {
Type componentType = ((GenericArrayType)type).getGenericComponentType();
Type componentType = ((GenericArrayType) type).getGenericComponentType();
return Array.newInstance(getRawType(componentType), 0).getClass();
} else if (type instanceof TypeVariable) {
@ -167,6 +176,106 @@ public final class Types {
}
}
/**
* Returns the element type of this collection type.
* @throws IllegalArgumentException if this type is not a collection.
*/
public static Type collectionElementType(Type context, Class<?> contextRawType) {
Type collectionType = getSupertype(context, contextRawType, Collection.class);
if (collectionType instanceof WildcardType) {
collectionType = ((WildcardType) collectionType).getUpperBounds()[0];
}
if (collectionType instanceof ParameterizedType) {
return ((ParameterizedType) collectionType).getActualTypeArguments()[0];
}
return Object.class;
}
/** Returns true if {@code a} and {@code b} are equal. */
public static boolean equals(@Nullable Type a, @Nullable Type b) {
if (a == b) {
return true; // Also handles (a == null && b == null).
} else if (a instanceof Class) {
if (b instanceof GenericArrayType) {
return equals(((Class) a).getComponentType(),
((GenericArrayType) b).getGenericComponentType());
}
return a.equals(b); // Class already specifies equals().
} else if (a instanceof ParameterizedType) {
if (!(b instanceof ParameterizedType)) return false;
ParameterizedType pa = (ParameterizedType) a;
ParameterizedType pb = (ParameterizedType) b;
Type[] aTypeArguments = pa instanceof ParameterizedTypeImpl
? ((ParameterizedTypeImpl) pa).typeArguments
: pa.getActualTypeArguments();
Type[] bTypeArguments = pb instanceof ParameterizedTypeImpl
? ((ParameterizedTypeImpl) pb).typeArguments
: pb.getActualTypeArguments();
return equals(pa.getOwnerType(), pb.getOwnerType())
&& pa.getRawType().equals(pb.getRawType())
&& Arrays.equals(aTypeArguments, bTypeArguments);
} else if (a instanceof GenericArrayType) {
if (b instanceof Class) {
return equals(((Class) b).getComponentType(),
((GenericArrayType) a).getGenericComponentType());
}
if (!(b instanceof GenericArrayType)) return false;
GenericArrayType ga = (GenericArrayType) a;
GenericArrayType gb = (GenericArrayType) b;
return equals(ga.getGenericComponentType(), gb.getGenericComponentType());
} else if (a instanceof WildcardType) {
if (!(b instanceof WildcardType)) return false;
WildcardType wa = (WildcardType) a;
WildcardType wb = (WildcardType) b;
return Arrays.equals(wa.getUpperBounds(), wb.getUpperBounds())
&& Arrays.equals(wa.getLowerBounds(), wb.getLowerBounds());
} else if (a instanceof TypeVariable) {
if (!(b instanceof TypeVariable)) return false;
TypeVariable<?> va = (TypeVariable<?>) a;
TypeVariable<?> vb = (TypeVariable<?>) b;
return va.getGenericDeclaration() == vb.getGenericDeclaration()
&& va.getName().equals(vb.getName());
} else {
// This isn't a supported type.
return false;
}
}
/**
* @param clazz the target class to read the {@code fieldName} field annotations from.
* @param fieldName the target field name on {@code clazz}.
* @return a set of {@link JsonQualifier}-annotated {@link Annotation} instances retrieved from
* the targeted field. Can be empty if none are found.
*/
public static Set<? extends Annotation> getFieldJsonQualifierAnnotations(Class<?> clazz,
String fieldName) {
try {
Field field = clazz.getDeclaredField(fieldName);
field.setAccessible(true);
Annotation[] fieldAnnotations = field.getDeclaredAnnotations();
Set<Annotation> annotations = new LinkedHashSet<>(fieldAnnotations.length);
for (Annotation annotation : fieldAnnotations) {
if (annotation.annotationType().isAnnotationPresent(JsonQualifier.class)) {
annotations.add(annotation);
}
}
return Collections.unmodifiableSet(annotations);
} catch (NoSuchFieldException e) {
throw new IllegalArgumentException("Could not access field "
+ fieldName
+ " on class "
+ clazz.getCanonicalName(),
e);
}
}
@SuppressWarnings("unchecked")
static <T extends Annotation> T createJsonQualifierImplementation(final Class<T> annotationType) {
if (!annotationType.isAnnotation()) {
@ -200,103 +309,21 @@ public final class Types {
});
}
static boolean equal(Object a, Object b) {
return a == b || (a != null && a.equals(b));
}
/** Returns true if {@code a} and {@code b} are equal. */
static boolean equals(Type a, Type b) {
if (a == b) {
return true; // Also handles (a == null && b == null).
} else if (a instanceof Class) {
return a.equals(b); // Class already specifies equals().
} else if (a instanceof ParameterizedType) {
if (!(b instanceof ParameterizedType)) return false;
ParameterizedType pa = (ParameterizedType) a;
ParameterizedType pb = (ParameterizedType) b;
Type[] aTypeArguments = pa instanceof ParameterizedTypeImpl
? ((ParameterizedTypeImpl) pa).typeArguments
: pa.getActualTypeArguments();
Type[] bTypeArguments = pb instanceof ParameterizedTypeImpl
? ((ParameterizedTypeImpl) pb).typeArguments
: pb.getActualTypeArguments();
return equal(pa.getOwnerType(), pb.getOwnerType())
&& pa.getRawType().equals(pb.getRawType())
&& Arrays.equals(aTypeArguments, bTypeArguments);
} else if (a instanceof GenericArrayType) {
if (!(b instanceof GenericArrayType)) return false;
GenericArrayType ga = (GenericArrayType) a;
GenericArrayType gb = (GenericArrayType) b;
return equals(ga.getGenericComponentType(), gb.getGenericComponentType());
} else if (a instanceof WildcardType) {
if (!(b instanceof WildcardType)) return false;
WildcardType wa = (WildcardType) a;
WildcardType wb = (WildcardType) b;
return Arrays.equals(wa.getUpperBounds(), wb.getUpperBounds())
&& Arrays.equals(wa.getLowerBounds(), wb.getLowerBounds());
} else if (a instanceof TypeVariable) {
if (!(b instanceof TypeVariable)) return false;
TypeVariable<?> va = (TypeVariable<?>) a;
TypeVariable<?> vb = (TypeVariable<?>) b;
return va.getGenericDeclaration() == vb.getGenericDeclaration()
&& va.getName().equals(vb.getName());
} else {
// This isn't a supported type.
return false;
}
}
static int hashCodeOrZero(Object o) {
return o != null ? o.hashCode() : 0;
}
static String typeToString(Type type) {
return type instanceof Class ? ((Class<?>) type).getName() : type.toString();
}
/**
* Returns the generic supertype for {@code supertype}. For example, given a class {@code
* IntegerSet}, the result for when supertype is {@code Set.class} is {@code Set<Integer>} and the
* result when the supertype is {@code Collection.class} is {@code Collection<Integer>}.
* Returns a two element array containing this map's key and value types in positions 0 and 1
* respectively.
*/
static Type getGenericSupertype(Type context, Class<?> rawType, Class<?> toResolve) {
if (toResolve == rawType) {
return context;
}
static Type[] mapKeyAndValueTypes(Type context, Class<?> contextRawType) {
// Work around a problem with the declaration of java.util.Properties. That class should extend
// Hashtable<String, String>, but it's declared to extend Hashtable<Object, Object>.
if (context == Properties.class) return new Type[] { String.class, String.class };
// we skip searching through interfaces if unknown is an interface
if (toResolve.isInterface()) {
Class<?>[] interfaces = rawType.getInterfaces();
for (int i = 0, length = interfaces.length; i < length; i++) {
if (interfaces[i] == toResolve) {
return rawType.getGenericInterfaces()[i];
} else if (toResolve.isAssignableFrom(interfaces[i])) {
return getGenericSupertype(rawType.getGenericInterfaces()[i], interfaces[i], toResolve);
}
}
Type mapType = getSupertype(context, contextRawType, Map.class);
if (mapType instanceof ParameterizedType) {
ParameterizedType mapParameterizedType = (ParameterizedType) mapType;
return mapParameterizedType.getActualTypeArguments();
}
// check our supertypes
if (!rawType.isInterface()) {
while (rawType != Object.class) {
Class<?> rawSupertype = rawType.getSuperclass();
if (rawSupertype == toResolve) {
return rawType.getGenericSuperclass();
} else if (toResolve.isAssignableFrom(rawSupertype)) {
return getGenericSupertype(rawType.getGenericSuperclass(), rawSupertype, toResolve);
}
rawType = rawSupertype;
}
}
// we can't resolve this further
return toResolve;
return new Type[] { Object.class, Object.class };
}
/**
@ -330,305 +357,4 @@ public final class Types {
return null;
}
}
/**
* Returns the element type of this collection type.
* @throws IllegalArgumentException if this type is not a collection.
*/
public static Type collectionElementType(Type context, Class<?> contextRawType) {
Type collectionType = getSupertype(context, contextRawType, Collection.class);
if (collectionType instanceof WildcardType) {
collectionType = ((WildcardType) collectionType).getUpperBounds()[0];
}
if (collectionType instanceof ParameterizedType) {
return ((ParameterizedType) collectionType).getActualTypeArguments()[0];
}
return Object.class;
}
/**
* Returns a two element array containing this map's key and value types in positions 0 and 1
* respectively.
*/
static Type[] mapKeyAndValueTypes(Type context, Class<?> contextRawType) {
// Work around a problem with the declaration of java.util.Properties. That class should extend
// Hashtable<String, String>, but it's declared to extend Hashtable<Object, Object>.
if (context == Properties.class) return new Type[] { String.class, String.class };
Type mapType = getSupertype(context, contextRawType, Map.class);
if (mapType instanceof ParameterizedType) {
ParameterizedType mapParameterizedType = (ParameterizedType) mapType;
return mapParameterizedType.getActualTypeArguments();
}
return new Type[] { Object.class, Object.class };
}
static Type resolve(Type context, Class<?> contextRawType, Type toResolve) {
// This implementation is made a little more complicated in an attempt to avoid object-creation.
while (true) {
if (toResolve instanceof TypeVariable) {
TypeVariable<?> typeVariable = (TypeVariable<?>) toResolve;
toResolve = resolveTypeVariable(context, contextRawType, typeVariable);
if (toResolve == typeVariable) return toResolve;
} else if (toResolve instanceof Class && ((Class<?>) toResolve).isArray()) {
Class<?> original = (Class<?>) toResolve;
Type componentType = original.getComponentType();
Type newComponentType = resolve(context, contextRawType, componentType);
return componentType == newComponentType
? original
: arrayOf(newComponentType);
} else if (toResolve instanceof GenericArrayType) {
GenericArrayType original = (GenericArrayType) toResolve;
Type componentType = original.getGenericComponentType();
Type newComponentType = resolve(context, contextRawType, componentType);
return componentType == newComponentType
? original
: arrayOf(newComponentType);
} else if (toResolve instanceof ParameterizedType) {
ParameterizedType original = (ParameterizedType) toResolve;
Type ownerType = original.getOwnerType();
Type newOwnerType = resolve(context, contextRawType, ownerType);
boolean changed = newOwnerType != ownerType;
Type[] args = original.getActualTypeArguments();
for (int t = 0, length = args.length; t < length; t++) {
Type resolvedTypeArgument = resolve(context, contextRawType, args[t]);
if (resolvedTypeArgument != args[t]) {
if (!changed) {
args = args.clone();
changed = true;
}
args[t] = resolvedTypeArgument;
}
}
return changed
? new ParameterizedTypeImpl(newOwnerType, original.getRawType(), args)
: original;
} else if (toResolve instanceof WildcardType) {
WildcardType original = (WildcardType) toResolve;
Type[] originalLowerBound = original.getLowerBounds();
Type[] originalUpperBound = original.getUpperBounds();
if (originalLowerBound.length == 1) {
Type lowerBound = resolve(context, contextRawType, originalLowerBound[0]);
if (lowerBound != originalLowerBound[0]) {
return supertypeOf(lowerBound);
}
} else if (originalUpperBound.length == 1) {
Type upperBound = resolve(context, contextRawType, originalUpperBound[0]);
if (upperBound != originalUpperBound[0]) {
return subtypeOf(upperBound);
}
}
return original;
} else {
return toResolve;
}
}
}
static Type resolveTypeVariable(Type context, Class<?> contextRawType, TypeVariable<?> unknown) {
Class<?> declaredByRaw = declaringClassOf(unknown);
// We can't reduce this further.
if (declaredByRaw == null) return unknown;
Type declaredBy = getGenericSupertype(context, contextRawType, declaredByRaw);
if (declaredBy instanceof ParameterizedType) {
int index = indexOf(declaredByRaw.getTypeParameters(), unknown);
return ((ParameterizedType) declaredBy).getActualTypeArguments()[index];
}
return unknown;
}
/**
* Returns true if this is a Type supported by {@link StandardJsonAdapters#FACTORY}.
*/
static boolean isAllowedPlatformType(Type type) {
return type == Boolean.class
|| type == Byte.class
|| type == Character.class
|| type == Double.class
|| type == Float.class
|| type == Integer.class
|| type == Long.class
|| type == Short.class
|| type == String.class
|| type == Object.class;
}
private static int indexOf(Object[] array, Object toFind) {
for (int i = 0; i < array.length; i++) {
if (toFind.equals(array[i])) return i;
}
throw new NoSuchElementException();
}
/**
* Returns the declaring class of {@code typeVariable}, or {@code null} if it was not declared by
* a class.
*/
private static Class<?> declaringClassOf(TypeVariable<?> typeVariable) {
GenericDeclaration genericDeclaration = typeVariable.getGenericDeclaration();
return genericDeclaration instanceof Class ? (Class<?>) genericDeclaration : null;
}
static void checkNotPrimitive(Type type) {
if ((type instanceof Class<?>) && ((Class<?>) type).isPrimitive()) {
throw new IllegalArgumentException();
}
}
private static final class ParameterizedTypeImpl implements ParameterizedType {
private final Type ownerType;
private final Type rawType;
final Type[] typeArguments;
ParameterizedTypeImpl(Type ownerType, Type rawType, Type... typeArguments) {
// Require an owner type if the raw type needs it.
if (rawType instanceof Class<?>
&& (ownerType == null) != (((Class<?>) rawType).getEnclosingClass() == null)) {
throw new IllegalArgumentException(
"unexpected owner type for " + rawType + ": " + ownerType);
}
this.ownerType = ownerType == null ? null : canonicalize(ownerType);
this.rawType = canonicalize(rawType);
this.typeArguments = typeArguments.clone();
for (int t = 0; t < this.typeArguments.length; t++) {
if (this.typeArguments[t] == null) throw new NullPointerException();
checkNotPrimitive(this.typeArguments[t]);
this.typeArguments[t] = canonicalize(this.typeArguments[t]);
}
}
@Override public Type[] getActualTypeArguments() {
return typeArguments.clone();
}
@Override public Type getRawType() {
return rawType;
}
@Override public Type getOwnerType() {
return ownerType;
}
@Override public boolean equals(Object other) {
return other instanceof ParameterizedType
&& Types.equals(this, (ParameterizedType) other);
}
@Override public int hashCode() {
return Arrays.hashCode(typeArguments)
^ rawType.hashCode()
^ hashCodeOrZero(ownerType);
}
@Override public String toString() {
StringBuilder result = new StringBuilder(30 * (typeArguments.length + 1));
result.append(typeToString(rawType));
if (typeArguments.length == 0) {
return result.toString();
}
result.append("<").append(typeToString(typeArguments[0]));
for (int i = 1; i < typeArguments.length; i++) {
result.append(", ").append(typeToString(typeArguments[i]));
}
return result.append(">").toString();
}
}
private static final class GenericArrayTypeImpl implements GenericArrayType {
private final Type componentType;
public GenericArrayTypeImpl(Type componentType) {
this.componentType = canonicalize(componentType);
}
@Override public Type getGenericComponentType() {
return componentType;
}
@Override public boolean equals(Object o) {
return o instanceof GenericArrayType
&& Types.equals(this, (GenericArrayType) o);
}
@Override public int hashCode() {
return componentType.hashCode();
}
@Override public String toString() {
return typeToString(componentType) + "[]";
}
}
/**
* The WildcardType interface supports multiple upper bounds and multiple lower bounds. We only
* support what the Java 6 language needs - at most one bound. If a lower bound is set, the upper
* bound must be Object.class.
*/
private static final class WildcardTypeImpl implements WildcardType {
private final Type upperBound;
private final Type lowerBound;
public WildcardTypeImpl(Type[] upperBounds, Type[] lowerBounds) {
if (lowerBounds.length > 1) throw new IllegalArgumentException();
if (upperBounds.length != 1) throw new IllegalArgumentException();
if (lowerBounds.length == 1) {
if (lowerBounds[0] == null) throw new NullPointerException();
checkNotPrimitive(lowerBounds[0]);
if (upperBounds[0] != Object.class) throw new IllegalArgumentException();
this.lowerBound = canonicalize(lowerBounds[0]);
this.upperBound = Object.class;
} else {
if (upperBounds[0] == null) throw new NullPointerException();
checkNotPrimitive(upperBounds[0]);
this.lowerBound = null;
this.upperBound = canonicalize(upperBounds[0]);
}
}
@Override public Type[] getUpperBounds() {
return new Type[] { upperBound };
}
@Override public Type[] getLowerBounds() {
return lowerBound != null ? new Type[] { lowerBound } : EMPTY_TYPE_ARRAY;
}
@Override public boolean equals(Object other) {
return other instanceof WildcardType
&& Types.equals(this, (WildcardType) other);
}
@Override public int hashCode() {
// This equals Arrays.hashCode(getLowerBounds()) ^ Arrays.hashCode(getUpperBounds()).
return (lowerBound != null ? 31 + lowerBound.hashCode() : 1)
^ (31 + upperBound.hashCode());
}
@Override public String toString() {
if (lowerBound != null) {
return "? super " + typeToString(lowerBound);
} else if (upperBound == Object.class) {
return "?";
} else {
return "? extends " + typeToString(upperBound);
}
}
}
}

View file

@ -1,66 +0,0 @@
/*
* Copyright (C) 2014 Square, Inc.
*
* 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 com.squareup.moshi;
import java.lang.annotation.Annotation;
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Type;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.Set;
final class Util {
public static final Set<Annotation> NO_ANNOTATIONS = Collections.emptySet();
public static boolean typesMatch(Type pattern, Type candidate) {
// TODO: permit raw types (like Set.class) to match non-raw candidates (like Set<Long>).
return pattern.equals(candidate);
}
public static Set<? extends Annotation> jsonAnnotations(AnnotatedElement annotatedElement) {
return jsonAnnotations(annotatedElement.getAnnotations());
}
public static Set<? extends Annotation> jsonAnnotations(Annotation[] annotations) {
Set<Annotation> result = null;
for (Annotation annotation : annotations) {
if (annotation.annotationType().isAnnotationPresent(JsonQualifier.class)) {
if (result == null) result = new LinkedHashSet<>();
result.add(annotation);
}
}
return result != null ? Collections.unmodifiableSet(result) : Util.NO_ANNOTATIONS;
}
public static boolean isAnnotationPresent(
Set<? extends Annotation> annotations, Class<? extends Annotation> annotationClass) {
if (annotations.isEmpty()) return false; // Save an iterator in the common case.
for (Annotation annotation : annotations) {
if (annotation.annotationType() == annotationClass) return true;
}
return false;
}
/** Returns true if {@code annotations} has any annotation whose simple name is Nullable. */
public static boolean hasNullable(Annotation[] annotations) {
for (Annotation annotation : annotations) {
if (annotation.annotationType().getSimpleName().equals("Nullable")) {
return true;
}
}
return false;
}
}

View file

@ -0,0 +1,55 @@
/*
* Copyright (C) 2019 Square, Inc.
*
* 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 com.squareup.moshi.internal;
import com.squareup.moshi.JsonAdapter;
import com.squareup.moshi.JsonReader;
import com.squareup.moshi.JsonWriter;
import java.io.IOException;
import javax.annotation.Nullable;
public final class NullSafeJsonAdapter<T> extends JsonAdapter<T> {
private final JsonAdapter<T> delegate;
public NullSafeJsonAdapter(JsonAdapter<T> delegate) {
this.delegate = delegate;
}
public JsonAdapter<T> delegate() {
return delegate;
}
@Override public @Nullable T fromJson(JsonReader reader) throws IOException {
if (reader.peek() == JsonReader.Token.NULL) {
return reader.nextNull();
} else {
return delegate.fromJson(reader);
}
}
@Override public void toJson(JsonWriter writer, @Nullable T value) throws IOException {
if (value == null) {
writer.nullValue();
} else {
delegate.toJson(writer, value);
}
}
@Override public String toString() {
return delegate + ".nullSafe()";
}
}

View file

@ -0,0 +1,523 @@
/*
* Copyright (C) 2014 Square, Inc.
*
* 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 com.squareup.moshi.internal;
import com.squareup.moshi.JsonAdapter;
import com.squareup.moshi.JsonClass;
import com.squareup.moshi.JsonQualifier;
import com.squareup.moshi.Moshi;
import com.squareup.moshi.Types;
import java.lang.annotation.Annotation;
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Constructor;
import java.lang.reflect.GenericArrayType;
import java.lang.reflect.GenericDeclaration;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.lang.reflect.TypeVariable;
import java.lang.reflect.WildcardType;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.NoSuchElementException;
import java.util.Set;
import javax.annotation.Nullable;
import static com.squareup.moshi.Types.arrayOf;
import static com.squareup.moshi.Types.subtypeOf;
import static com.squareup.moshi.Types.supertypeOf;
public final class Util {
public static final Set<Annotation> NO_ANNOTATIONS = Collections.emptySet();
public static final Type[] EMPTY_TYPE_ARRAY = new Type[] {};
private Util() {
}
public static boolean typesMatch(Type pattern, Type candidate) {
// TODO: permit raw types (like Set.class) to match non-raw candidates (like Set<Long>).
return Types.equals(pattern, candidate);
}
public static Set<? extends Annotation> jsonAnnotations(AnnotatedElement annotatedElement) {
return jsonAnnotations(annotatedElement.getAnnotations());
}
public static Set<? extends Annotation> jsonAnnotations(Annotation[] annotations) {
Set<Annotation> result = null;
for (Annotation annotation : annotations) {
if (annotation.annotationType().isAnnotationPresent(JsonQualifier.class)) {
if (result == null) result = new LinkedHashSet<>();
result.add(annotation);
}
}
return result != null ? Collections.unmodifiableSet(result) : Util.NO_ANNOTATIONS;
}
public static boolean isAnnotationPresent(
Set<? extends Annotation> annotations, Class<? extends Annotation> annotationClass) {
if (annotations.isEmpty()) return false; // Save an iterator in the common case.
for (Annotation annotation : annotations) {
if (annotation.annotationType() == annotationClass) return true;
}
return false;
}
/** Returns true if {@code annotations} has any annotation whose simple name is Nullable. */
public static boolean hasNullable(Annotation[] annotations) {
for (Annotation annotation : annotations) {
if (annotation.annotationType().getSimpleName().equals("Nullable")) {
return true;
}
}
return false;
}
/**
* Returns true if {@code rawType} is built in. We don't reflect on private fields of platform
* types because they're unspecified and likely to be different on Java vs. Android.
*/
public static boolean isPlatformType(Class<?> rawType) {
String name = rawType.getName();
return name.startsWith("android.")
|| name.startsWith("androidx.")
|| name.startsWith("java.")
|| name.startsWith("javax.")
|| name.startsWith("kotlin.")
|| name.startsWith("scala.");
}
/** Throws the cause of {@code e}, wrapping it if it is checked. */
public static RuntimeException rethrowCause(InvocationTargetException e) {
Throwable cause = e.getTargetException();
if (cause instanceof RuntimeException) throw (RuntimeException) cause;
if (cause instanceof Error) throw (Error) cause;
throw new RuntimeException(cause);
}
/**
* Returns a type that is functionally equal but not necessarily equal according to {@link
* Object#equals(Object) Object.equals()}.
*/
public static Type canonicalize(Type type) {
if (type instanceof Class) {
Class<?> c = (Class<?>) type;
return c.isArray() ? new GenericArrayTypeImpl(canonicalize(c.getComponentType())) : c;
} else if (type instanceof ParameterizedType) {
if (type instanceof ParameterizedTypeImpl) return type;
ParameterizedType p = (ParameterizedType) type;
return new ParameterizedTypeImpl(p.getOwnerType(),
p.getRawType(), p.getActualTypeArguments());
} else if (type instanceof GenericArrayType) {
if (type instanceof GenericArrayTypeImpl) return type;
GenericArrayType g = (GenericArrayType) type;
return new GenericArrayTypeImpl(g.getGenericComponentType());
} else if (type instanceof WildcardType) {
if (type instanceof WildcardTypeImpl) return type;
WildcardType w = (WildcardType) type;
return new WildcardTypeImpl(w.getUpperBounds(), w.getLowerBounds());
} else {
return type; // This type is unsupported!
}
}
/**
* If type is a "? extends X" wildcard, returns X; otherwise returns type unchanged.
*/
public static Type removeSubtypeWildcard(Type type) {
if (!(type instanceof WildcardType)) return type;
Type[] lowerBounds = ((WildcardType) type).getLowerBounds();
if (lowerBounds.length != 0) return type;
Type[] upperBounds = ((WildcardType) type).getUpperBounds();
if (upperBounds.length != 1) throw new IllegalArgumentException();
return upperBounds[0];
}
public static Type resolve(Type context, Class<?> contextRawType, Type toResolve) {
// This implementation is made a little more complicated in an attempt to avoid object-creation.
while (true) {
if (toResolve instanceof TypeVariable) {
TypeVariable<?> typeVariable = (TypeVariable<?>) toResolve;
toResolve = resolveTypeVariable(context, contextRawType, typeVariable);
if (toResolve == typeVariable) return toResolve;
} else if (toResolve instanceof Class && ((Class<?>) toResolve).isArray()) {
Class<?> original = (Class<?>) toResolve;
Type componentType = original.getComponentType();
Type newComponentType = resolve(context, contextRawType, componentType);
return componentType == newComponentType
? original
: arrayOf(newComponentType);
} else if (toResolve instanceof GenericArrayType) {
GenericArrayType original = (GenericArrayType) toResolve;
Type componentType = original.getGenericComponentType();
Type newComponentType = resolve(context, contextRawType, componentType);
return componentType == newComponentType
? original
: arrayOf(newComponentType);
} else if (toResolve instanceof ParameterizedType) {
ParameterizedType original = (ParameterizedType) toResolve;
Type ownerType = original.getOwnerType();
Type newOwnerType = resolve(context, contextRawType, ownerType);
boolean changed = newOwnerType != ownerType;
Type[] args = original.getActualTypeArguments();
for (int t = 0, length = args.length; t < length; t++) {
Type resolvedTypeArgument = resolve(context, contextRawType, args[t]);
if (resolvedTypeArgument != args[t]) {
if (!changed) {
args = args.clone();
changed = true;
}
args[t] = resolvedTypeArgument;
}
}
return changed
? new ParameterizedTypeImpl(newOwnerType, original.getRawType(), args)
: original;
} else if (toResolve instanceof WildcardType) {
WildcardType original = (WildcardType) toResolve;
Type[] originalLowerBound = original.getLowerBounds();
Type[] originalUpperBound = original.getUpperBounds();
if (originalLowerBound.length == 1) {
Type lowerBound = resolve(context, contextRawType, originalLowerBound[0]);
if (lowerBound != originalLowerBound[0]) {
return supertypeOf(lowerBound);
}
} else if (originalUpperBound.length == 1) {
Type upperBound = resolve(context, contextRawType, originalUpperBound[0]);
if (upperBound != originalUpperBound[0]) {
return subtypeOf(upperBound);
}
}
return original;
} else {
return toResolve;
}
}
}
static Type resolveTypeVariable(Type context, Class<?> contextRawType, TypeVariable<?> unknown) {
Class<?> declaredByRaw = declaringClassOf(unknown);
// We can't reduce this further.
if (declaredByRaw == null) return unknown;
Type declaredBy = getGenericSupertype(context, contextRawType, declaredByRaw);
if (declaredBy instanceof ParameterizedType) {
int index = indexOf(declaredByRaw.getTypeParameters(), unknown);
return ((ParameterizedType) declaredBy).getActualTypeArguments()[index];
}
return unknown;
}
/**
* Returns the generic supertype for {@code supertype}. For example, given a class {@code
* IntegerSet}, the result for when supertype is {@code Set.class} is {@code Set<Integer>} and the
* result when the supertype is {@code Collection.class} is {@code Collection<Integer>}.
*/
public static Type getGenericSupertype(Type context, Class<?> rawType, Class<?> toResolve) {
if (toResolve == rawType) {
return context;
}
// we skip searching through interfaces if unknown is an interface
if (toResolve.isInterface()) {
Class<?>[] interfaces = rawType.getInterfaces();
for (int i = 0, length = interfaces.length; i < length; i++) {
if (interfaces[i] == toResolve) {
return rawType.getGenericInterfaces()[i];
} else if (toResolve.isAssignableFrom(interfaces[i])) {
return getGenericSupertype(rawType.getGenericInterfaces()[i], interfaces[i], toResolve);
}
}
}
// check our supertypes
if (!rawType.isInterface()) {
while (rawType != Object.class) {
Class<?> rawSupertype = rawType.getSuperclass();
if (rawSupertype == toResolve) {
return rawType.getGenericSuperclass();
} else if (toResolve.isAssignableFrom(rawSupertype)) {
return getGenericSupertype(rawType.getGenericSuperclass(), rawSupertype, toResolve);
}
rawType = rawSupertype;
}
}
// we can't resolve this further
return toResolve;
}
static int hashCodeOrZero(@Nullable Object o) {
return o != null ? o.hashCode() : 0;
}
static String typeToString(Type type) {
return type instanceof Class ? ((Class<?>) type).getName() : type.toString();
}
static int indexOf(Object[] array, Object toFind) {
for (int i = 0; i < array.length; i++) {
if (toFind.equals(array[i])) return i;
}
throw new NoSuchElementException();
}
/**
* Returns the declaring class of {@code typeVariable}, or {@code null} if it was not declared by
* a class.
*/
static @Nullable Class<?> declaringClassOf(TypeVariable<?> typeVariable) {
GenericDeclaration genericDeclaration = typeVariable.getGenericDeclaration();
return genericDeclaration instanceof Class ? (Class<?>) genericDeclaration : null;
}
static void checkNotPrimitive(Type type) {
if ((type instanceof Class<?>) && ((Class<?>) type).isPrimitive()) {
throw new IllegalArgumentException("Unexpected primitive " + type + ". Use the boxed type.");
}
}
public static final class ParameterizedTypeImpl implements ParameterizedType {
private final @Nullable Type ownerType;
private final Type rawType;
public final Type[] typeArguments;
public ParameterizedTypeImpl(@Nullable Type ownerType, Type rawType, Type... typeArguments) {
// Require an owner type if the raw type needs it.
if (rawType instanceof Class<?>) {
Class<?> enclosingClass = ((Class<?>) rawType).getEnclosingClass();
if (ownerType != null) {
if (enclosingClass == null || Types.getRawType(ownerType) != enclosingClass) {
throw new IllegalArgumentException(
"unexpected owner type for " + rawType + ": " + ownerType);
}
} else if (enclosingClass != null) {
throw new IllegalArgumentException(
"unexpected owner type for " + rawType + ": null");
}
}
this.ownerType = ownerType == null ? null : canonicalize(ownerType);
this.rawType = canonicalize(rawType);
this.typeArguments = typeArguments.clone();
for (int t = 0; t < this.typeArguments.length; t++) {
if (this.typeArguments[t] == null) throw new NullPointerException();
checkNotPrimitive(this.typeArguments[t]);
this.typeArguments[t] = canonicalize(this.typeArguments[t]);
}
}
@Override public Type[] getActualTypeArguments() {
return typeArguments.clone();
}
@Override public Type getRawType() {
return rawType;
}
@Override public @Nullable Type getOwnerType() {
return ownerType;
}
@Override public boolean equals(Object other) {
return other instanceof ParameterizedType
&& Types.equals(this, (ParameterizedType) other);
}
@Override public int hashCode() {
return Arrays.hashCode(typeArguments)
^ rawType.hashCode()
^ hashCodeOrZero(ownerType);
}
@Override public String toString() {
StringBuilder result = new StringBuilder(30 * (typeArguments.length + 1));
result.append(typeToString(rawType));
if (typeArguments.length == 0) {
return result.toString();
}
result.append("<").append(typeToString(typeArguments[0]));
for (int i = 1; i < typeArguments.length; i++) {
result.append(", ").append(typeToString(typeArguments[i]));
}
return result.append(">").toString();
}
}
public static final class GenericArrayTypeImpl implements GenericArrayType {
private final Type componentType;
public GenericArrayTypeImpl(Type componentType) {
this.componentType = canonicalize(componentType);
}
@Override public Type getGenericComponentType() {
return componentType;
}
@Override public boolean equals(Object o) {
return o instanceof GenericArrayType
&& Types.equals(this, (GenericArrayType) o);
}
@Override public int hashCode() {
return componentType.hashCode();
}
@Override public String toString() {
return typeToString(componentType) + "[]";
}
}
/**
* The WildcardType interface supports multiple upper bounds and multiple lower bounds. We only
* support what the Java 6 language needs - at most one bound. If a lower bound is set, the upper
* bound must be Object.class.
*/
public static final class WildcardTypeImpl implements WildcardType {
private final Type upperBound;
private final @Nullable Type lowerBound;
public WildcardTypeImpl(Type[] upperBounds, Type[] lowerBounds) {
if (lowerBounds.length > 1) throw new IllegalArgumentException();
if (upperBounds.length != 1) throw new IllegalArgumentException();
if (lowerBounds.length == 1) {
if (lowerBounds[0] == null) throw new NullPointerException();
checkNotPrimitive(lowerBounds[0]);
if (upperBounds[0] != Object.class) throw new IllegalArgumentException();
this.lowerBound = canonicalize(lowerBounds[0]);
this.upperBound = Object.class;
} else {
if (upperBounds[0] == null) throw new NullPointerException();
checkNotPrimitive(upperBounds[0]);
this.lowerBound = null;
this.upperBound = canonicalize(upperBounds[0]);
}
}
@Override public Type[] getUpperBounds() {
return new Type[] { upperBound };
}
@Override public Type[] getLowerBounds() {
return lowerBound != null ? new Type[] { lowerBound } : EMPTY_TYPE_ARRAY;
}
@Override public boolean equals(Object other) {
return other instanceof WildcardType
&& Types.equals(this, (WildcardType) other);
}
@Override public int hashCode() {
// This equals Arrays.hashCode(getLowerBounds()) ^ Arrays.hashCode(getUpperBounds()).
return (lowerBound != null ? 31 + lowerBound.hashCode() : 1)
^ (31 + upperBound.hashCode());
}
@Override public String toString() {
if (lowerBound != null) {
return "? super " + typeToString(lowerBound);
} else if (upperBound == Object.class) {
return "?";
} else {
return "? extends " + typeToString(upperBound);
}
}
}
public static String typeAnnotatedWithAnnotations(Type type,
Set<? extends Annotation> annotations) {
return type + (annotations.isEmpty() ? " (with no annotations)" : " annotated " + annotations);
}
/**
* Loads the generated JsonAdapter for classes annotated {@link JsonClass}. This works because it
* uses the same naming conventions as {@code JsonClassCodeGenProcessor}.
*/
public static @Nullable JsonAdapter<?> generatedAdapter(Moshi moshi, Type type,
Class<?> rawType) {
JsonClass jsonClass = rawType.getAnnotation(JsonClass.class);
if (jsonClass == null || !jsonClass.generateAdapter()) {
return null;
}
String adapterClassName = Types.generatedJsonAdapterName(rawType.getName());
try {
@SuppressWarnings("unchecked") // We generate types to match.
Class<? extends JsonAdapter<?>> adapterClass = (Class<? extends JsonAdapter<?>>)
Class.forName(adapterClassName, true, rawType.getClassLoader());
Constructor<? extends JsonAdapter<?>> constructor;
Object[] args;
if (type instanceof ParameterizedType) {
Type[] typeArgs = ((ParameterizedType) type).getActualTypeArguments();
try {
// Common case first
constructor = adapterClass.getDeclaredConstructor(Moshi.class, Type[].class);
args = new Object[] { moshi, typeArgs };
} catch (NoSuchMethodException e) {
constructor = adapterClass.getDeclaredConstructor(Type[].class);
args = new Object[] { typeArgs };
}
} else {
try {
// Common case first
constructor = adapterClass.getDeclaredConstructor(Moshi.class);
args = new Object[] { moshi };
} catch (NoSuchMethodException e) {
constructor = adapterClass.getDeclaredConstructor();
args = new Object[0];
}
}
constructor.setAccessible(true);
return constructor.newInstance(args).nullSafe();
} catch (ClassNotFoundException e) {
throw new RuntimeException(
"Failed to find the generated JsonAdapter class for " + rawType, e);
} catch (NoSuchMethodException e) {
throw new RuntimeException(
"Failed to find the generated JsonAdapter constructor for " + rawType, e);
} catch (IllegalAccessException e) {
throw new RuntimeException(
"Failed to access the generated JsonAdapter for " + rawType, e);
} catch (InstantiationException e) {
throw new RuntimeException(
"Failed to instantiate the generated JsonAdapter for " + rawType, e);
} catch (InvocationTargetException e) {
throw rethrowCause(e);
}
}
}

View file

@ -0,0 +1,3 @@
/** Moshi is modern JSON library for Android and Java. */
@javax.annotation.ParametersAreNonnullByDefault
package com.squareup.moshi;

View file

@ -0,0 +1,49 @@
# JSR 305 annotations are for embedding nullability information.
-dontwarn javax.annotation.**
-keepclasseswithmembers class * {
@com.squareup.moshi.* <methods>;
}
-keep @com.squareup.moshi.JsonQualifier interface *
# Enum field names are used by the integrated EnumJsonAdapter.
# Annotate enums with @JsonClass(generateAdapter = false) to use them with Moshi.
-keepclassmembers @com.squareup.moshi.JsonClass class * extends java.lang.Enum {
<fields>;
}
# The name of @JsonClass types is used to look up the generated adapter.
-keepnames @com.squareup.moshi.JsonClass class *
# Retain generated JsonAdapters if annotated type is retained.
-if @com.squareup.moshi.JsonClass class *
-keep class <1>JsonAdapter {
<init>(...);
<fields>;
}
-if @com.squareup.moshi.JsonClass class **$*
-keep class <1>_<2>JsonAdapter {
<init>(...);
<fields>;
}
-if @com.squareup.moshi.JsonClass class **$*$*
-keep class <1>_<2>_<3>JsonAdapter {
<init>(...);
<fields>;
}
-if @com.squareup.moshi.JsonClass class **$*$*$*
-keep class <1>_<2>_<3>_<4>JsonAdapter {
<init>(...);
<fields>;
}
-if @com.squareup.moshi.JsonClass class **$*$*$*$*
-keep class <1>_<2>_<3>_<4>_<5>JsonAdapter {
<init>(...);
<fields>;
}
-if @com.squareup.moshi.JsonClass class **$*$*$*$*$*
-keep class <1>_<2>_<3>_<4>_<5>_<6>JsonAdapter {
<init>(...);
<fields>;
}

View file

@ -15,6 +15,8 @@
*/
package com.squareup.moshi;
import com.squareup.moshi.MoshiTest.Uppercase;
import com.squareup.moshi.MoshiTest.UppercaseAdapterFactory;
import java.io.IOException;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@ -24,8 +26,10 @@ import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import okio.ByteString;
import org.junit.Test;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
@ -79,6 +83,91 @@ public final class AdapterMethodsTest {
}
}
private static final class PointJsonAdapterWithDelegate {
@FromJson Point fromJson(JsonReader reader, JsonAdapter<Point> delegate) throws IOException {
reader.beginArray();
Point value = delegate.fromJson(reader);
reader.endArray();
return value;
}
@ToJson void toJson(JsonWriter writer, Point value, JsonAdapter<Point> delegate)
throws IOException {
writer.beginArray();
delegate.toJson(writer, value);
writer.endArray();
}
}
private static final class PointJsonAdapterWithDelegateWithQualifier {
@FromJson @WithParens Point fromJson(JsonReader reader, @WithParens JsonAdapter<Point> delegate)
throws IOException {
reader.beginArray();
Point value = delegate.fromJson(reader);
reader.endArray();
return value;
}
@ToJson void toJson(JsonWriter writer, @WithParens Point value,
@WithParens JsonAdapter<Point> delegate)
throws IOException {
writer.beginArray();
delegate.toJson(writer, value);
writer.endArray();
}
}
@Test public void toAndFromWithDelegate() throws Exception {
Moshi moshi = new Moshi.Builder()
.add(new PointJsonAdapterWithDelegate())
.build();
JsonAdapter<Point> adapter = moshi.adapter(Point.class);
Point point = new Point(5, 8);
assertThat(adapter.toJson(point)).isEqualTo("[{\"x\":5,\"y\":8}]");
assertThat(adapter.fromJson("[{\"x\":5,\"y\":8}]")).isEqualTo(point);
}
@Test public void toAndFromWithDelegateWithQualifier() throws Exception {
Moshi moshi = new Moshi.Builder()
.add(new PointJsonAdapterWithDelegateWithQualifier())
.add(new PointWithParensJsonAdapter())
.build();
JsonAdapter<Point> adapter = moshi.adapter(Point.class, WithParens.class);
Point point = new Point(5, 8);
assertThat(adapter.toJson(point)).isEqualTo("[\"(5 8)\"]");
assertThat(adapter.fromJson("[\"(5 8)\"]")).isEqualTo(point);
}
@Test public void toAndFromWithIntermediate() throws Exception {
Moshi moshi = new Moshi.Builder().add(new Object() {
@FromJson String fromJson(String string) {
return string.substring(1, string.length() - 1);
}
@ToJson String toJson(String value) {
return "|" + value + "|";
}
}).build();
JsonAdapter<String> adapter = moshi.adapter(String.class);
assertThat(adapter.toJson("pizza")).isEqualTo("\"|pizza|\"");
assertThat(adapter.fromJson("\"|pizza|\"")).isEqualTo("pizza");
}
@Test public void toAndFromWithIntermediateWithQualifier() throws Exception {
Moshi moshi = new Moshi.Builder().add(new Object() {
@FromJson @Uppercase String fromJson(@Uppercase String string) {
return string.substring(1, string.length() - 1);
}
@ToJson @Uppercase String toJson(@Uppercase String value) {
return "|" + value + "|";
}
}).add(new UppercaseAdapterFactory()).build();
JsonAdapter<String> adapter = moshi.adapter(String.class, Uppercase.class);
assertThat(adapter.toJson("pizza")).isEqualTo("\"|PIZZA|\"");
assertThat(adapter.fromJson("\"|pizza|\"")).isEqualTo("PIZZA");
}
@Test public void toJsonOnly() throws Exception {
Moshi moshi = new Moshi.Builder()
.add(new PointAsListOfIntegersToAdapter())
@ -215,6 +304,8 @@ public final class AdapterMethodsTest {
+ "(com.squareup.moshi.AdapterMethodsTest$Point).\n"
+ "@ToJson method signatures may have one of the following structures:\n"
+ " <any access modifier> void toJson(JsonWriter writer, T value) throws <any>;\n"
+ " <any access modifier> void toJson(JsonWriter writer, T value,"
+ " JsonAdapter<any> delegate, <any more delegates>) throws <any>;\n"
+ " <any access modifier> R toJson(T value) throws <any>;\n");
}
}
@ -234,7 +325,9 @@ public final class AdapterMethodsTest {
+ "com.squareup.moshi.AdapterMethodsTest$UnexpectedSignatureFromJsonAdapter.pointFromJson"
+ "(java.lang.String).\n"
+ "@FromJson method signatures may have one of the following structures:\n"
+ " <any access modifier> void fromJson(JsonReader jsonReader) throws <any>;\n"
+ " <any access modifier> R fromJson(JsonReader jsonReader) throws <any>;\n"
+ " <any access modifier> R fromJson(JsonReader jsonReader,"
+ " JsonAdapter<any> delegate, <any more delegates>) throws <any>;\n"
+ " <any access modifier> R fromJson(T value) throws <any>;\n");
}
}
@ -304,7 +397,7 @@ public final class AdapterMethodsTest {
}
static class NullableIntToJsonAdapter {
@FromJson int intToJson(JsonReader reader) throws IOException {
@FromJson int jsonToInt(JsonReader reader) throws IOException {
if (reader.peek() == JsonReader.Token.NULL) {
reader.nextNull();
return -1;
@ -312,7 +405,7 @@ public final class AdapterMethodsTest {
return reader.nextInt();
}
@ToJson void jsonToInt(JsonWriter writer, int value) throws IOException {
@ToJson void intToJson(JsonWriter writer, int value) throws IOException {
if (value == -1) {
writer.nullValue();
} else {
@ -369,7 +462,10 @@ public final class AdapterMethodsTest {
fail();
} catch (IllegalArgumentException e) {
assertThat(e).hasMessage("No @FromJson adapter for interface "
+ "com.squareup.moshi.AdapterMethodsTest$Shape annotated []");
+ "com.squareup.moshi.AdapterMethodsTest$Shape (with no annotations)");
assertThat(e).hasCauseExactlyInstanceOf(IllegalArgumentException.class);
assertThat(e.getCause()).hasMessage("No next JsonAdapter for interface "
+ "com.squareup.moshi.AdapterMethodsTest$Shape (with no annotations)");
}
}
@ -388,7 +484,10 @@ public final class AdapterMethodsTest {
fail();
} catch (IllegalArgumentException e) {
assertThat(e).hasMessage("No @ToJson adapter for interface "
+ "com.squareup.moshi.AdapterMethodsTest$Shape annotated []");
+ "com.squareup.moshi.AdapterMethodsTest$Shape (with no annotations)");
assertThat(e).hasCauseExactlyInstanceOf(IllegalArgumentException.class);
assertThat(e.getCause()).hasMessage("No next JsonAdapter for interface "
+ "com.squareup.moshi.AdapterMethodsTest$Shape (with no annotations)");
}
}
@ -615,6 +714,57 @@ public final class AdapterMethodsTest {
}
}
@Test public void genericArrayTypes() throws Exception {
Moshi moshi = new Moshi.Builder()
.add(new ByteArrayJsonAdapter())
.build();
JsonAdapter<MapOfByteArrays> jsonAdapter = moshi.adapter(MapOfByteArrays.class);
MapOfByteArrays mapOfByteArrays = new MapOfByteArrays(
Collections.singletonMap("a", new byte[] { 0, -1}));
String json = "{\"map\":{\"a\":\"00ff\"}}";
assertThat(jsonAdapter.toJson(mapOfByteArrays)).isEqualTo(json);
assertThat(jsonAdapter.fromJson(json)).isEqualTo(mapOfByteArrays);
}
static class ByteArrayJsonAdapter {
@ToJson String byteArrayToJson(byte[] b) {
return ByteString.of(b).hex();
}
@FromJson byte[] byteArrayFromJson(String s) throws Exception {
return ByteString.decodeHex(s).toByteArray();
}
}
static class MapOfByteArrays {
final Map<String, byte[]> map;
public MapOfByteArrays(Map<String, byte[]> map) {
this.map = map;
}
@Override public boolean equals(Object o) {
return o instanceof MapOfByteArrays && o.toString().equals(toString());
}
@Override public int hashCode() {
return toString().hashCode();
}
@Override public String toString() {
StringBuilder result = new StringBuilder();
for (Map.Entry<String, byte[]> entry : map.entrySet()) {
if (result.length() > 0) result.append(", ");
result.append(entry.getKey())
.append(":")
.append(Arrays.toString(entry.getValue()));
}
return result.toString();
}
}
static class Point {
final int x;
final int y;

View file

@ -15,6 +15,7 @@
*/
package com.squareup.moshi;
import com.squareup.moshi.internal.Util;
import java.io.IOException;
import java.lang.annotation.Annotation;
import java.lang.annotation.Retention;

View file

@ -25,7 +25,7 @@ import okio.Buffer;
import org.junit.Test;
import static com.squareup.moshi.TestUtil.newReader;
import static com.squareup.moshi.Util.NO_ANNOTATIONS;
import static com.squareup.moshi.internal.Util.NO_ANNOTATIONS;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.Assert.fail;
@ -341,8 +341,23 @@ public final class ClassJsonAdapterTest {
}
}
@Test public void localClassNotSupported() throws Exception {
class Local {
}
try {
ClassJsonAdapter.FACTORY.create(Local.class, NO_ANNOTATIONS, moshi);
fail();
} catch (IllegalArgumentException expected) {
assertThat(expected).hasMessage("Cannot serialize local class "
+ "com.squareup.moshi.ClassJsonAdapterTest$1Local");
}
}
interface Interface {
}
@Test public void interfaceNotSupported() throws Exception {
assertThat(ClassJsonAdapter.FACTORY.create(Runnable.class, NO_ANNOTATIONS, moshi)).isNull();
assertThat(ClassJsonAdapter.FACTORY.create(Interface.class, NO_ANNOTATIONS, moshi)).isNull();
}
static abstract class Abstract {
@ -429,6 +444,23 @@ public final class ClassJsonAdapterTest {
assertThat(fromJson.zipCode).isEqualTo("94043");
}
static final class Box<T> {
final T data;
Box(T data) {
this.data = data;
}
}
@Test public void parameterizedType() throws Exception {
@SuppressWarnings("unchecked")
JsonAdapter<Box<Integer>> adapter = (JsonAdapter<Box<Integer>>) ClassJsonAdapter.FACTORY.create(
Types.newParameterizedTypeWithOwner(ClassJsonAdapterTest.class, Box.class, Integer.class),
NO_ANNOTATIONS, moshi);
assertThat(adapter.fromJson("{\"data\":5}").data).isEqualTo(5);
assertThat(adapter.toJson(new Box<>(5))).isEqualTo("{\"data\":5}");
}
private <T> String toJson(Class<T> type, T value) throws IOException {
@SuppressWarnings("unchecked") // Factory.create returns an adapter that matches its argument.
JsonAdapter<T> jsonAdapter = (JsonAdapter<T>) ClassJsonAdapter.FACTORY.create(

View file

@ -0,0 +1,120 @@
/*
* Copyright (C) 2018 Square, Inc.
*
* 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 com.squareup.moshi;
import java.lang.annotation.Annotation;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import javax.annotation.Nullable;
import org.junit.Test;
import static org.assertj.core.api.Assertions.assertThat;
public final class DeferredAdapterTest {
/**
* When a type's JsonAdapter is circularly-dependent, Moshi creates a 'deferred adapter' to make
* the cycle work. It's important that any adapters that depend on this deferred adapter don't
* leak out until it's ready.
*
* <p>This test sets up a circular dependency [BlueNode -> GreenNode -> BlueNode] and then tries
* to use a GreenNode JSON adapter before the BlueNode JSON adapter is built. It creates a
* similar cycle [BlueNode -> RedNode -> BlueNode] so the order adapters are retrieved is
* insignificant.
*
* <p>This used to trigger a crash because we'd incorrectly put the GreenNode JSON adapter in the
* cache even though it depended upon an incomplete BlueNode JSON adapter.
*/
@Test public void concurrentSafe() {
final List<Throwable> failures = new ArrayList<>();
JsonAdapter.Factory factory = new JsonAdapter.Factory() {
int redAndGreenCount = 0;
@Override public @Nullable JsonAdapter<?> create(
Type type, Set<? extends Annotation> annotations, final Moshi moshi) {
if ((type == RedNode.class || type == GreenNode.class) && redAndGreenCount++ == 1) {
doInAnotherThread(new Runnable() {
@Override public void run() {
GreenNode greenBlue = new GreenNode(new BlueNode(null, null));
assertThat(moshi.adapter(GreenNode.class).toJson(greenBlue))
.isEqualTo("{\"blue\":{}}");
RedNode redBlue = new RedNode(new BlueNode(null, null));
assertThat(moshi.adapter(RedNode.class).toJson(redBlue))
.isEqualTo("{\"blue\":{}}");
}
});
}
return null;
}
};
Moshi moshi = new Moshi.Builder()
.add(factory)
.build();
JsonAdapter<BlueNode> jsonAdapter = moshi.adapter(BlueNode.class);
assertThat(jsonAdapter.toJson(new BlueNode(new GreenNode(new BlueNode(null, null)), null)))
.isEqualTo("{\"green\":{\"blue\":{}}}");
assertThat(failures).isEmpty();
}
private void doInAnotherThread(Runnable runnable) {
ExecutorService executor = Executors.newSingleThreadExecutor();
Future<?> future = executor.submit(runnable);
executor.shutdown();
try {
future.get();
} catch (InterruptedException e) {
throw new RuntimeException(e);
} catch (ExecutionException e) {
throw new RuntimeException(e.getCause());
}
}
static class BlueNode {
@Nullable GreenNode green;
@Nullable RedNode red;
BlueNode(@Nullable GreenNode green, @Nullable RedNode red) {
this.green = green;
this.red = red;
}
}
static class RedNode {
@Nullable BlueNode blue;
RedNode(@Nullable BlueNode blue) {
this.blue = blue;
}
}
static class GreenNode {
@Nullable BlueNode blue;
GreenNode(@Nullable BlueNode blue) {
this.blue = blue;
}
}
}

View file

@ -0,0 +1,338 @@
/*
* Copyright (C) 2018 Square, Inc.
*
* 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 com.squareup.moshi;
import java.util.Arrays;
import java.util.List;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.junit.runners.Parameterized.Parameter;
import org.junit.runners.Parameterized.Parameters;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.Assert.fail;
/** Note that this test makes heavy use of nested blocks, but these are for readability only. */
@RunWith(Parameterized.class)
public final class FlattenTest {
@Parameter public JsonCodecFactory factory;
@Parameters(name = "{0}")
public static List<Object[]> parameters() {
return JsonCodecFactory.factories();
}
@Test public void flattenExample() throws Exception {
Moshi moshi = new Moshi.Builder().build();
JsonAdapter<List<Integer>> integersAdapter =
moshi.adapter(Types.newParameterizedType(List.class, Integer.class));
JsonWriter writer = factory.newWriter();
writer.beginArray();
int token = writer.beginFlatten();
writer.value(1);
integersAdapter.toJson(writer, Arrays.asList(2, 3, 4));
writer.value(5);
writer.endFlatten(token);
writer.endArray();
assertThat(factory.json()).isEqualTo("[1,2,3,4,5]");
}
@Test public void flattenObject() throws Exception {
JsonWriter writer = factory.newWriter();
writer.beginObject();
{
writer.name("a");
writer.value("aaa");
int token = writer.beginFlatten();
{
writer.beginObject();
{
writer.name("b");
writer.value("bbb");
}
writer.endObject();
}
writer.endFlatten(token);
writer.name("c");
writer.value("ccc");
}
writer.endObject();
assertThat(factory.json()).isEqualTo("{\"a\":\"aaa\",\"b\":\"bbb\",\"c\":\"ccc\"}");
}
@Test public void flattenArray() throws Exception {
JsonWriter writer = factory.newWriter();
writer.beginArray();
{
writer.value("a");
int token = writer.beginFlatten();
{
writer.beginArray();
{
writer.value("b");
}
writer.endArray();
}
writer.endFlatten(token);
writer.value("c");
}
writer.endArray();
assertThat(factory.json()).isEqualTo("[\"a\",\"b\",\"c\"]");
}
@Test public void recursiveFlatten() throws Exception {
JsonWriter writer = factory.newWriter();
writer.beginArray();
{
writer.value("a");
int token1 = writer.beginFlatten();
{
writer.beginArray();
{
writer.value("b");
int token2 = writer.beginFlatten();
{
writer.beginArray();
{
writer.value("c");
}
writer.endArray();
}
writer.endFlatten(token2);
writer.value("d");
}
writer.endArray();
}
writer.endFlatten(token1);
writer.value("e");
}
writer.endArray();
assertThat(factory.json()).isEqualTo("[\"a\",\"b\",\"c\",\"d\",\"e\"]");
}
@Test public void flattenMultipleNested() throws Exception {
JsonWriter writer = factory.newWriter();
writer.beginArray();
{
writer.value("a");
int token = writer.beginFlatten();
{
writer.beginArray();
{
writer.value("b");
}
writer.endArray();
writer.beginArray();
{
writer.value("c");
}
writer.endArray();
}
writer.endFlatten(token);
writer.value("d");
}
writer.endArray();
assertThat(factory.json()).isEqualTo("[\"a\",\"b\",\"c\",\"d\"]");
}
@Test public void flattenIsOnlyOneLevelDeep() throws Exception {
JsonWriter writer = factory.newWriter();
writer.beginArray();
{
writer.value("a");
int token = writer.beginFlatten();
{
writer.beginArray();
{
writer.value("b");
writer.beginArray();
{
writer.value("c");
}
writer.endArray();
writer.value("d");
}
writer.endArray();
}
writer.endFlatten(token);
writer.value("e");
}
writer.endArray();
assertThat(factory.json()).isEqualTo("[\"a\",\"b\",[\"c\"],\"d\",\"e\"]");
}
@Test public void flattenOnlySomeChildren() throws Exception {
JsonWriter writer = factory.newWriter();
writer.beginArray();
{
writer.value("a");
int token = writer.beginFlatten();
{
writer.beginArray();
{
writer.value("b");
}
writer.endArray();
}
writer.endFlatten(token);
writer.beginArray();
{
writer.value("c");
}
writer.endArray();
writer.value("d");
}
writer.endArray();
assertThat(factory.json()).isEqualTo("[\"a\",\"b\",[\"c\"],\"d\"]");
}
@Test public void multipleCallsToFlattenSameNesting() throws Exception {
JsonWriter writer = factory.newWriter();
writer.beginArray();
{
writer.value("a");
int token1 = writer.beginFlatten();
{
writer.beginArray();
{
writer.value("b");
}
writer.endArray();
int token2 = writer.beginFlatten();
{
writer.beginArray();
{
writer.value("c");
}
writer.endArray();
}
writer.endFlatten(token2);
writer.beginArray();
{
writer.value("d");
}
writer.endArray();
}
writer.endFlatten(token1);
writer.value("e");
}
writer.endArray();
assertThat(factory.json()).isEqualTo("[\"a\",\"b\",\"c\",\"d\",\"e\"]");
}
@Test public void deepFlatten() throws Exception {
JsonWriter writer = factory.newWriter();
writer.beginArray();
{
int token1 = writer.beginFlatten();
{
writer.beginArray();
{
int token2 = writer.beginFlatten();
{
writer.beginArray();
{
int token3 = writer.beginFlatten();
{
writer.beginArray();
{
writer.value("a");
}
writer.endArray();
}
writer.endFlatten(token3);
}
writer.endArray();
}
writer.endFlatten(token2);
}
writer.endArray();
}
writer.endFlatten(token1);
}
writer.endArray();
assertThat(factory.json()).isEqualTo("[\"a\"]");
}
@Test public void flattenTopLevel() {
JsonWriter writer = factory.newWriter();
try {
writer.beginFlatten();
fail();
} catch (IllegalStateException e) {
assertThat(e).hasMessage("Nesting problem.");
}
}
@Test public void flattenDoesNotImpactOtherTypesInObjects() throws Exception {
JsonWriter writer = factory.newWriter();
writer.beginObject();
{
int token = writer.beginFlatten();
writer.name("a");
writer.beginArray();
writer.value("aaa");
writer.endArray();
writer.beginObject();
{
writer.name("b");
writer.value("bbb");
}
writer.endObject();
writer.name("c");
writer.beginArray();
writer.value("ccc");
writer.endArray();
writer.endFlatten(token);
}
writer.endObject();
assertThat(factory.json()).isEqualTo("{\"a\":[\"aaa\"],\"b\":\"bbb\",\"c\":[\"ccc\"]}");
}
@Test public void flattenDoesNotImpactOtherTypesInArrays() throws Exception {
JsonWriter writer = factory.newWriter();
writer.beginArray();
{
int token = writer.beginFlatten();
{
writer.beginObject();
{
writer.name("a");
writer.value("aaa");
}
writer.endObject();
writer.beginArray();
{
writer.value("bbb");
}
writer.endArray();
writer.value("ccc");
writer.beginObject();
{
writer.name("d");
writer.value("ddd");
}
writer.endObject();
}
writer.endFlatten(token);
}
writer.endArray();
assertThat(factory.json()).isEqualTo("[{\"a\":\"aaa\"},\"bbb\",\"ccc\",{\"d\":\"ddd\"}]");
}
}

View file

@ -21,6 +21,7 @@ import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import javax.annotation.Nullable;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
@ -94,6 +95,45 @@ public final class JsonAdapterTest {
assertThat(factory.json()).isEqualTo("[\"A\",null,\"C\"]");
}
@Test public void nonNull() throws Exception {
JsonAdapter<String> toUpperCase = new JsonAdapter<String>() {
@Override public String fromJson(JsonReader reader) throws IOException {
return reader.nextString().toUpperCase(Locale.US);
}
@Override public void toJson(JsonWriter writer, String value) throws IOException {
writer.value(value.toUpperCase(Locale.US));
}
}.nonNull();
JsonReader reader = factory.newReader("[\"a\", null, \"c\"]");
reader.beginArray();
assertThat(toUpperCase.fromJson(reader)).isEqualTo("A");
try {
toUpperCase.fromJson(reader);
fail();
} catch (JsonDataException expected) {
assertThat(expected).hasMessage("Unexpected null at $[1]");
assertThat(reader.nextNull()).isNull();
}
assertThat(toUpperCase.fromJson(reader)).isEqualTo("C");
reader.endArray();
JsonWriter writer = factory.newWriter();
writer.beginArray();
toUpperCase.toJson(writer, "a");
try {
toUpperCase.toJson(writer, null);
fail();
} catch (JsonDataException expected) {
assertThat(expected).hasMessage("Unexpected null at $[1]");
writer.nullValue();
}
toUpperCase.toJson(writer, "c");
writer.endArray();
assertThat(factory.json()).isEqualTo("[\"A\",null,\"C\"]");
}
@Test public void failOnUnknown() throws Exception {
JsonAdapter<String> alwaysSkip = new JsonAdapter<String>() {
@Override public String fromJson(JsonReader reader) throws IOException {
@ -145,6 +185,24 @@ public final class JsonAdapterTest {
+ "]");
}
@Test public void indentDisallowsNull() throws Exception {
JsonAdapter<Object> adapter = new JsonAdapter<Object>() {
@Override public Object fromJson(JsonReader reader) {
throw new AssertionError();
}
@Override public void toJson(JsonWriter writer, Object value) {
throw new AssertionError();
}
};
try {
adapter.indent(null);
fail();
} catch (NullPointerException expected) {
assertThat(expected).hasMessage("indent == null");
}
}
@Test public void serializeNulls() throws Exception {
JsonAdapter<Map<String, String>> serializeNulls = new JsonAdapter<Map<String, String>>() {
@Override public Map<String, String> fromJson(JsonReader reader) throws IOException {
@ -164,4 +222,67 @@ public final class JsonAdapterTest {
serializeNulls.toJson(writer, Collections.<String, String>singletonMap("a", null));
assertThat(factory.json()).isEqualTo("{\"a\":null}");
}
@Test public void stringDocumentMustBeFullyConsumed() throws IOException {
JsonAdapter<String> brokenAdapter = new JsonAdapter<String>() {
@Override public String fromJson(JsonReader reader) throws IOException {
return "Forgot to call reader.nextString().";
}
@Override public void toJson(JsonWriter writer, @Nullable String value) throws IOException {
throw new AssertionError();
}
};
try {
brokenAdapter.fromJson("\"value\"");
fail();
} catch (JsonDataException e) {
assertThat(e).hasMessage("JSON document was not fully consumed.");
}
}
@Test public void adapterFromJsonStringPeeksAtEnd() throws IOException {
JsonAdapter<Boolean> adapter = new JsonAdapter<Boolean>() {
@Override public Boolean fromJson(JsonReader reader) throws IOException {
return reader.nextBoolean();
}
@Override public void toJson(JsonWriter writer, @Nullable Boolean value) throws IOException {
throw new AssertionError();
}
};
try {
adapter.fromJson("true true");
fail();
} catch (JsonEncodingException e) {
assertThat(e).hasMessage(
"Use JsonReader.setLenient(true) to accept malformed JSON at path $");
}
}
@Test public void lenientAdapterFromJsonStringDoesNotPeekAtEnd() throws IOException {
JsonAdapter<Boolean> adapter = new JsonAdapter<Boolean>() {
@Override public Boolean fromJson(JsonReader reader) throws IOException {
return reader.nextBoolean();
}
@Override public void toJson(JsonWriter writer, @Nullable Boolean value) throws IOException {
throw new AssertionError();
}
}.lenient();
assertThat(adapter.fromJson("true true")).isEqualTo(true);
}
@Test public void adaptersDelegateLeniency() throws IOException {
JsonAdapter<Boolean> adapter = new JsonAdapter<Boolean>() {
@Override public Boolean fromJson(JsonReader reader) throws IOException {
return reader.nextBoolean();
}
@Override public void toJson(JsonWriter writer, @Nullable Boolean value) throws IOException {
throw new AssertionError();
}
}.lenient().nonNull();
assertThat(adapter.fromJson("true true")).isEqualTo(true);
}
}

View file

@ -96,9 +96,38 @@ abstract class JsonCodecFactory {
}
};
final JsonCodecFactory valuePeek = new JsonCodecFactory() {
@Override public JsonReader newReader(String json) throws IOException {
return value.newReader(json).peekJson();
}
// TODO(jwilson): fix precision checks and delete his method.
@Override boolean implementsStrictPrecision() {
return false;
}
@Override JsonWriter newWriter() {
return value.newWriter();
}
@Override String json() {
return value.json();
}
// TODO(jwilson): support BigDecimal and BigInteger and delete his method.
@Override boolean supportsBigNumbers() {
return false;
}
@Override public String toString() {
return "ValuePeek";
}
};
return Arrays.asList(
new Object[] { utf8 },
new Object[] { value });
new Object[] { value },
new Object[] { valuePeek });
}
abstract JsonReader newReader(String json) throws IOException;

View file

@ -332,8 +332,16 @@ public final class JsonQualifiersTest {
moshi.adapter(StringAndFooString.class);
fail();
} catch (IllegalArgumentException expected) {
assertThat(expected).hasMessage("No @FromJson adapter for class java.lang.String "
assertThat(expected).hasMessage("No @FromJson adapter for class java.lang.String annotated "
+ "[@com.squareup.moshi.JsonQualifiersTest$FooPrefix()]"
+ "\nfor class java.lang.String b"
+ "\nfor class com.squareup.moshi.JsonQualifiersTest$StringAndFooString");
assertThat(expected).hasCauseExactlyInstanceOf(IllegalArgumentException.class);
assertThat(expected.getCause()).hasMessage("No @FromJson adapter for class java.lang.String "
+ "annotated [@com.squareup.moshi.JsonQualifiersTest$FooPrefix()]");
assertThat(expected.getCause()).hasCauseExactlyInstanceOf(IllegalArgumentException.class);
assertThat(expected.getCause().getCause()).hasMessage("No next JsonAdapter for class "
+ "java.lang.String annotated [@com.squareup.moshi.JsonQualifiersTest$FooPrefix()]");
}
}
@ -353,8 +361,16 @@ public final class JsonQualifiersTest {
moshi.adapter(StringAndFooString.class);
fail();
} catch (IllegalArgumentException expected) {
assertThat(expected).hasMessage("No @ToJson adapter for class java.lang.String "
assertThat(expected).hasMessage("No @ToJson adapter for class java.lang.String annotated "
+ "[@com.squareup.moshi.JsonQualifiersTest$FooPrefix()]"
+ "\nfor class java.lang.String b"
+ "\nfor class com.squareup.moshi.JsonQualifiersTest$StringAndFooString");
assertThat(expected).hasCauseExactlyInstanceOf(IllegalArgumentException.class);
assertThat(expected.getCause()).hasMessage("No @ToJson adapter for class java.lang.String "
+ "annotated [@com.squareup.moshi.JsonQualifiersTest$FooPrefix()]");
assertThat(expected.getCause()).hasCauseExactlyInstanceOf(IllegalArgumentException.class);
assertThat(expected.getCause().getCause()).hasMessage("No next JsonAdapter for class "
+ "java.lang.String annotated [@com.squareup.moshi.JsonQualifiersTest$FooPrefix()]");
}
}

View file

@ -35,6 +35,7 @@ public final class JsonReaderPathTest {
return JsonCodecFactory.factories();
}
@SuppressWarnings("CheckReturnValue")
@Test public void path() throws IOException {
JsonReader reader = factory.newReader("{\"a\":[2,true,false,null,\"b\",{\"c\":\"d\"},[3]]}");
assertThat(reader.getPath()).isEqualTo("$");
@ -114,6 +115,7 @@ public final class JsonReaderPathTest {
assertThat(reader.getPath()).isEqualTo("$");
}
@SuppressWarnings("CheckReturnValue")
@Test public void objectPath() throws IOException {
JsonReader reader = factory.newReader("{\"a\":1,\"b\":2}");
assertThat(reader.getPath()).isEqualTo("$");
@ -154,6 +156,7 @@ public final class JsonReaderPathTest {
assertThat(reader.getPath()).isEqualTo("$");
}
@SuppressWarnings("CheckReturnValue")
@Test public void arrayPath() throws IOException {
JsonReader reader = factory.newReader("[1,2]");
assertThat(reader.getPath()).isEqualTo("$");
@ -212,6 +215,7 @@ public final class JsonReaderPathTest {
assertThat(reader.getPath()).isEqualTo("$.null");
}
@SuppressWarnings("CheckReturnValue")
@Test public void skipObjectValues() throws IOException {
JsonReader reader = factory.newReader("{\"a\":1,\"b\":2}");
reader.beginObject();

View file

@ -33,10 +33,12 @@ import static com.squareup.moshi.JsonReader.Token.STRING;
import static com.squareup.moshi.TestUtil.repeat;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.fail;
import static org.junit.Assume.assumeTrue;
@RunWith(Parameterized.class)
@SuppressWarnings("CheckReturnValue")
public final class JsonReaderTest {
@Parameter public JsonCodecFactory factory;
@ -852,6 +854,21 @@ public final class JsonReaderTest {
reader.endArray();
}
@Test public void selectStringWithoutString() throws IOException {
JsonReader.Options numbers = JsonReader.Options.of("1", "2.0", "true", "4");
JsonReader reader = newReader("[0, 2.0, true, \"4\"]");
reader.beginArray();
assertThat(reader.selectString(numbers)).isEqualTo(-1);
reader.skipValue();
assertThat(reader.selectString(numbers)).isEqualTo(-1);
reader.skipValue();
assertThat(reader.selectString(numbers)).isEqualTo(-1);
reader.skipValue();
assertThat(reader.selectString(numbers)).isEqualTo(3);
reader.endArray();
}
@Test public void stringToNumberCoersion() throws Exception {
JsonReader reader = newReader("[\"0\", \"9223372036854775807\", \"1.5\"]");
reader.beginArray();
@ -945,4 +962,227 @@ public final class JsonReaderTest {
assertThat(value).isEqualTo(
Collections.singletonMap("pizzas", Arrays.asList("cheese", "pepperoni")));
}
@Test public void skipName() throws IOException {
JsonReader reader = newReader("{\"a\":1}");
reader.beginObject();
reader.skipName();
assertThat(reader.peek()).isEqualTo(JsonReader.Token.NUMBER);
reader.skipValue();
reader.endObject();
}
@Test public void skipNameOnValueFails() throws IOException {
JsonReader reader = newReader("1");
try {
reader.skipName();
fail();
} catch (JsonDataException expected) {
}
assertThat(reader.nextInt()).isEqualTo(1);
}
@Test public void emptyDocumentHasNextReturnsFalse() throws IOException {
JsonReader reader = newReader("1");
reader.readJsonValue();
assertThat(reader.hasNext()).isFalse();
}
@Test public void skipValueAtEndOfObjectFails() throws IOException {
JsonReader reader = newReader("{}");
reader.beginObject();
try {
reader.skipValue();
fail();
} catch (JsonDataException expected) {
assertThat(expected).hasMessage("Expected a value but was END_OBJECT at path $.");
}
reader.endObject();
assertThat(reader.peek()).isEqualTo(JsonReader.Token.END_DOCUMENT);
}
@Test public void skipValueAtEndOfArrayFails() throws IOException {
JsonReader reader = newReader("[]");
reader.beginArray();
try {
reader.skipValue();
fail();
} catch (JsonDataException expected) {
assertThat(expected).hasMessage("Expected a value but was END_ARRAY at path $[0]");
}
reader.endArray();
assertThat(reader.peek()).isEqualTo(JsonReader.Token.END_DOCUMENT);
}
@Test public void skipValueAtEndOfDocumentFails() throws IOException {
JsonReader reader = newReader("1");
reader.nextInt();
try {
reader.skipValue();
fail();
} catch (JsonDataException expected) {
assertThat(expected).hasMessage("Expected a value but was END_DOCUMENT at path $");
}
assertThat(reader.peek()).isEqualTo(JsonReader.Token.END_DOCUMENT);
}
@Test public void basicPeekJson() throws IOException {
JsonReader reader = newReader("{\"a\":12,\"b\":[34,56],\"c\":78}");
reader.beginObject();
assertThat(reader.nextName()).isEqualTo("a");
assertThat(reader.nextInt()).isEqualTo(12);
assertThat(reader.nextName()).isEqualTo("b");
reader.beginArray();
assertThat(reader.nextInt()).isEqualTo(34);
// Peek.
JsonReader peekReader = reader.peekJson();
assertThat(peekReader.nextInt()).isEqualTo(56);
peekReader.endArray();
assertThat(peekReader.nextName()).isEqualTo("c");
assertThat(peekReader.nextInt()).isEqualTo(78);
peekReader.endObject();
assertThat(peekReader.peek()).isEqualTo(JsonReader.Token.END_DOCUMENT);
// Read again.
assertThat(reader.nextInt()).isEqualTo(56);
reader.endArray();
assertThat(reader.nextName()).isEqualTo("c");
assertThat(reader.nextInt()).isEqualTo(78);
reader.endObject();
assertThat(reader.peek()).isEqualTo(JsonReader.Token.END_DOCUMENT);
}
/**
* We have a document that requires 12 operations to read. We read it step-by-step with one real
* reader. Before each of the real readers operations we create a peeking reader and let it read
* the rest of the document.
*/
@Test public void peekJsonReader() throws IOException {
JsonReader reader = newReader("[12,34,{\"a\":56,\"b\":78},90]");
for (int i = 0; i < 12; i++) {
readPeek12Steps(reader.peekJson(), i, 12);
readPeek12Steps(reader, i, i + 1);
}
}
/**
* Read a fragment of {@code reader}. This assumes the fixed document defined in {@link
* #peekJsonReader} and reads a range of it on each call.
*/
private void readPeek12Steps(JsonReader reader, int from, int until) throws IOException {
switch (from) {
case 0:
if (until == 0) break;
reader.beginArray();
assertThat(reader.getPath()).isEqualTo("$[0]");
case 1:
if (until == 1) break;
assertThat(reader.nextInt()).isEqualTo(12);
assertThat(reader.getPath()).isEqualTo("$[1]");
case 2:
if (until == 2) break;
assertThat(reader.nextInt()).isEqualTo(34);
assertThat(reader.getPath()).isEqualTo("$[2]");
case 3:
if (until == 3) break;
reader.beginObject();
assertThat(reader.getPath()).isEqualTo("$[2].");
case 4:
if (until == 4) break;
assertThat(reader.nextName()).isEqualTo("a");
assertThat(reader.getPath()).isEqualTo("$[2].a");
case 5:
if (until == 5) break;
assertThat(reader.nextInt()).isEqualTo(56);
assertThat(reader.getPath()).isEqualTo("$[2].a");
case 6:
if (until == 6) break;
assertThat(reader.nextName()).isEqualTo("b");
assertThat(reader.getPath()).isEqualTo("$[2].b");
case 7:
if (until == 7) break;
assertThat(reader.nextInt()).isEqualTo(78);
assertThat(reader.getPath()).isEqualTo("$[2].b");
case 8:
if (until == 8) break;
reader.endObject();
assertThat(reader.getPath()).isEqualTo("$[3]");
case 9:
if (until == 9) break;
assertThat(reader.nextInt()).isEqualTo(90);
assertThat(reader.getPath()).isEqualTo("$[4]");
case 10:
if (until == 10) break;
reader.endArray();
assertThat(reader.getPath()).isEqualTo("$");
case 11:
if (until == 11) break;
assertThat(reader.peek()).isEqualTo(JsonReader.Token.END_DOCUMENT);
assertThat(reader.getPath()).isEqualTo("$");
}
}
/** Confirm that we can peek in every state of the UTF-8 reader. */
@Test public void peekAfterPeek() throws IOException {
JsonReader reader = newReader(
"[{\"a\":\"aaa\",'b':'bbb',c:c,\"d\":\"d\"},true,false,null,1,2.0]");
reader.setLenient(true);
readValue(reader, true);
reader.peekJson();
}
@Test public void peekAfterPromoteNameToValue() throws IOException {
JsonReader reader = newReader("{\"a\":\"b\"}");
reader.beginObject();
reader.promoteNameToValue();
assertEquals("a", reader.peekJson().nextString());
assertEquals("a", reader.nextString());
assertEquals("b", reader.peekJson().nextString());
assertEquals("b", reader.nextString());
reader.endObject();
}
/** Peek a value, then read it, recursively. */
private void readValue(JsonReader reader, boolean peekJsonFirst) throws IOException {
JsonReader.Token token = reader.peek();
if (peekJsonFirst) {
readValue(reader.peekJson(), false);
}
switch (token) {
case BEGIN_ARRAY:
reader.beginArray();
while (reader.hasNext()) {
readValue(reader, peekJsonFirst);
}
reader.peekJson().endArray();
reader.endArray();
break;
case BEGIN_OBJECT:
reader.beginObject();
while (reader.hasNext()) {
assertNotNull(reader.peekJson().nextName());
assertNotNull(reader.nextName());
readValue(reader, peekJsonFirst);
}
reader.peekJson().endObject();
reader.endObject();
break;
case STRING:
reader.nextString();
break;
case NUMBER:
reader.nextDouble();
break;
case BOOLEAN:
reader.nextBoolean();
break;
case NULL:
reader.nextNull();
break;
default:
throw new AssertionError();
}
}
}

Some files were not shown because too many files have changed in this diff Show more