From a187cecedf35921ddbd7c9a7fbec2f3eaff27599 Mon Sep 17 00:00:00 2001 From: Sri Adarsh Kumar Date: Thu, 17 Oct 2024 14:51:34 +0200 Subject: [PATCH] Move jackson-datatype-money module from zalando --- README.md | 3 + javax-money/README.md | 212 ++++++++++ javax-money/pom.xml | 124 ++++++ .../jackson/datatype/money/AmountWriter.java | 16 + .../money/BigDecimalAmountWriter.java | 17 + .../money/CurrencyUnitDeserializer.java | 32 ++ .../money/CurrencyUnitSerializer.java | 35 ++ .../datatype/money/DecimalAmountWriter.java | 19 + .../jackson/datatype/money/FieldNames.java | 26 ++ .../money/MonetaryAmountDeserializer.java | 77 ++++ .../datatype/money/MonetaryAmountFactory.java | 17 + .../money/MonetaryAmountFormatFactory.java | 20 + .../money/MonetaryAmountSerializer.java | 122 ++++++ .../jackson/datatype/money/MoneyModule.java | 171 ++++++++ .../datatype/money/PackageVersion.java.in | 20 + .../money/QuotedDecimalAmountWriter.java | 20 + .../src/main/resources/META-INF/LICENSE | 23 ++ .../src/main/resources/META-INF/NOTICE | 17 + .../com.fasterxml.jackson.databind.Module | 1 + javax-money/src/moditect/module-info.java | 13 + .../money/CurrencyUnitDeserializerTest.java | 43 ++ .../CurrencyUnitSchemaSerializerTest.java | 25 ++ .../money/CurrencyUnitSerializerTest.java | 27 ++ .../datatype/money/FieldNamesTest.java | 20 + .../money/MonetaryAmountDeserializerTest.java | 273 +++++++++++++ .../MonetaryAmountSchemaSerializerTest.java | 87 ++++ .../money/MonetaryAmountSerializerTest.java | 377 ++++++++++++++++++ .../datatype/money/SchemaTestClass.java | 15 + pom.xml | 2 + 29 files changed, 1854 insertions(+) create mode 100644 javax-money/README.md create mode 100644 javax-money/pom.xml create mode 100644 javax-money/src/main/java/com/fasterxml/jackson/datatype/money/AmountWriter.java create mode 100644 javax-money/src/main/java/com/fasterxml/jackson/datatype/money/BigDecimalAmountWriter.java create mode 100644 javax-money/src/main/java/com/fasterxml/jackson/datatype/money/CurrencyUnitDeserializer.java create mode 100644 javax-money/src/main/java/com/fasterxml/jackson/datatype/money/CurrencyUnitSerializer.java create mode 100644 javax-money/src/main/java/com/fasterxml/jackson/datatype/money/DecimalAmountWriter.java create mode 100644 javax-money/src/main/java/com/fasterxml/jackson/datatype/money/FieldNames.java create mode 100644 javax-money/src/main/java/com/fasterxml/jackson/datatype/money/MonetaryAmountDeserializer.java create mode 100644 javax-money/src/main/java/com/fasterxml/jackson/datatype/money/MonetaryAmountFactory.java create mode 100644 javax-money/src/main/java/com/fasterxml/jackson/datatype/money/MonetaryAmountFormatFactory.java create mode 100644 javax-money/src/main/java/com/fasterxml/jackson/datatype/money/MonetaryAmountSerializer.java create mode 100644 javax-money/src/main/java/com/fasterxml/jackson/datatype/money/MoneyModule.java create mode 100644 javax-money/src/main/java/com/fasterxml/jackson/datatype/money/PackageVersion.java.in create mode 100644 javax-money/src/main/java/com/fasterxml/jackson/datatype/money/QuotedDecimalAmountWriter.java create mode 100644 javax-money/src/main/resources/META-INF/LICENSE create mode 100644 javax-money/src/main/resources/META-INF/NOTICE create mode 100644 javax-money/src/main/resources/META-INF/services/com.fasterxml.jackson.databind.Module create mode 100644 javax-money/src/moditect/module-info.java create mode 100644 javax-money/src/test/java/com/fasterxml/jackson/datatype/money/CurrencyUnitDeserializerTest.java create mode 100644 javax-money/src/test/java/com/fasterxml/jackson/datatype/money/CurrencyUnitSchemaSerializerTest.java create mode 100644 javax-money/src/test/java/com/fasterxml/jackson/datatype/money/CurrencyUnitSerializerTest.java create mode 100644 javax-money/src/test/java/com/fasterxml/jackson/datatype/money/FieldNamesTest.java create mode 100644 javax-money/src/test/java/com/fasterxml/jackson/datatype/money/MonetaryAmountDeserializerTest.java create mode 100644 javax-money/src/test/java/com/fasterxml/jackson/datatype/money/MonetaryAmountSchemaSerializerTest.java create mode 100644 javax-money/src/test/java/com/fasterxml/jackson/datatype/money/MonetaryAmountSerializerTest.java create mode 100644 javax-money/src/test/java/com/fasterxml/jackson/datatype/money/SchemaTestClass.java diff --git a/README.md b/README.md index 361cf31..e450c45 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,7 @@ datatype modules to support 3rd party libraries. Currently included are: * [jackson-datatype-joda-money](joda-money/) for [Joda-Money](https://www.joda.org/joda-money/) datatypes +* [jackson-datatype-money](javax-money/) for [JavaMoney](https://javamoney.github.io/) datatypes (starting with Jackson 2.19) * JSR-353/JSON-P: 2 variants (starting with Jackson 2.12.2) * [jackson-datatype-jsr353](jsr-353/) for older "javax.json" [JSR-353](https://www.jcp.org/en/jsr/detail?id=353) (aka JSON-P) datatypes (package `javax.json`) * [jackson-datatype-jakarta-jsonp](jakarta-jsonp/) for newer "Jakarta" JSON-P datatypes (package `jakarta.json`) @@ -16,6 +17,7 @@ Currently included are: Note that this repo was created for Jackson 2.11: prior to this, individual datatype modules had their own repositories. + ## License All modules are licensed under [Apache License 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt). @@ -62,6 +64,7 @@ mapper.registerModule(new JSONPModule()); // new (jakarta) json-P API ObjectMapper mapper = JsonMapper.builder() .addModule(new JsonOrgModule()) .addModule(new JodaMoneyModule()) + .addModule(new MoneyModule()) // ONE of these (not both): .addModule(new JSR353Module()) // old (javax) json-p API .addModule(new JSONPModule()) // new (jakarta) json-P API diff --git a/javax-money/README.md b/javax-money/README.md new file mode 100644 index 0000000..2e02b54 --- /dev/null +++ b/javax-money/README.md @@ -0,0 +1,212 @@ +# Jackson Datatype Money + +*Jackson Datatype Money* is a [Jackson](https://github.com/codehaus/jackson) module to support JSON serialization and +deserialization of [JavaMoney](https://github.com/JavaMoney/jsr354-api) data types. It fills a niche, in that it +integrates JavaMoney and Jackson so that they work seamlessly together, without requiring additional +developer effort. In doing so, it aims to perform a small but repetitive task — once and for all. + +With this library, it is possible to represent monetary amounts in JSON as follows: + +```json +{ + "amount": 29.95, + "currency": "EUR" +} +``` + +## Features + +- enables you to express monetary amounts in JSON +- can be used in a REST APIs +- customized field names +- localization of formatted monetary amounts +- allows you to implement RESTful API endpoints that format monetary amounts based on the Accept-Language header +- is unique and flexible + +## Dependencies + +- Java 8 or higher +- Any build tool using Maven Central, or direct download +- Jackson +- JavaMoney + +## Installation + +Add the following dependency to your project: + +```xml + + + com.fasterxml.jackson.datatype + jackson-datatype-money + ${jackson-datatype-money.version} + +``` + +For ultimate flexibility, this module is compatible with the official version as well as the backport of JavaMoney. The +actual version will be selected by a profile based on the current JDK version. + +## Configuration + +Register the module with your `ObjectMapper`: + +```java +ObjectMapper mapper = new ObjectMapper() + .registerModule(new MoneyModule()); +``` + +Alternatively, you can use the SPI capabilities: + +```java +ObjectMapper mapper = new ObjectMapper() + .findAndRegisterModules(); +``` + +### Serialization + +For serialization this module currently supports +[ +`javax.money.MonetaryAmount`](https://github.com/JavaMoney/jsr354-api/blob/master/src/main/java/javax/money/MonetaryAmount.java) +and will, by default, serialize it as: + +```json +{ + "amount": 99.95, + "currency": "EUR" +} +``` + +To serialize number as a JSON string, you have to configure the quoted decimal number value serializer: + +```java +ObjectMapper mapper = new ObjectMapper() + .registerModule(new MoneyModule().withQuotedDecimalNumbers()); +``` + +```json +{ + "amount": "99.95", + "currency": "EUR" +} +``` + +### Formatting + +A special feature for serializing monetary amounts is *formatting*, which is **disabled by default**. To enable it, you +have to either enable default formatting: + +```java +ObjectMapper mapper = new ObjectMapper() + .registerModule(new MoneyModule().withDefaultFormatting()); +``` + +... or pass in a `MonetaryAmountFormatFactory` implementation to the `MoneyModule`: + +```java +ObjectMapper mapper = new ObjectMapper() + .registerModule(new MoneyModule() + .withFormatting(new CustomMonetaryAmountFormatFactory())); +``` + +The default formatting delegates directly to `MonetaryFormats.getAmountFormat(Locale, String...)`. + +Formatting only affects the serialization and can be customized based on the *current* locale, as defined by the +[ +`SerializationConfig`](https://fasterxml.github.io/jackson-databind/javadoc/2.0.0/com/fasterxml/jackson/databind/SerializationConfig.html#with\(java.util.Locale\)). +This allows to implement RESTful API endpoints +that format monetary amounts based on the `Accept-Language` header. + +The first example serializes a monetary amount using the `de_DE` locale: + +```java +ObjectWriter writer = mapper.writer().with(Locale.GERMANY); +writer. + +writeValueAsString(Money.of(29.95, "EUR")); +``` + +```json +{ + "amount": 29.95, + "currency": "EUR", + "formatted": "29,95 EUR" +} +``` + +The following example uses `en_US`: + +```java +ObjectWriter writer = mapper.writer().with(Locale.US); +writer. + +writeValueAsString(Money.of(29.95, "USD")); +``` + +```json +{ + "amount": 29.95, + "currency": "USD", + "formatted": "USD29.95" +} +``` + +More sophisticated formatting rules can be supported by implementing `MonetaryAmountFormatFactory` directly. + +### Deserialization + +This module will use `org.javamoney.moneta.Money` as an implementation for `javax.money.MonetaryAmount` by default when +deserializing money values. If you need a different implementation, you can pass a different `MonetaryAmountFactory` +to the `MoneyModule`: + +```java +ObjectMapper mapper = new ObjectMapper() + .registerModule(new MoneyModule() + .withMonetaryAmount(new CustomMonetaryAmountFactory())); +``` + +You can also pass in a method reference: + +```java +ObjectMapper mapper = new ObjectMapper() + .registerModule(new MoneyModule() + .withMonetaryAmount(FastMoney::of)); +``` + +*Jackson Datatype Money* comes with support for all `MonetaryAmount` implementations from Moneta, the reference +implementation of JavaMoney: + +| `MonetaryAmount` Implementation | Factory | +|-------------------------------------|-----------------------------------------------------------------------------------------------------------------------| +| `org.javamoney.moneta.FastMoney` | [`new MoneyModule().withFastMoney()`](src/main/java/com/fasterxml/jackson/datatype/money/FastMoneyFactory.java) | +| `org.javamoney.moneta.Money` | [`new MoneyModule().withMoney()`](src/main/java/com/fasterxml/jackson/datatype/money/MoneyFactory.java) | +| `org.javamoney.moneta.RoundedMoney` | [`new MoneyModule().withRoundedMoney()`](src/main/java/com/fasterxml/jackson/datatype/money/RoundedMoneyFactory.java) | | + +Module supports deserialization of amount number from JSON number as well as from JSON string without any special +configuration required. + +### Custom Field Names + +As you have seen in the previous examples the `MoneyModule` uses the field names `amount`, `currency` and `formatted` +by default. Those names can be overridden if desired: + +```java +ObjectMapper mapper = new ObjectMapper() + .registerModule(new MoneyModule() + .withAmountFieldName("value") + .withCurrencyFieldName("unit") + .withFormattedFieldName("pretty")); +``` + +## Usage + +After registering and configuring the module you're now free to directly use `MonetaryAmount` in your data types: + +```java +import javax.money.MonetaryAmount; + +public class Product { + private String sku; + private MonetaryAmount price; + ... +} +``` \ No newline at end of file diff --git a/javax-money/pom.xml b/javax-money/pom.xml new file mode 100644 index 0000000..575601d --- /dev/null +++ b/javax-money/pom.xml @@ -0,0 +1,124 @@ + + + + + + + 4.0.0 + + com.fasterxml.jackson.datatype + jackson-datatypes-misc-parent + 2.19.0-SNAPSHOT + + jackson-datatype-money + Jackson datatype: javax-money + jar + 2.19.0-SNAPSHOT + Support for datatypes of Javax Money library (https://javamoney.github.io/) + + https://github.com/FasterXML/jackson-datatypes-misc + + + The Apache Software License, Version 2.0 + https://www.apache.org/licenses/LICENSE-2.0.txt + repo + + + + + com/fasterxml/jackson/datatype/money + ${project.groupId}.money + 2.0.6 + 5.9.2 + + + + + javax.money + money-api + 1.1 + + + + org.javamoney.moneta + moneta-core + 1.4.2 + + + + org.apiguardian + apiguardian-api + 1.1.2 + + + com.google.code.findbugs + jsr305 + 3.0.2 + provided + + + org.projectlombok + lombok + 1.18.34 + provided + + + + + org.slf4j + slf4j-nop + ${slf4j.version} + test + + + org.assertj + assertj-core + 3.24.2 + test + + + org.mockito + mockito-core + 4.5.1 + test + + + com.fasterxml.jackson.module + jackson-module-jsonSchema + test + + + com.kjetland + mbknor-jackson-jsonschema_2.12 + 1.0.39 + test + + + javax.validation + validation-api + 2.0.1.Final + test + + + + pl.pragmatists + JUnitParams + 1.1.1 + test + + + + + + + com.google.code.maven-replacer-plugin + replacer + + + org.moditect + moditect-maven-plugin + + + + + diff --git a/javax-money/src/main/java/com/fasterxml/jackson/datatype/money/AmountWriter.java b/javax-money/src/main/java/com/fasterxml/jackson/datatype/money/AmountWriter.java new file mode 100644 index 0000000..0327409 --- /dev/null +++ b/javax-money/src/main/java/com/fasterxml/jackson/datatype/money/AmountWriter.java @@ -0,0 +1,16 @@ +package com.fasterxml.jackson.datatype.money; + +import org.apiguardian.api.API; + +import javax.money.MonetaryAmount; + +import static org.apiguardian.api.API.Status.EXPERIMENTAL; + +@API(status = EXPERIMENTAL) +public interface AmountWriter { + + Class getType(); + + T write(MonetaryAmount amount); + +} diff --git a/javax-money/src/main/java/com/fasterxml/jackson/datatype/money/BigDecimalAmountWriter.java b/javax-money/src/main/java/com/fasterxml/jackson/datatype/money/BigDecimalAmountWriter.java new file mode 100644 index 0000000..849a360 --- /dev/null +++ b/javax-money/src/main/java/com/fasterxml/jackson/datatype/money/BigDecimalAmountWriter.java @@ -0,0 +1,17 @@ +package com.fasterxml.jackson.datatype.money; + +import org.apiguardian.api.API; + +import java.math.BigDecimal; + +import static org.apiguardian.api.API.Status.EXPERIMENTAL; + +@API(status = EXPERIMENTAL) +public interface BigDecimalAmountWriter extends AmountWriter { + + @Override + default Class getType() { + return BigDecimal.class; + } + +} diff --git a/javax-money/src/main/java/com/fasterxml/jackson/datatype/money/CurrencyUnitDeserializer.java b/javax-money/src/main/java/com/fasterxml/jackson/datatype/money/CurrencyUnitDeserializer.java new file mode 100644 index 0000000..7ed5c34 --- /dev/null +++ b/javax-money/src/main/java/com/fasterxml/jackson/datatype/money/CurrencyUnitDeserializer.java @@ -0,0 +1,32 @@ +package com.fasterxml.jackson.datatype.money; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.jsontype.TypeDeserializer; +import org.apiguardian.api.API; + +import javax.money.CurrencyUnit; +import javax.money.Monetary; +import java.io.IOException; + +import static org.apiguardian.api.API.Status.MAINTAINED; + +@API(status = MAINTAINED) +public final class CurrencyUnitDeserializer extends JsonDeserializer { + + @Override + public Object deserializeWithType(final JsonParser parser, final DeserializationContext context, + final TypeDeserializer deserializer) throws IOException { + + // effectively assuming no type information at all + return deserialize(parser, context); + } + + @Override + public CurrencyUnit deserialize(final JsonParser parser, final DeserializationContext context) throws IOException { + final String currencyCode = parser.getValueAsString(); + return Monetary.getCurrency(currencyCode); + } + +} diff --git a/javax-money/src/main/java/com/fasterxml/jackson/datatype/money/CurrencyUnitSerializer.java b/javax-money/src/main/java/com/fasterxml/jackson/datatype/money/CurrencyUnitSerializer.java new file mode 100644 index 0000000..fb511c0 --- /dev/null +++ b/javax-money/src/main/java/com/fasterxml/jackson/datatype/money/CurrencyUnitSerializer.java @@ -0,0 +1,35 @@ +package com.fasterxml.jackson.datatype.money; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.jsonFormatVisitors.JsonFormatVisitorWrapper; +import com.fasterxml.jackson.databind.ser.std.StdSerializer; +import org.apiguardian.api.API; + +import javax.money.CurrencyUnit; +import java.io.IOException; + +import static org.apiguardian.api.API.Status.MAINTAINED; + +@API(status = MAINTAINED) +public final class CurrencyUnitSerializer extends StdSerializer { + + CurrencyUnitSerializer() { + super(CurrencyUnit.class); + } + + @Override + public void serialize(final CurrencyUnit value, final JsonGenerator generator, final SerializerProvider serializers) + throws IOException { + generator.writeString(value.getCurrencyCode()); + } + + @Override + public void acceptJsonFormatVisitor(final JsonFormatVisitorWrapper visitor, final JavaType hint) + throws JsonMappingException { + visitor.expectStringFormat(hint); + } + +} diff --git a/javax-money/src/main/java/com/fasterxml/jackson/datatype/money/DecimalAmountWriter.java b/javax-money/src/main/java/com/fasterxml/jackson/datatype/money/DecimalAmountWriter.java new file mode 100644 index 0000000..3f5b3a7 --- /dev/null +++ b/javax-money/src/main/java/com/fasterxml/jackson/datatype/money/DecimalAmountWriter.java @@ -0,0 +1,19 @@ +package com.fasterxml.jackson.datatype.money; + +import javax.annotation.Nonnull; +import javax.money.MonetaryAmount; +import java.math.BigDecimal; +import java.math.RoundingMode; + +final class DecimalAmountWriter implements BigDecimalAmountWriter { + + @Override + public BigDecimal write(@Nonnull final MonetaryAmount amount) { + final BigDecimal decimal = amount.getNumber().numberValueExact(BigDecimal.class); + final int defaultFractionDigits = amount.getCurrency().getDefaultFractionDigits(); + final int scale = Math.max(decimal.scale(), defaultFractionDigits); + + return decimal.setScale(scale, RoundingMode.UNNECESSARY); + } + +} diff --git a/javax-money/src/main/java/com/fasterxml/jackson/datatype/money/FieldNames.java b/javax-money/src/main/java/com/fasterxml/jackson/datatype/money/FieldNames.java new file mode 100644 index 0000000..ed06953 --- /dev/null +++ b/javax-money/src/main/java/com/fasterxml/jackson/datatype/money/FieldNames.java @@ -0,0 +1,26 @@ +package com.fasterxml.jackson.datatype.money; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.With; + +@AllArgsConstructor(staticName = "valueOf") +@Getter +final class FieldNames { + + static final FieldNames DEFAULT = FieldNames.valueOf("amount", "currency", "formatted"); + + @With + private final String amount; + + @With + private final String currency; + + @With + private final String formatted; + + static FieldNames defaults() { + return DEFAULT; + } + +} diff --git a/javax-money/src/main/java/com/fasterxml/jackson/datatype/money/MonetaryAmountDeserializer.java b/javax-money/src/main/java/com/fasterxml/jackson/datatype/money/MonetaryAmountDeserializer.java new file mode 100644 index 0000000..e053b50 --- /dev/null +++ b/javax-money/src/main/java/com/fasterxml/jackson/datatype/money/MonetaryAmountDeserializer.java @@ -0,0 +1,77 @@ +package com.fasterxml.jackson.datatype.money; + +import com.fasterxml.jackson.core.JsonParseException; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonToken; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException; +import com.fasterxml.jackson.databind.jsontype.TypeDeserializer; + +import javax.annotation.Nullable; +import javax.money.CurrencyUnit; +import javax.money.MonetaryAmount; +import java.io.IOException; +import java.math.BigDecimal; +import java.util.Arrays; + +import static com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES; +import static java.lang.String.format; + +final class MonetaryAmountDeserializer extends JsonDeserializer { + + private final MonetaryAmountFactory factory; + private final FieldNames names; + + MonetaryAmountDeserializer(final MonetaryAmountFactory factory, final FieldNames names) { + this.factory = factory; + this.names = names; + } + + @Override + public Object deserializeWithType(final JsonParser parser, final DeserializationContext context, + final TypeDeserializer deserializer) throws IOException { + + // effectively assuming no type information at all + return deserialize(parser, context); + } + + @Override + public M deserialize(final JsonParser parser, final DeserializationContext context) throws IOException { + BigDecimal amount = null; + CurrencyUnit currency = null; + + while (parser.nextToken() != JsonToken.END_OBJECT) { + final String field = parser.getCurrentName(); + + parser.nextToken(); + + if (field.equals(names.getAmount())) { + amount = context.readValue(parser, BigDecimal.class); + } else if (field.equals(names.getCurrency())) { + currency = context.readValue(parser, CurrencyUnit.class); + } else if (field.equals(names.getFormatted())) { + //noinspection UnnecessaryContinue + continue; + } else if (context.isEnabled(FAIL_ON_UNKNOWN_PROPERTIES)) { + throw UnrecognizedPropertyException.from(parser, MonetaryAmount.class, field, + Arrays.asList(names.getAmount(), names.getCurrency(), names.getFormatted())); + } else { + parser.skipChildren(); + } + } + + checkPresent(parser, amount, names.getAmount()); + checkPresent(parser, currency, names.getCurrency()); + + return factory.create(amount, currency); + } + + private void checkPresent(final JsonParser parser, @Nullable final Object value, final String name) + throws JsonParseException { + if (value == null) { + throw new JsonParseException(parser, format("Missing property: '%s'", name)); + } + } + +} diff --git a/javax-money/src/main/java/com/fasterxml/jackson/datatype/money/MonetaryAmountFactory.java b/javax-money/src/main/java/com/fasterxml/jackson/datatype/money/MonetaryAmountFactory.java new file mode 100644 index 0000000..e422914 --- /dev/null +++ b/javax-money/src/main/java/com/fasterxml/jackson/datatype/money/MonetaryAmountFactory.java @@ -0,0 +1,17 @@ +package com.fasterxml.jackson.datatype.money; + +import org.apiguardian.api.API; + +import javax.money.CurrencyUnit; +import javax.money.MonetaryAmount; +import java.math.BigDecimal; + +import static org.apiguardian.api.API.Status.STABLE; + +@API(status = STABLE) +@FunctionalInterface +public interface MonetaryAmountFactory { + + M create(BigDecimal amount, CurrencyUnit currency); + +} diff --git a/javax-money/src/main/java/com/fasterxml/jackson/datatype/money/MonetaryAmountFormatFactory.java b/javax-money/src/main/java/com/fasterxml/jackson/datatype/money/MonetaryAmountFormatFactory.java new file mode 100644 index 0000000..39d8d63 --- /dev/null +++ b/javax-money/src/main/java/com/fasterxml/jackson/datatype/money/MonetaryAmountFormatFactory.java @@ -0,0 +1,20 @@ +package com.fasterxml.jackson.datatype.money; + +import org.apiguardian.api.API; + +import javax.annotation.Nullable; +import javax.money.format.MonetaryAmountFormat; +import java.util.Locale; + +import static org.apiguardian.api.API.Status.STABLE; + +@API(status = STABLE) +@FunctionalInterface +public interface MonetaryAmountFormatFactory { + + MonetaryAmountFormatFactory NONE = locale -> null; + + @Nullable + MonetaryAmountFormat create(final Locale defaultLocale); + +} diff --git a/javax-money/src/main/java/com/fasterxml/jackson/datatype/money/MonetaryAmountSerializer.java b/javax-money/src/main/java/com/fasterxml/jackson/datatype/money/MonetaryAmountSerializer.java new file mode 100644 index 0000000..2aadde7 --- /dev/null +++ b/javax-money/src/main/java/com/fasterxml/jackson/datatype/money/MonetaryAmountSerializer.java @@ -0,0 +1,122 @@ +package com.fasterxml.jackson.datatype.money; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.jsonFormatVisitors.JsonFormatVisitorWrapper; +import com.fasterxml.jackson.databind.jsonFormatVisitors.JsonObjectFormatVisitor; +import com.fasterxml.jackson.databind.jsontype.TypeSerializer; +import com.fasterxml.jackson.databind.ser.std.StdSerializer; +import com.fasterxml.jackson.databind.util.NameTransformer; + +import javax.annotation.Nullable; +import javax.money.CurrencyUnit; +import javax.money.MonetaryAmount; +import javax.money.format.MonetaryAmountFormat; +import java.io.IOException; +import java.util.Locale; + +final class MonetaryAmountSerializer extends StdSerializer { + + private final FieldNames names; + private final AmountWriter writer; + private final MonetaryAmountFormatFactory factory; + private final boolean isUnwrapping; + private final NameTransformer nameTransformer; + + MonetaryAmountSerializer(final FieldNames names, final AmountWriter writer, + final MonetaryAmountFormatFactory factory, boolean isUnwrapping, @Nullable final NameTransformer nameTransformer) { + super(MonetaryAmount.class); + this.writer = writer; + this.factory = factory; + this.names = names; + this.isUnwrapping = isUnwrapping; + this.nameTransformer = nameTransformer; + } + + MonetaryAmountSerializer(final FieldNames names, final AmountWriter writer, + final MonetaryAmountFormatFactory factory) { + this(names, writer, factory, false, null); + } + + @Override + public void acceptJsonFormatVisitor(final JsonFormatVisitorWrapper wrapper, final JavaType hint) + throws JsonMappingException { + + @Nullable final JsonObjectFormatVisitor visitor = wrapper.expectObjectFormat(hint); + + if (visitor == null) { + return; + } + + final SerializerProvider provider = wrapper.getProvider(); + + visitor.property(names.getAmount(), + provider.findValueSerializer(writer.getType()), + provider.constructType(writer.getType())); + + visitor.property(names.getCurrency(), + provider.findValueSerializer(CurrencyUnit.class), + provider.constructType(CurrencyUnit.class)); + + visitor.optionalProperty(names.getFormatted(), + provider.findValueSerializer(String.class), + provider.constructType(String.class)); + } + + @Override + public void serializeWithType(final MonetaryAmount value, final JsonGenerator generator, + final SerializerProvider provider, final TypeSerializer serializer) throws IOException { + + // effectively assuming no type information at all + serialize(value, generator, provider); + } + + @Override + public void serialize(final MonetaryAmount value, final JsonGenerator json, final SerializerProvider provider) + throws IOException { + + final CurrencyUnit currency = value.getCurrency(); + @Nullable final String formatted = format(value, provider); + + if (!isUnwrapping) { + json.writeStartObject(); + } + + { + provider.defaultSerializeField(transformName(names.getAmount()), writer.write(value), json); + provider.defaultSerializeField(transformName(names.getCurrency()), currency, json); + + if (formatted != null) { + provider.defaultSerializeField(transformName(names.getFormatted()), formatted, json); + } + } + + if (!isUnwrapping) { + json.writeEndObject(); + } + } + + private String transformName(String name) { + return (nameTransformer != null) ? nameTransformer.transform(name) : name; + } + + @Nullable + private String format(final MonetaryAmount value, final SerializerProvider provider) { + final Locale locale = provider.getConfig().getLocale(); + final MonetaryAmountFormat format = factory.create(locale); + return format == null ? null : format.format(value); + } + + @Override + public boolean isUnwrappingSerializer() { + return isUnwrapping; + } + + @Override + public JsonSerializer unwrappingSerializer(@Nullable final NameTransformer nameTransformer) { + return new MonetaryAmountSerializer(names, writer, factory, true, nameTransformer); + } +} diff --git a/javax-money/src/main/java/com/fasterxml/jackson/datatype/money/MoneyModule.java b/javax-money/src/main/java/com/fasterxml/jackson/datatype/money/MoneyModule.java new file mode 100644 index 0000000..328247e --- /dev/null +++ b/javax-money/src/main/java/com/fasterxml/jackson/datatype/money/MoneyModule.java @@ -0,0 +1,171 @@ +package com.fasterxml.jackson.datatype.money; + +import com.fasterxml.jackson.core.Version; +import com.fasterxml.jackson.core.util.VersionUtil; +import com.fasterxml.jackson.databind.Module; +import com.fasterxml.jackson.databind.module.SimpleDeserializers; +import com.fasterxml.jackson.databind.module.SimpleSerializers; +import org.apiguardian.api.API; +import org.javamoney.moneta.FastMoney; +import org.javamoney.moneta.Money; +import org.javamoney.moneta.RoundedMoney; + +import javax.money.CurrencyUnit; +import javax.money.MonetaryAmount; +import javax.money.MonetaryOperator; +import javax.money.MonetaryRounding; +import javax.money.format.MonetaryFormats; + +import static org.apiguardian.api.API.Status.EXPERIMENTAL; +import static org.apiguardian.api.API.Status.STABLE; + + +@API(status = STABLE) +public final class MoneyModule extends Module { + + private final AmountWriter writer; + private final FieldNames names; + private final MonetaryAmountFormatFactory formatFactory; + private final MonetaryAmountFactory amountFactory; + private final MonetaryAmountFactory fastMoneyFactory; + private final MonetaryAmountFactory moneyFactory; + private final MonetaryAmountFactory roundedMoneyFactory; + + public MoneyModule() { + this(new DecimalAmountWriter(), FieldNames.defaults(), MonetaryAmountFormatFactory.NONE, + Money::of, FastMoney::of, Money::of, RoundedMoney::of); + } + + private MoneyModule(final AmountWriter writer, + final FieldNames names, + final MonetaryAmountFormatFactory formatFactory, + final MonetaryAmountFactory amountFactory, + final MonetaryAmountFactory fastMoneyFactory, + final MonetaryAmountFactory moneyFactory, + final MonetaryAmountFactory roundedMoneyFactory) { + + this.writer = writer; + this.names = names; + this.formatFactory = formatFactory; + this.amountFactory = amountFactory; + this.fastMoneyFactory = fastMoneyFactory; + this.moneyFactory = moneyFactory; + this.roundedMoneyFactory = roundedMoneyFactory; + } + + @Override + public String getModuleName() { + return MoneyModule.class.getSimpleName(); + } + + @Override + @SuppressWarnings("deprecation") + public Version version() { + final ClassLoader loader = MoneyModule.class.getClassLoader(); + return VersionUtil.mavenVersionFor(loader, "org.zalando", "jackson-datatype-money"); + } + + @Override + public void setupModule(final SetupContext context) { + final SimpleSerializers serializers = new SimpleSerializers(); + serializers.addSerializer(CurrencyUnit.class, new CurrencyUnitSerializer()); + serializers.addSerializer(MonetaryAmount.class, new MonetaryAmountSerializer(names, writer, formatFactory)); + context.addSerializers(serializers); + + final SimpleDeserializers deserializers = new SimpleDeserializers(); + deserializers.addDeserializer(CurrencyUnit.class, new CurrencyUnitDeserializer()); + deserializers.addDeserializer(MonetaryAmount.class, new MonetaryAmountDeserializer<>(amountFactory, names)); + // for reading into concrete implementation types + deserializers.addDeserializer(Money.class, new MonetaryAmountDeserializer<>(moneyFactory, names)); + deserializers.addDeserializer(FastMoney.class, new MonetaryAmountDeserializer<>(fastMoneyFactory, names)); + deserializers.addDeserializer(RoundedMoney.class, new MonetaryAmountDeserializer<>(roundedMoneyFactory, names)); + context.addDeserializers(deserializers); + } + + public MoneyModule withDecimalNumbers() { + return withNumbers(new DecimalAmountWriter()); + } + + public MoneyModule withQuotedDecimalNumbers() { + return withNumbers(new QuotedDecimalAmountWriter()); + } + + @API(status = EXPERIMENTAL) + public MoneyModule withNumbers(final AmountWriter writer) { + return new MoneyModule(writer, names, formatFactory, amountFactory, + fastMoneyFactory, moneyFactory, roundedMoneyFactory); + } + + /** + * @see FastMoney + * @return new {@link MoneyModule} using {@link FastMoney} + */ + public MoneyModule withFastMoney() { + return withMonetaryAmount(fastMoneyFactory); + } + + /** + * @see Money + * @return new {@link MoneyModule} using {@link Money} + */ + public MoneyModule withMoney() { + return withMonetaryAmount(moneyFactory); + } + + /** + * @see RoundedMoney + * @return new {@link MoneyModule} using {@link RoundedMoney} + */ + public MoneyModule withRoundedMoney() { + return withMonetaryAmount(roundedMoneyFactory); + } + + /** + * @see RoundedMoney + * @param rounding the rounding operator + * @return new {@link MoneyModule} using {@link RoundedMoney} with the given {@link MonetaryRounding} + */ + public MoneyModule withRoundedMoney(final MonetaryOperator rounding) { + final MonetaryAmountFactory factory = (amount, currency) -> + RoundedMoney.of(amount, currency, rounding); + + return new MoneyModule(writer, names, formatFactory, factory, + fastMoneyFactory, moneyFactory, factory); + } + + public MoneyModule withMonetaryAmount(final MonetaryAmountFactory amountFactory) { + return new MoneyModule(writer, names, formatFactory, amountFactory, + fastMoneyFactory, moneyFactory, roundedMoneyFactory); + } + + public MoneyModule withoutFormatting() { + return withFormatting(MonetaryAmountFormatFactory.NONE); + } + + public MoneyModule withDefaultFormatting() { + return withFormatting(MonetaryFormats::getAmountFormat); + } + + public MoneyModule withFormatting(final MonetaryAmountFormatFactory formatFactory) { + return new MoneyModule(writer, names, formatFactory, amountFactory, + fastMoneyFactory, moneyFactory, roundedMoneyFactory); + } + + public MoneyModule withAmountFieldName(final String name) { + return withFieldNames(names.withAmount(name)); + } + + public MoneyModule withCurrencyFieldName(final String name) { + return withFieldNames(names.withCurrency(name)); + } + + public MoneyModule withFormattedFieldName(final String name) { + return withFieldNames(names.withFormatted(name)); + } + + private MoneyModule withFieldNames(final FieldNames names) { + return new MoneyModule(writer, names, formatFactory, amountFactory, + fastMoneyFactory, moneyFactory, roundedMoneyFactory); + } + +} \ No newline at end of file diff --git a/javax-money/src/main/java/com/fasterxml/jackson/datatype/money/PackageVersion.java.in b/javax-money/src/main/java/com/fasterxml/jackson/datatype/money/PackageVersion.java.in new file mode 100644 index 0000000..7860aa1 --- /dev/null +++ b/javax-money/src/main/java/com/fasterxml/jackson/datatype/money/PackageVersion.java.in @@ -0,0 +1,20 @@ +package @package@; + +import com.fasterxml.jackson.core.Version; +import com.fasterxml.jackson.core.Versioned; +import com.fasterxml.jackson.core.util.VersionUtil; + +/** + * Automatically generated from PackageVersion.java.in during + * packageVersion-generate execution of maven-replacer-plugin in + * pom.xml. + */ +public final class PackageVersion implements Versioned { + public final static Version VERSION = VersionUtil.parseVersion( + "@projectversion@", "@projectgroupid@", "@projectartifactid@"); + + @Override + public Version version() { + return VERSION; + } +} diff --git a/javax-money/src/main/java/com/fasterxml/jackson/datatype/money/QuotedDecimalAmountWriter.java b/javax-money/src/main/java/com/fasterxml/jackson/datatype/money/QuotedDecimalAmountWriter.java new file mode 100644 index 0000000..c5c8fac --- /dev/null +++ b/javax-money/src/main/java/com/fasterxml/jackson/datatype/money/QuotedDecimalAmountWriter.java @@ -0,0 +1,20 @@ +package com.fasterxml.jackson.datatype.money; + +import javax.money.MonetaryAmount; +import java.math.BigDecimal; + +final class QuotedDecimalAmountWriter implements AmountWriter { + + private final AmountWriter delegate = new DecimalAmountWriter(); + + @Override + public Class getType() { + return String.class; + } + + @Override + public String write(final MonetaryAmount amount) { + return delegate.write(amount).toPlainString(); + } + +} diff --git a/javax-money/src/main/resources/META-INF/LICENSE b/javax-money/src/main/resources/META-INF/LICENSE new file mode 100644 index 0000000..003d9c7 --- /dev/null +++ b/javax-money/src/main/resources/META-INF/LICENSE @@ -0,0 +1,23 @@ +TODO What goes here? (This original one or another?) + +The MIT License (MIT) + +Copyright (c) 2015-2016 Zalando SE + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/javax-money/src/main/resources/META-INF/NOTICE b/javax-money/src/main/resources/META-INF/NOTICE new file mode 100644 index 0000000..d55c59a --- /dev/null +++ b/javax-money/src/main/resources/META-INF/NOTICE @@ -0,0 +1,17 @@ +# Jackson JSON processor + +Jackson is a high-performance, Free/Open Source JSON processing library. +It was originally written by Tatu Saloranta (tatu.saloranta@iki.fi), and has +been in development since 2007. +It is currently developed by a community of developers. + +## Licensing + +Jackson components are licensed under Apache (Software) License, version 2.0, +as per accompanying LICENSE file. + +## Credits + +A list of contributors may be found from CREDITS file, which is included +in some artifacts (usually source distributions); but is always available +from the source code management (SCM) system project uses. diff --git a/javax-money/src/main/resources/META-INF/services/com.fasterxml.jackson.databind.Module b/javax-money/src/main/resources/META-INF/services/com.fasterxml.jackson.databind.Module new file mode 100644 index 0000000..935b160 --- /dev/null +++ b/javax-money/src/main/resources/META-INF/services/com.fasterxml.jackson.databind.Module @@ -0,0 +1 @@ +com.fasterxml.jackson.datatype.money.MoneyModule diff --git a/javax-money/src/moditect/module-info.java b/javax-money/src/moditect/module-info.java new file mode 100644 index 0000000..b8dab6f --- /dev/null +++ b/javax-money/src/moditect/module-info.java @@ -0,0 +1,13 @@ +//TODO how is this generated +// Generated 27-Mar-2019 using Moditect maven plugin +module com.fasterxml.jackson.datatype.money { + requires com.fasterxml.jackson.annotation; + requires com.fasterxml.jackson.core; + requires com.fasterxml.jackson.databind; + requires javax.money; + + exports com.fasterxml.jackson.datatype.money; + + provides com.fasterxml.jackson.databind.Module with + com.fasterxml.jackson.datatype.money.MoneyModule; +} diff --git a/javax-money/src/test/java/com/fasterxml/jackson/datatype/money/CurrencyUnitDeserializerTest.java b/javax-money/src/test/java/com/fasterxml/jackson/datatype/money/CurrencyUnitDeserializerTest.java new file mode 100644 index 0000000..670848c --- /dev/null +++ b/javax-money/src/test/java/com/fasterxml/jackson/datatype/money/CurrencyUnitDeserializerTest.java @@ -0,0 +1,43 @@ +package com.fasterxml.jackson.datatype.money; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.jsontype.BasicPolymorphicTypeValidator; +import org.javamoney.moneta.CurrencyUnitBuilder; +import org.junit.Test; + +import javax.money.CurrencyUnit; +import javax.money.UnknownCurrencyException; +import java.io.IOException; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.Assert.assertThrows; + +public final class CurrencyUnitDeserializerTest { + + private final ObjectMapper unit = new ObjectMapper().findAndRegisterModules(); + + @Test + public void shouldDeserialize() throws IOException { + final CurrencyUnit actual = unit.readValue("\"EUR\"", CurrencyUnit.class); + final CurrencyUnit expected = CurrencyUnitBuilder.of("EUR", "default").build(); + + assertThat(actual).isEqualTo(expected); + } + + @Test + public void shouldNotDeserializeInvalidCurrency() { + assertThrows(UnknownCurrencyException.class, () -> + unit.readValue("\"FOO\"", CurrencyUnit.class)); + } + + @Test + public void shouldDeserializeWithTyping() throws IOException { + unit.activateDefaultTyping(BasicPolymorphicTypeValidator.builder().build()); + + final CurrencyUnit actual = unit.readValue("\"EUR\"", CurrencyUnit.class); + final CurrencyUnit expected = CurrencyUnitBuilder.of("EUR", "default").build(); + + assertThat(actual).isEqualTo(expected); + } + +} diff --git a/javax-money/src/test/java/com/fasterxml/jackson/datatype/money/CurrencyUnitSchemaSerializerTest.java b/javax-money/src/test/java/com/fasterxml/jackson/datatype/money/CurrencyUnitSchemaSerializerTest.java new file mode 100644 index 0000000..ceaa6f6 --- /dev/null +++ b/javax-money/src/test/java/com/fasterxml/jackson/datatype/money/CurrencyUnitSchemaSerializerTest.java @@ -0,0 +1,25 @@ +package com.fasterxml.jackson.datatype.money; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.module.jsonSchema.JsonSchema; +import com.fasterxml.jackson.module.jsonSchema.JsonSchemaGenerator; +import org.junit.Test; + +import javax.money.CurrencyUnit; + +import static org.assertj.core.api.Assertions.assertThat; + +public final class CurrencyUnitSchemaSerializerTest { + + private final ObjectMapper unit = new ObjectMapper().findAndRegisterModules(); + + @Test + public void shouldSerializeJsonSchema() throws Exception { + JsonSchemaGenerator generator = new JsonSchemaGenerator(unit); + JsonSchema jsonSchema = generator.generateSchema(CurrencyUnit.class); + final String actual = unit.writeValueAsString(jsonSchema); + final String expected = "{\"type\":\"string\"}"; + + assertThat(actual).isEqualTo(expected); + } +} diff --git a/javax-money/src/test/java/com/fasterxml/jackson/datatype/money/CurrencyUnitSerializerTest.java b/javax-money/src/test/java/com/fasterxml/jackson/datatype/money/CurrencyUnitSerializerTest.java new file mode 100644 index 0000000..0ee47f5 --- /dev/null +++ b/javax-money/src/test/java/com/fasterxml/jackson/datatype/money/CurrencyUnitSerializerTest.java @@ -0,0 +1,27 @@ +package com.fasterxml.jackson.datatype.money; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.javamoney.moneta.CurrencyUnitBuilder; +import org.junit.Test; + +import javax.money.CurrencyUnit; + +import static org.assertj.core.api.Assertions.assertThat; + + +public final class CurrencyUnitSerializerTest { + + private final ObjectMapper unit = new ObjectMapper().findAndRegisterModules(); + + @Test + public void shouldSerialize() throws JsonProcessingException { + final String expected = "EUR"; + final CurrencyUnit currency = CurrencyUnitBuilder.of(expected, "default").build(); + + final String actual = unit.writeValueAsString(currency); + + assertThat(actual).isEqualTo('"' + expected + '"'); + } + +} diff --git a/javax-money/src/test/java/com/fasterxml/jackson/datatype/money/FieldNamesTest.java b/javax-money/src/test/java/com/fasterxml/jackson/datatype/money/FieldNamesTest.java new file mode 100644 index 0000000..9d1702d --- /dev/null +++ b/javax-money/src/test/java/com/fasterxml/jackson/datatype/money/FieldNamesTest.java @@ -0,0 +1,20 @@ +package com.fasterxml.jackson.datatype.money; + +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +public final class FieldNamesTest { + + @Test + public void shouldOptimizeWithMethods() { + final FieldNames expected = FieldNames.defaults(); + final FieldNames actual = expected + .withAmount(expected.getAmount()) + .withCurrency(expected.getCurrency()) + .withFormatted(expected.getFormatted()); + + assertThat(actual).isSameAs(expected); + } + +} diff --git a/javax-money/src/test/java/com/fasterxml/jackson/datatype/money/MonetaryAmountDeserializerTest.java b/javax-money/src/test/java/com/fasterxml/jackson/datatype/money/MonetaryAmountDeserializerTest.java new file mode 100644 index 0000000..55551b1 --- /dev/null +++ b/javax-money/src/test/java/com/fasterxml/jackson/datatype/money/MonetaryAmountDeserializerTest.java @@ -0,0 +1,273 @@ +package com.fasterxml.jackson.datatype.money; + +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.Module; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException; +import com.fasterxml.jackson.databind.jsontype.BasicPolymorphicTypeValidator; +import junitparams.JUnitParamsRunner; +import junitparams.Parameters; +import org.javamoney.moneta.FastMoney; +import org.javamoney.moneta.Money; +import org.javamoney.moneta.RoundedMoney; +import org.junit.Test; +import org.junit.runner.RunWith; + +import javax.money.Monetary; +import javax.money.MonetaryAmount; +import java.io.IOException; +import java.math.BigDecimal; + +import static com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES; +import static junitparams.JUnitParamsRunner.$; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.Assert.assertThrows; + +@RunWith(JUnitParamsRunner.class) +public final class MonetaryAmountDeserializerTest { + + @SuppressWarnings("unused") + private Object[] data() { + return $($(Money.class, (Configurer) module -> module), + $(FastMoney.class, (Configurer) module -> new MoneyModule().withFastMoney()), + $(Money.class, (Configurer) module -> new MoneyModule().withMoney()), + $(RoundedMoney.class, (Configurer) module -> new MoneyModule().withRoundedMoney()), + $(RoundedMoney.class, (Configurer) module -> module.withRoundedMoney(Monetary.getDefaultRounding()))); + } + + private interface Configurer { + MoneyModule configure(MoneyModule module); + } + + private ObjectMapper unit(final Configurer configurer) { + return unit(module(configurer)); + } + + private ObjectMapper unit(final Module module) { + return new ObjectMapper().registerModule(module); + } + + private MoneyModule module(final Configurer configurer) { + return configurer.configure(new MoneyModule()); + } + + @Test + public void shouldDeserializeMoneyByDefault() throws IOException { + final ObjectMapper unit = new ObjectMapper().findAndRegisterModules(); + + final String content = "{\"amount\":29.95,\"currency\":\"EUR\"}"; + final MonetaryAmount amount = unit.readValue(content, MonetaryAmount.class); + + assertThat(amount).isInstanceOf(Money.class); + } + + @Test + @Parameters(method = "data") + public void shouldDeserializeToCorrectType(final Class type, final Configurer configurer) throws IOException { + final ObjectMapper unit = unit(configurer); + + final String content = "{\"amount\":29.95,\"currency\":\"EUR\"}"; + final MonetaryAmount amount = unit.readValue(content, type); + + assertThat(amount).isInstanceOf(type); + } + + @Test + @Parameters(method = "data") + public void shouldDeserialize(final Class type, final Configurer configurer) throws IOException { + final ObjectMapper unit = unit(configurer); + + final String content = "{\"amount\":29.95,\"currency\":\"EUR\"}"; + final MonetaryAmount amount = unit.readValue(content, type); + + assertThat(amount.getNumber().numberValueExact(BigDecimal.class)).isEqualTo(new BigDecimal("29.95")); + assertThat(amount.getCurrency().getCurrencyCode()).isEqualTo("EUR"); + } + + @Test + @Parameters(method = "data") + public void shouldDeserializeWithHighNumberOfFractionDigits(final Class type, + final Configurer configurer) throws IOException { + final ObjectMapper unit = unit(configurer); + + final String content = "{\"amount\":29.9501,\"currency\":\"EUR\"}"; + final MonetaryAmount amount = unit.readValue(content, type); + + assertThat(amount.getNumber().numberValueExact(BigDecimal.class)).isEqualTo(new BigDecimal("29.9501")); + assertThat(amount.getCurrency().getCurrencyCode()).isEqualTo("EUR"); + } + + @Test + @Parameters(method = "data") + public void shouldDeserializeCorrectlyWhenAmountIsAStringValue(final Class type, + final Configurer configurer) throws IOException { + final ObjectMapper unit = unit(configurer); + + final String content = "{\"currency\":\"EUR\",\"amount\":\"29.95\"}"; + final MonetaryAmount amount = unit.readValue(content, type); + + assertThat(amount.getNumber().numberValueExact(BigDecimal.class)).isEqualTo(new BigDecimal("29.95")); + assertThat(amount.getCurrency().getCurrencyCode()).isEqualTo(("EUR")); + } + + @Test + @Parameters(method = "data") + public void shouldDeserializeCorrectlyWhenPropertiesAreInDifferentOrder(final Class type, + final Configurer configurer) throws IOException { + final ObjectMapper unit = unit(configurer); + + final String content = "{\"currency\":\"EUR\",\"amount\":29.95}"; + final MonetaryAmount amount = unit.readValue(content, type); + + assertThat(amount.getNumber().numberValueExact(BigDecimal.class)).isEqualTo((new BigDecimal("29.95"))); + assertThat(amount.getCurrency().getCurrencyCode()).isEqualTo(("EUR")); + } + + @Test + @Parameters(method = "data") + public void shouldDeserializeWithCustomNames(final Class type, final Configurer configurer) throws IOException { + final ObjectMapper unit = unit(module(configurer) + .withAmountFieldName("value") + .withCurrencyFieldName("unit")); + + final String content = "{\"value\":29.95,\"unit\":\"EUR\"}"; + final MonetaryAmount amount = unit.readValue(content, type); + + assertThat(amount.getNumber().numberValueExact(BigDecimal.class)).isEqualTo((new BigDecimal("29.95"))); + assertThat(amount.getCurrency().getCurrencyCode()).isEqualTo(("EUR")); + } + + @Test + @Parameters(method = "data") + public void shouldIgnoreFormattedValue(final Class type, final Configurer configurer) throws IOException { + final ObjectMapper unit = unit(configurer); + + final String content = "{\"amount\":29.95,\"currency\":\"EUR\",\"formatted\":\"30.00 EUR\"}"; + final MonetaryAmount amount = unit.readValue(content, type); + + assertThat(amount.getNumber().numberValueExact(BigDecimal.class)).isEqualTo((new BigDecimal("29.95"))); + assertThat(amount.getCurrency().getCurrencyCode()).isEqualTo(("EUR")); + } + + @Test + @Parameters(method = "data") + public void shouldUpdateExistingValueUsingTreeTraversingParser(final Class type, + final Configurer configurer) throws IOException { + final ObjectMapper unit = unit(configurer); + + final String content = "{\"amount\":29.95,\"currency\":\"EUR\"}"; + final MonetaryAmount amount = unit.readValue(content, type); + + assertThat(amount).isNotNull(); + + // we need a json node to get a TreeTraversingParser with codec of type ObjectReader + final JsonNode ownerNode = + unit.readTree("{\"value\":{\"amount\":29.95,\"currency\":\"EUR\",\"formatted\":\"30.00EUR\"}}"); + + final Owner owner = new Owner(); + owner.setValue(amount); + + // try to update + final Owner result = unit.readerForUpdating(owner).readValue(ownerNode); + assertThat(result).isNotNull(); + assertThat(result.getValue()).isEqualTo((amount)); + } + + private static class Owner { + + private MonetaryAmount value; + + MonetaryAmount getValue() { + return value; + } + + void setValue(final MonetaryAmount value) { + this.value = value; + } + + } + + @Test + @Parameters(method = "data") + public void shouldFailToDeserializeWithoutAmount(final Class type, final Configurer configurer) { + final ObjectMapper unit = unit(configurer); + + final String content = "{\"currency\":\"EUR\"}"; + + final JsonProcessingException exception = assertThrows( + JsonProcessingException.class, () -> unit.readValue(content, type)); + + assertThat(exception.getMessage()).contains("Missing property: 'amount'"); + } + + @Test + @Parameters(method = "data") + public void shouldFailToDeserializeWithoutCurrency(final Class type, final Configurer configurer) { + final ObjectMapper unit = unit(configurer); + + final String content = "{\"amount\":29.95}"; + + final JsonProcessingException exception = assertThrows( + JsonProcessingException.class, () -> unit.readValue(content, type)); + + assertThat(exception.getMessage()).contains("Missing property: 'currency'"); + } + + @Test + @Parameters(method = "data") + public void shouldFailToDeserializeWithAdditionalProperties(final Class type, + final Configurer configurer) { + final ObjectMapper unit = unit(configurer); + + final String content = "{\"amount\":29.95,\"currency\":\"EUR\",\"version\":\"1\"}"; + + final JsonProcessingException exception = assertThrows( + UnrecognizedPropertyException.class, () -> unit.readValue(content, type)); + + assertThat(exception.getMessage()).startsWith( + "Unrecognized field \"version\" (class javax.money.MonetaryAmount), " + + "not marked as ignorable (3 known properties: \"amount\", \"currency\", \"formatted\"])"); + } + + @Test + @Parameters(method = "data") + public void shouldNotFailToDeserializeWithAdditionalProperties(final Class type, + final Configurer configurer) throws IOException { + final ObjectMapper unit = unit(configurer).disable(FAIL_ON_UNKNOWN_PROPERTIES); + + final String content = "{\"source\":{\"provider\":\"ECB\",\"date\":\"2016-09-29\"},\"amount\":29.95,\"currency\":\"EUR\",\"version\":\"1\"}"; + unit.readValue(content, type); + } + + @Test + @Parameters(method = "data") + public void shouldDeserializeWithTypeInformation(final Class type, final Configurer configurer) throws IOException { + final ObjectMapper unit = unit(configurer) + .activateDefaultTyping( + BasicPolymorphicTypeValidator.builder().build(), + ObjectMapper.DefaultTyping.OBJECT_AND_NON_CONCRETE, + JsonTypeInfo.As.EXISTING_PROPERTY) + .disable(FAIL_ON_UNKNOWN_PROPERTIES); + + final String content = "{\"type\":\"org.javamoney.moneta.Money\",\"amount\":29.95,\"currency\":\"EUR\"}"; + final M amount = unit.readValue(content, type); + + // type information is ignored?! + assertThat(amount).isInstanceOf(type); + } + + @Test + @Parameters(method = "data") + public void shouldDeserializeWithoutTypeInformation(final Class type, final Configurer configurer) throws IOException { + final ObjectMapper unit = unit(configurer).activateDefaultTyping( + BasicPolymorphicTypeValidator.builder().build()); + + final String content = "{\"amount\":29.95,\"currency\":\"EUR\"}"; + final M amount = unit.readValue(content, type); + + assertThat(amount).isInstanceOf(type); + } + +} diff --git a/javax-money/src/test/java/com/fasterxml/jackson/datatype/money/MonetaryAmountSchemaSerializerTest.java b/javax-money/src/test/java/com/fasterxml/jackson/datatype/money/MonetaryAmountSchemaSerializerTest.java new file mode 100644 index 0000000..c827bd6 --- /dev/null +++ b/javax-money/src/test/java/com/fasterxml/jackson/datatype/money/MonetaryAmountSchemaSerializerTest.java @@ -0,0 +1,87 @@ +package com.fasterxml.jackson.datatype.money; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.Module; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.module.jsonSchema.JsonSchema; +import com.fasterxml.jackson.module.jsonSchema.JsonSchemaGenerator; +import org.junit.Test; + +import javax.money.MonetaryAmount; + +import static org.assertj.core.api.Assertions.assertThat; + +public final class MonetaryAmountSchemaSerializerTest { + + @Test + public void shouldSerializeJsonSchema() throws Exception { + final ObjectMapper unit = unit(module()); + final JsonSchemaGenerator generator = new JsonSchemaGenerator(unit); + final JsonSchema jsonSchema = generator.generateSchema(MonetaryAmount.class); + final String actual = unit.writeValueAsString(jsonSchema); + final String expected = "{\"type\":\"object\",\"id\":\"urn:jsonschema:javax:money:MonetaryAmount\",\"properties\":" + + "{\"amount\":{\"type\":\"number\",\"required\":true}," + + "\"currency\":{\"type\":\"string\",\"required\":true}," + + "\"formatted\":{\"type\":\"string\"}}}"; + + assertThat(actual).isEqualTo(expected); + } + + @Test + public void shouldSerializeJsonSchemaWithCustomFieldNames() throws Exception { + final ObjectMapper unit = unit(module().withAmountFieldName("value") + .withCurrencyFieldName("unit") + .withFormattedFieldName("pretty")); + final JsonSchemaGenerator generator = new JsonSchemaGenerator(unit); + final JsonSchema jsonSchema = generator.generateSchema(MonetaryAmount.class); + final String actual = unit.writeValueAsString(jsonSchema); + final String expected = "{\"type\":\"object\",\"id\":\"urn:jsonschema:javax:money:MonetaryAmount\",\"properties\":" + + "{\"value\":{\"type\":\"number\",\"required\":true}," + + "\"unit\":{\"type\":\"string\",\"required\":true}," + + "\"pretty\":{\"type\":\"string\"}}}"; + + assertThat(actual).isEqualTo(expected); + } + + @Test + public void shouldSerializeJsonSchemaWithQuotedDecimalNumbers() throws Exception { + final ObjectMapper unit = unit(module().withQuotedDecimalNumbers()); + final JsonSchemaGenerator generator = new JsonSchemaGenerator(unit); + final JsonSchema jsonSchema = generator.generateSchema(MonetaryAmount.class); + final String actual = unit.writeValueAsString(jsonSchema); + final String expected = "{\"type\":\"object\",\"id\":\"urn:jsonschema:javax:money:MonetaryAmount\",\"properties\":" + + "{\"amount\":{\"type\":\"string\",\"required\":true}," + + "\"currency\":{\"type\":\"string\",\"required\":true}," + + "\"formatted\":{\"type\":\"string\"}}}"; + + assertThat(actual).isEqualTo(expected); + } + + @Test + public void shouldSerializeJsonSchemaWithMultipleMonetayAmountsAndAlternativeGenerator() throws Exception { + final ObjectMapper unit = unit(module()); + final com.kjetland.jackson.jsonSchema.JsonSchemaGenerator generator = + new com.kjetland.jackson.jsonSchema.JsonSchemaGenerator(unit); + + final JsonNode jsonSchema = generator.generateJsonSchema(SchemaTestClass.class); + + final String actual = unit.writeValueAsString(jsonSchema); + final String expected = "{\"$schema\":\"http://json-schema.org/draft-04/schema#\",\"title\":\"Schema Test Class\"," + + "\"type\":\"object\",\"additionalProperties\":false,\"properties\":{\"moneyOne\":{\"$ref\":" + + "\"#/definitions/MonetaryAmount\"},\"moneyTwo\":{\"$ref\":\"#/definitions/MonetaryAmount\"}}," + + "\"definitions\":{\"MonetaryAmount\":{\"type\":\"object\",\"additionalProperties\":false,\"properties\"" + + ":{\"amount\":{\"type\":\"number\"},\"currency\":{\"type\":\"string\"},\"formatted\":" + + "{\"type\":\"string\"}},\"required\":[\"amount\",\"currency\"]}}}"; + + assertThat(actual).isEqualTo(expected); + } + + private ObjectMapper unit(final Module module) { + return new ObjectMapper().registerModule(module); + } + + private MoneyModule module() { + return new MoneyModule(); + } + +} diff --git a/javax-money/src/test/java/com/fasterxml/jackson/datatype/money/MonetaryAmountSerializerTest.java b/javax-money/src/test/java/com/fasterxml/jackson/datatype/money/MonetaryAmountSerializerTest.java new file mode 100644 index 0000000..f95bca1 --- /dev/null +++ b/javax-money/src/test/java/com/fasterxml/jackson/datatype/money/MonetaryAmountSerializerTest.java @@ -0,0 +1,377 @@ +package com.fasterxml.jackson.datatype.money; + +import com.fasterxml.jackson.annotation.JsonUnwrapped; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.json.JsonWriteFeature; +import com.fasterxml.jackson.databind.Module; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.ObjectWriter; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.databind.json.JsonMapper; +import com.fasterxml.jackson.databind.jsonFormatVisitors.JsonFormatVisitorWrapper; +import com.fasterxml.jackson.databind.jsontype.BasicPolymorphicTypeValidator; +import com.fasterxml.jackson.databind.type.SimpleType; +import junitparams.JUnitParamsRunner; +import junitparams.Parameters; +import lombok.Value; +import org.javamoney.moneta.FastMoney; +import org.javamoney.moneta.Money; +import org.javamoney.moneta.RoundedMoney; +import org.junit.Test; +import org.junit.runner.RunWith; + +import javax.money.MonetaryAmount; +import java.io.IOException; +import java.math.BigDecimal; +import java.util.Arrays; +import java.util.Locale; + +import static javax.money.Monetary.getDefaultRounding; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; + +@RunWith(JUnitParamsRunner.class) +public final class MonetaryAmountSerializerTest { + + static Iterable amounts() { + return Arrays.asList( + FastMoney.of(29.95, "EUR"), + Money.of(29.95, "EUR"), + RoundedMoney.of(29.95, "EUR", getDefaultRounding())); + } + + static Iterable hundreds() { + return Arrays.asList( + FastMoney.of(100, "EUR"), + Money.of(100, "EUR"), + RoundedMoney.of(100, "EUR", getDefaultRounding())); + } + + static Iterable fractions() { + return Arrays.asList( + FastMoney.of(0.0001, "EUR"), + Money.of(0.0001, "EUR"), + RoundedMoney.of(0.0001, "EUR", getDefaultRounding())); + } + + private ObjectMapper unit() { + return unit(module()); + } + + private ObjectMapper unit(final Module module) { + return build(module).build(); + } + + private JsonMapper.Builder build() { + return build(module()); + } + + private JsonMapper.Builder build(final Module module) { + return JsonMapper.builder() + .addModule(module); + } + + private MoneyModule module() { + return new MoneyModule(); + } + + @Test + @Parameters(method = "amounts") + public void shouldSerialize(final MonetaryAmount amount) throws JsonProcessingException { + final ObjectMapper unit = unit(); + + final String expected = "{\"amount\":29.95,\"currency\":\"EUR\"}"; + final String actual = unit.writeValueAsString(amount); + + assertThat(actual).isEqualTo(expected); + } + + @Test + @Parameters(method = "amounts") + public void shouldSerializeWithoutFormattedValueIfFactoryProducesNull( + final MonetaryAmount amount) throws JsonProcessingException { + final ObjectMapper unit = unit(module().withoutFormatting()); + + final String expected = "{\"amount\":29.95,\"currency\":\"EUR\"}"; + final String actual = unit.writeValueAsString(amount); + + assertThat(actual).isEqualTo(expected); + } + + @Test + @Parameters(method = "amounts") + public void shouldSerializeWithFormattedGermanValue(final MonetaryAmount amount) throws JsonProcessingException { + final ObjectMapper unit = unit(new MoneyModule().withDefaultFormatting()); + + final String expected = "{\"amount\":29.95,\"currency\":\"EUR\",\"formatted\":\"29,95 EUR\"}"; + + final ObjectWriter writer = unit.writer().with(Locale.GERMANY); + final String actual = writer.writeValueAsString(amount); + + assertThat(actual).isEqualTo(expected); + } + + @Test + @Parameters(method = "amounts") + public void shouldSerializeWithFormattedAmericanValue(final MonetaryAmount amount) throws JsonProcessingException { + final ObjectMapper unit = unit(module().withDefaultFormatting()); + + final String expected = "{\"amount\":29.95,\"currency\":\"USD\",\"formatted\":\"USD29.95\"}"; + + final ObjectWriter writer = unit.writer().with(Locale.US); + final String actual = writer.writeValueAsString(amount.getFactory().setCurrency("USD").create()); + + assertThat(actual).isEqualTo(expected); + } + + @Test + @Parameters(method = "amounts") + public void shouldSerializeWithCustomName(final MonetaryAmount amount) throws IOException { + final ObjectMapper unit = unit(module().withDefaultFormatting() + .withAmountFieldName("value") + .withCurrencyFieldName("unit") + .withFormattedFieldName("pretty")); + + final String expected = "{\"value\":29.95,\"unit\":\"EUR\",\"pretty\":\"29,95 EUR\"}"; + + final ObjectWriter writer = unit.writer().with(Locale.GERMANY); + final String actual = writer.writeValueAsString(amount); + + assertThat(actual).isEqualTo(expected); + } + + @Test + @Parameters(method = "amounts") + public void shouldSerializeAmountAsDecimal(final MonetaryAmount amount) throws JsonProcessingException { + final ObjectMapper unit = unit(module().withDecimalNumbers()); + + final String expected = "{\"amount\":29.95,\"currency\":\"EUR\"}"; + final String actual = unit.writeValueAsString(amount); + + assertThat(actual).isEqualTo(expected); + } + + @Test + @Parameters(method = "hundreds") + public void shouldSerializeAmountAsDecimalWithDefaultFractionDigits( + final MonetaryAmount hundred) throws JsonProcessingException { + final ObjectMapper unit = unit(module().withDecimalNumbers()); + + final String expected = "{\"amount\":100.00,\"currency\":\"EUR\"}"; + final String actual = unit.writeValueAsString(hundred); + + assertThat(actual).isEqualTo(expected); + } + + @Test + @Parameters(method = "fractions") + public void shouldSerializeAmountAsDecimalWithHigherNumberOfFractionDigits( + final MonetaryAmount fraction) throws JsonProcessingException { + final ObjectMapper unit = unit(module().withDecimalNumbers()); + + final String expected = "{\"amount\":0.0001,\"currency\":\"EUR\"}"; + final String actual = unit.writeValueAsString(fraction); + + assertThat(actual).isEqualTo(expected); + } + + @Test + @Parameters(method = "hundreds") + public void shouldSerializeAmountAsDecimalWithLowerNumberOfFractionDigits( + final MonetaryAmount hundred) throws JsonProcessingException { + final ObjectMapper unit = unit(module().withNumbers(new AmountWriter() { + @Override + public Class getType() { + return BigDecimal.class; + } + + @Override + public BigDecimal write(final MonetaryAmount amount) { + return amount.getNumber().numberValueExact(BigDecimal.class).stripTrailingZeros(); + } + })); + + final String expected = "{\"amount\":1E+2,\"currency\":\"EUR\"}"; + final String actual = unit.writeValueAsString(hundred); + + assertThat(actual).isEqualTo(expected); + } + + @Test + @Parameters(method = "amounts") + public void shouldSerializeAmountAsQuotedDecimal(final MonetaryAmount amount) throws JsonProcessingException { + final ObjectMapper unit = unit(module().withQuotedDecimalNumbers()); + + final String expected = "{\"amount\":\"29.95\",\"currency\":\"EUR\"}"; + final String actual = unit.writeValueAsString(amount); + + assertThat(actual).isEqualTo(expected); + } + + @Test + @Parameters(method = "hundreds") + public void shouldSerializeAmountAsQuotedDecimalWithDefaultFractionDigits( + final MonetaryAmount hundred) throws JsonProcessingException { + final ObjectMapper unit = unit(module().withQuotedDecimalNumbers()); + + final String expected = "{\"amount\":\"100.00\",\"currency\":\"EUR\"}"; + final String actual = unit.writeValueAsString(hundred); + + assertThat(actual).isEqualTo(expected); + } + + @Test + @Parameters(method = "fractions") + public void shouldSerializeAmountAsQuotedDecimalWithHigherNumberOfFractionDigits( + final MonetaryAmount fraction) throws JsonProcessingException { + final ObjectMapper unit = unit(module().withQuotedDecimalNumbers()); + + final String expected = "{\"amount\":\"0.0001\",\"currency\":\"EUR\"}"; + final String actual = unit.writeValueAsString(fraction); + + assertThat(actual).isEqualTo(expected); + } + + @Test + @Parameters(method = "hundreds") + public void shouldSerializeAmountAsQuotedDecimalWithLowerNumberOfFractionDigits( + final MonetaryAmount hundred) throws JsonProcessingException { + final ObjectMapper unit = unit(module().withNumbers(new AmountWriter() { + @Override + public Class getType() { + return String.class; + } + + @Override + public String write(final MonetaryAmount amount) { + return amount.getNumber().numberValueExact(BigDecimal.class).stripTrailingZeros().toPlainString(); + } + })); + + final String expected = "{\"amount\":\"100\",\"currency\":\"EUR\"}"; + final String actual = unit.writeValueAsString(hundred); + + assertThat(actual).isEqualTo(expected); + } + + @Test + @Parameters(method = "hundreds") + public void shouldSerializeAmountAsQuotedDecimalPlainString(final MonetaryAmount hundred) throws JsonProcessingException { + final ObjectMapper unit = unit(module().withQuotedDecimalNumbers()); + unit.enable(JsonGenerator.Feature.WRITE_BIGDECIMAL_AS_PLAIN); + + final String expected = "{\"amount\":\"100.00\",\"currency\":\"EUR\"}"; + final String actual = unit.writeValueAsString(hundred); + + assertThat(actual).isEqualTo(expected); + } + + @Test + @Parameters(method = "amounts") + public void shouldWriteNumbersAsStrings(final MonetaryAmount amount) throws JsonProcessingException { + final ObjectMapper unit = build() + .enable(JsonWriteFeature.WRITE_NUMBERS_AS_STRINGS) + .build(); + + final String expected = "{\"amount\":\"29.95\",\"currency\":\"EUR\"}"; + final String actual = unit.writeValueAsString(amount); + + assertThat(actual).isEqualTo(expected); + } + + @Test + @Parameters(method = "hundreds") + public void shouldWriteNumbersAsPlainStrings(final MonetaryAmount hundred) throws JsonProcessingException { + final ObjectMapper unit = build() + .enable(JsonWriteFeature.WRITE_NUMBERS_AS_STRINGS) + .enable(JsonGenerator.Feature.WRITE_BIGDECIMAL_AS_PLAIN) + .build(); + + final String expected = "{\"amount\":\"100.00\",\"currency\":\"EUR\"}"; + final String actual = unit.writeValueAsString(hundred); + + assertThat(actual).isEqualTo(expected); + } + + @Value + private static class Price { + MonetaryAmount amount; + } + + @Test + @Parameters(method = "amounts") + public void shouldSerializeWithType(final MonetaryAmount amount) throws JsonProcessingException { + final ObjectMapper unit = unit(module()).activateDefaultTyping(BasicPolymorphicTypeValidator.builder().build()); + + final String expected = "{\"amount\":{\"amount\":29.95,\"currency\":\"EUR\"}}"; + final String actual = unit.writeValueAsString(new Price(amount)); + + assertThat(actual).isEqualTo(expected); + } + + @Value + private static class PriceUnwrapped { + @JsonUnwrapped + MonetaryAmount amount; + } + + @Test + @Parameters(method = "amounts") + public void shouldSerializeWithTypeUnwrapped(final MonetaryAmount amount) throws JsonProcessingException { + final ObjectMapper unit = unit(module()).activateDefaultTyping(BasicPolymorphicTypeValidator.builder().build()); + + final String expected = "{\"amount\":29.95,\"currency\":\"EUR\"}"; + final String actual = unit.writeValueAsString(new PriceUnwrapped(amount)); + + assertThat(actual).isEqualTo(expected); + } + + @Value + private static class PriceUnwrappedTransformedNames { + @JsonUnwrapped(prefix = "Price-", suffix = "-Field") + MonetaryAmount amount; + } + + @Test + @Parameters(method = "amounts") + public void shouldSerializeWithTypeUnwrappedAndNamesTransformed(final MonetaryAmount amount) throws JsonProcessingException { + final ObjectMapper unit = unit(module()).activateDefaultTyping(BasicPolymorphicTypeValidator.builder().build()); + + final String expected = "{\"Price-amount-Field\":29.95,\"Price-currency-Field\":\"EUR\"}"; + final String actual = unit.writeValueAsString(new PriceUnwrappedTransformedNames(amount)); + + assertThat(actual).isEqualTo(expected); + } + + @Test + public void shouldHandleNullValueFromExpectObjectFormatInSchemaVisitor() throws Exception { + final MonetaryAmountSerializer unit = new MonetaryAmountSerializer(FieldNames.defaults(), + new DecimalAmountWriter(), MonetaryAmountFormatFactory.NONE); + + final JsonFormatVisitorWrapper wrapper = mock(JsonFormatVisitorWrapper.class); + unit.acceptJsonFormatVisitor(wrapper, SimpleType.constructUnsafe(MonetaryAmount.class)); + } + + /** + * Fixes a bug that caused the amount field to be written as + * + * "amount": {"BigDecimal":12.34} + * + * + * @param amount + * @throws JsonProcessingException + */ + @Test + @Parameters(method = "amounts") + public void shouldSerializeWithWrapRootValue(final MonetaryAmount amount) throws JsonProcessingException { + final ObjectMapper unit = unit(module()) + .configure(SerializationFeature.WRAP_ROOT_VALUE, true); + + final String expected = "{\"Price\":{\"amount\":{\"amount\":29.95,\"currency\":\"EUR\"}}}"; + final String actual = unit.writeValueAsString(new Price(amount)); + + assertThat(actual).isEqualTo(expected); + } + +} diff --git a/javax-money/src/test/java/com/fasterxml/jackson/datatype/money/SchemaTestClass.java b/javax-money/src/test/java/com/fasterxml/jackson/datatype/money/SchemaTestClass.java new file mode 100644 index 0000000..21448e4 --- /dev/null +++ b/javax-money/src/test/java/com/fasterxml/jackson/datatype/money/SchemaTestClass.java @@ -0,0 +1,15 @@ +package com.fasterxml.jackson.datatype.money; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +import javax.money.MonetaryAmount; + +@AllArgsConstructor +@Getter +public class SchemaTestClass { + + private final MonetaryAmount moneyOne; + private final MonetaryAmount moneyTwo; + +} diff --git a/pom.xml b/pom.xml index 3df30cf..a26acb1 100644 --- a/pom.xml +++ b/pom.xml @@ -27,6 +27,8 @@ jakarta-jsonp jakarta-mail + + javax-money https://github.com/FasterXML/jackson-datatypes-misc