diff --git a/core/jvm/src/test/scala/kantan/codecs/strings/DateCodecTests.scala b/core/jvm/src/test/scala/kantan/codecs/strings/DateCodecTests.scala index 598e53a4..8aca6940 100644 --- a/core/jvm/src/test/scala/kantan/codecs/strings/DateCodecTests.scala +++ b/core/jvm/src/test/scala/kantan/codecs/strings/DateCodecTests.scala @@ -18,13 +18,21 @@ package kantan.codecs.strings import java.text.SimpleDateFormat import java.util.{Date, Locale} +import kantan.codecs.laws.{HasIllegalValues, HasLegalValues} import kantan.codecs.laws.discipline.{DisciplineSuite, StringCodecTests} import kantan.codecs.laws.discipline.arbitrary._ class DateCodecTests extends DisciplineSuite { + val format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSz", Locale.ENGLISH) implicit val codec: StringCodec[Date] = - StringCodec.dateCodec(new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSz", Locale.ENGLISH)) + StringCodec.dateCodec(format) + + implicit val hasLegalValues: HasLegalValues[String, Date, codecs.type] = + HasLegalValues.from(date => format.synchronized(format.format(date))) + + implicit val hasIllegalValues: HasIllegalValues[String, Date, codecs.type] = + HasIllegalValues.fromUnsafeString(s => format.synchronized(format.parse(s))) checkAll("StringDecoder[Date]", StringCodecTests[Date].decoder[Int, Int]) checkAll("StringEncoder[Date]", StringCodecTests[Date].encoder[Int, Int]) diff --git a/core/shared/src/test/scala/kantan/codecs/strings/BigDecimalCodecTests.scala b/core/shared/src/test/scala/kantan/codecs/strings/BigDecimalCodecTests.scala index f7fa994b..6513ecbd 100644 --- a/core/shared/src/test/scala/kantan/codecs/strings/BigDecimalCodecTests.scala +++ b/core/shared/src/test/scala/kantan/codecs/strings/BigDecimalCodecTests.scala @@ -17,7 +17,6 @@ package kantan.codecs.strings import kantan.codecs.laws.discipline.{DisciplineSuite, StringCodecTests} -import kantan.codecs.laws.discipline.arbitrary._ class BigDecimalCodecTests extends DisciplineSuite { diff --git a/core/shared/src/test/scala/kantan/codecs/strings/BigIntCodecTests.scala b/core/shared/src/test/scala/kantan/codecs/strings/BigIntCodecTests.scala index 4531f403..9cb47bfd 100644 --- a/core/shared/src/test/scala/kantan/codecs/strings/BigIntCodecTests.scala +++ b/core/shared/src/test/scala/kantan/codecs/strings/BigIntCodecTests.scala @@ -17,7 +17,6 @@ package kantan.codecs.strings import kantan.codecs.laws.discipline.{DisciplineSuite, StringCodecTests} -import kantan.codecs.laws.discipline.arbitrary._ class BigIntCodecTests extends DisciplineSuite { diff --git a/core/shared/src/test/scala/kantan/codecs/strings/BooleanCodecTests.scala b/core/shared/src/test/scala/kantan/codecs/strings/BooleanCodecTests.scala index 69a04a02..cf505e0e 100644 --- a/core/shared/src/test/scala/kantan/codecs/strings/BooleanCodecTests.scala +++ b/core/shared/src/test/scala/kantan/codecs/strings/BooleanCodecTests.scala @@ -17,7 +17,6 @@ package kantan.codecs.strings import kantan.codecs.laws.discipline.{DisciplineSuite, StringCodecTests} -import kantan.codecs.laws.discipline.arbitrary._ class BooleanCodecTests extends DisciplineSuite { diff --git a/core/shared/src/test/scala/kantan/codecs/strings/ByteCodecTests.scala b/core/shared/src/test/scala/kantan/codecs/strings/ByteCodecTests.scala index a59c6869..e0b261a6 100644 --- a/core/shared/src/test/scala/kantan/codecs/strings/ByteCodecTests.scala +++ b/core/shared/src/test/scala/kantan/codecs/strings/ByteCodecTests.scala @@ -17,7 +17,6 @@ package kantan.codecs.strings import kantan.codecs.laws.discipline.{DisciplineSuite, StringCodecTests} -import kantan.codecs.laws.discipline.arbitrary._ class ByteCodecTests extends DisciplineSuite { diff --git a/core/shared/src/test/scala/kantan/codecs/strings/CharCodecTests.scala b/core/shared/src/test/scala/kantan/codecs/strings/CharCodecTests.scala index d886ca86..57f24c12 100644 --- a/core/shared/src/test/scala/kantan/codecs/strings/CharCodecTests.scala +++ b/core/shared/src/test/scala/kantan/codecs/strings/CharCodecTests.scala @@ -17,7 +17,6 @@ package kantan.codecs.strings import kantan.codecs.laws.discipline.{DisciplineSuite, StringCodecTests} -import kantan.codecs.laws.discipline.arbitrary._ class CharCodecTests extends DisciplineSuite { diff --git a/core/shared/src/test/scala/kantan/codecs/strings/DoubleCodecTests.scala b/core/shared/src/test/scala/kantan/codecs/strings/DoubleCodecTests.scala index 27d9e40b..ea944334 100644 --- a/core/shared/src/test/scala/kantan/codecs/strings/DoubleCodecTests.scala +++ b/core/shared/src/test/scala/kantan/codecs/strings/DoubleCodecTests.scala @@ -17,7 +17,6 @@ package kantan.codecs.strings import kantan.codecs.laws.discipline.{DisciplineSuite, StringCodecTests} -import kantan.codecs.laws.discipline.arbitrary._ class DoubleCodecTests extends DisciplineSuite { diff --git a/core/shared/src/test/scala/kantan/codecs/strings/EitherCodecTests.scala b/core/shared/src/test/scala/kantan/codecs/strings/EitherCodecTests.scala index 0109f578..f9a5012e 100644 --- a/core/shared/src/test/scala/kantan/codecs/strings/EitherCodecTests.scala +++ b/core/shared/src/test/scala/kantan/codecs/strings/EitherCodecTests.scala @@ -17,7 +17,6 @@ package kantan.codecs.strings import kantan.codecs.laws.discipline.{DisciplineSuite, StringDecoderTests, StringEncoderTests} -import kantan.codecs.laws.discipline.arbitrary._ import kantan.codecs.strings.tagged._ class EitherCodecTests extends DisciplineSuite { diff --git a/core/shared/src/test/scala/kantan/codecs/strings/FloatCodecTests.scala b/core/shared/src/test/scala/kantan/codecs/strings/FloatCodecTests.scala index db8c8555..aedbc167 100644 --- a/core/shared/src/test/scala/kantan/codecs/strings/FloatCodecTests.scala +++ b/core/shared/src/test/scala/kantan/codecs/strings/FloatCodecTests.scala @@ -17,7 +17,6 @@ package kantan.codecs.strings import kantan.codecs.laws.discipline.{DisciplineSuite, StringCodecTests} -import kantan.codecs.laws.discipline.arbitrary._ class FloatCodecTests extends DisciplineSuite { diff --git a/core/shared/src/test/scala/kantan/codecs/strings/IntCodecTests.scala b/core/shared/src/test/scala/kantan/codecs/strings/IntCodecTests.scala index d42f5429..84daeddf 100644 --- a/core/shared/src/test/scala/kantan/codecs/strings/IntCodecTests.scala +++ b/core/shared/src/test/scala/kantan/codecs/strings/IntCodecTests.scala @@ -17,7 +17,6 @@ package kantan.codecs.strings import kantan.codecs.laws.discipline.{DisciplineSuite, StringCodecTests} -import kantan.codecs.laws.discipline.arbitrary._ class IntCodecTests extends DisciplineSuite { diff --git a/core/shared/src/test/scala/kantan/codecs/strings/LongCodecTests.scala b/core/shared/src/test/scala/kantan/codecs/strings/LongCodecTests.scala index 6686f6e4..7054e429 100644 --- a/core/shared/src/test/scala/kantan/codecs/strings/LongCodecTests.scala +++ b/core/shared/src/test/scala/kantan/codecs/strings/LongCodecTests.scala @@ -17,7 +17,6 @@ package kantan.codecs.strings import kantan.codecs.laws.discipline.{DisciplineSuite, StringCodecTests} -import kantan.codecs.laws.discipline.arbitrary._ class LongCodecTests extends DisciplineSuite { diff --git a/core/shared/src/test/scala/kantan/codecs/strings/OptionCodecTests.scala b/core/shared/src/test/scala/kantan/codecs/strings/OptionCodecTests.scala index de9a6ae6..45e77942 100644 --- a/core/shared/src/test/scala/kantan/codecs/strings/OptionCodecTests.scala +++ b/core/shared/src/test/scala/kantan/codecs/strings/OptionCodecTests.scala @@ -17,7 +17,6 @@ package kantan.codecs.strings import kantan.codecs.laws.discipline.{DisciplineSuite, StringDecoderTests, StringEncoderTests} -import kantan.codecs.laws.discipline.arbitrary._ import kantan.codecs.strings.tagged._ class OptionCodecTests extends DisciplineSuite { diff --git a/core/shared/src/test/scala/kantan/codecs/strings/ShortCodecTests.scala b/core/shared/src/test/scala/kantan/codecs/strings/ShortCodecTests.scala index b10eb373..8521fa00 100644 --- a/core/shared/src/test/scala/kantan/codecs/strings/ShortCodecTests.scala +++ b/core/shared/src/test/scala/kantan/codecs/strings/ShortCodecTests.scala @@ -17,7 +17,6 @@ package kantan.codecs.strings import kantan.codecs.laws.discipline.{DisciplineSuite, StringCodecTests} -import kantan.codecs.laws.discipline.arbitrary._ class ShortCodecTests extends DisciplineSuite { diff --git a/core/shared/src/test/scala/kantan/codecs/strings/StringsCodecTests.scala b/core/shared/src/test/scala/kantan/codecs/strings/StringsCodecTests.scala index f7b10b1e..78869d25 100644 --- a/core/shared/src/test/scala/kantan/codecs/strings/StringsCodecTests.scala +++ b/core/shared/src/test/scala/kantan/codecs/strings/StringsCodecTests.scala @@ -17,7 +17,6 @@ package kantan.codecs.strings import kantan.codecs.laws.discipline.{DisciplineSuite, StringCodecTests} -import kantan.codecs.laws.discipline.arbitrary._ class StringsCodecTests extends DisciplineSuite { diff --git a/core/shared/src/test/scala/kantan/codecs/strings/tagged.scala b/core/shared/src/test/scala/kantan/codecs/strings/tagged.scala index 21817f99..fdf17aad 100644 --- a/core/shared/src/test/scala/kantan/codecs/strings/tagged.scala +++ b/core/shared/src/test/scala/kantan/codecs/strings/tagged.scala @@ -19,6 +19,7 @@ package kantan.codecs.strings import imp.imp import kantan.codecs.{Codec, Decoder, Encoder} import kantan.codecs.laws.{DecoderLaws, EncoderLaws} +import kantan.codecs.laws.{HasIllegalStringValues, HasIllegalValues, HasLegalStringValues, HasLegalValues} import kantan.codecs.laws.CodecValue.LegalValue import kantan.codecs.laws.discipline.{DecoderTests => RootDecoderTests, EncoderTests => RootEncoderTests} import kantan.codecs.laws.discipline.arbitrary._ @@ -32,13 +33,15 @@ object tagged { // - Type aliases for readability ------------------------------------------------------------------------------------ // ------------------------------------------------------------------------------------------------------------------- - type DecoderTests[D] = RootDecoderTests[String, D, DecodeError, tagged.type] - type EncoderTests[D] = RootEncoderTests[String, D, tagged.type] - type TaggedDecoder[D] = Decoder[String, D, DecodeError, tagged.type] - type TaggedEncoder[D] = Encoder[String, D, tagged.type] - type TaggedDecoderLaws[D] = DecoderLaws[String, D, DecodeError, tagged.type] - type TaggedEncoderLaws[D] = EncoderLaws[String, D, tagged.type] - type TaggedLegalValue[D] = LegalValue[String, D, tagged.type] + type DecoderTests[D] = RootDecoderTests[String, D, DecodeError, tagged.type] + type EncoderTests[D] = RootEncoderTests[String, D, tagged.type] + type TaggedDecoder[D] = Decoder[String, D, DecodeError, tagged.type] + type TaggedEncoder[D] = Encoder[String, D, tagged.type] + type TaggedDecoderLaws[D] = DecoderLaws[String, D, DecodeError, tagged.type] + type TaggedEncoderLaws[D] = EncoderLaws[String, D, tagged.type] + type TaggedLegalValue[D] = LegalValue[String, D, tagged.type] + type HasLegalTaggedValues[D] = HasLegalValues[String, D, tagged.type] + type HasIllegalTaggedValues[D] = HasIllegalValues[String, D, tagged.type] // - Specialised tests ----------------------------------------------------------------------------------------------- // ------------------------------------------------------------------------------------------------------------------- @@ -76,4 +79,10 @@ object tagged { def codec[D: StringCodec]: Codec[String, D, DecodeError, tagged.type] = imp[StringCodec[D]].tag[tagged.type] + implicit def hasLegalValues[D](implicit hlv: HasLegalStringValues[D]): HasLegalTaggedValues[D] = + hlv.tag[tagged.type] + + implicit def hasIllegalValues[D](implicit hiv: HasIllegalStringValues[D]): HasIllegalTaggedValues[D] = + hiv.tag[tagged.type] + } diff --git a/laws/js/src/main/scala/kantan/codecs/laws/PlatformSpecificHasIllegalValues.scala b/laws/js/src/main/scala/kantan/codecs/laws/PlatformSpecificHasIllegalValues.scala new file mode 100644 index 00000000..f36b2fef --- /dev/null +++ b/laws/js/src/main/scala/kantan/codecs/laws/PlatformSpecificHasIllegalValues.scala @@ -0,0 +1,19 @@ +/* + * Copyright 2016 Nicolas Rinaudo + * + * 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 kantan.codecs.laws + +trait PlatformSpecificHasIllegalValues diff --git a/laws/js/src/main/scala/kantan/codecs/laws/PlatformSpecificHasLegalValues.scala b/laws/js/src/main/scala/kantan/codecs/laws/PlatformSpecificHasLegalValues.scala new file mode 100644 index 00000000..8dc46fa2 --- /dev/null +++ b/laws/js/src/main/scala/kantan/codecs/laws/PlatformSpecificHasLegalValues.scala @@ -0,0 +1,19 @@ +/* + * Copyright 2016 Nicolas Rinaudo + * + * 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 kantan.codecs.laws + +trait PlatformSpecificHasLegalValues diff --git a/laws/jvm/src/main/scala/kantan/codecs/laws/PlatformSpecificHasIllegalValues.scala b/laws/jvm/src/main/scala/kantan/codecs/laws/PlatformSpecificHasIllegalValues.scala new file mode 100644 index 00000000..32b5d49e --- /dev/null +++ b/laws/jvm/src/main/scala/kantan/codecs/laws/PlatformSpecificHasIllegalValues.scala @@ -0,0 +1,30 @@ +/* + * Copyright 2016 Nicolas Rinaudo + * + * 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 kantan.codecs.laws + +import java.io.File +import java.net.URL +import java.nio.file.{Path, Paths} +import kantan.codecs.laws.HasIllegalValues._ + +trait PlatformSpecificHasIllegalValues { + + implicit val url: HasIllegalStringValues[URL] = fromUnsafeString(e => new URL(e)) + implicit val path: HasIllegalStringValues[Path] = fromUnsafeString(e => Paths.get(e.toString)) + implicit val file: HasIllegalStringValues[File] = fromUnsafeString(e => new File(e)) + +} diff --git a/laws/jvm/src/main/scala/kantan/codecs/laws/PlatformSpecificHasLegalValues.scala b/laws/jvm/src/main/scala/kantan/codecs/laws/PlatformSpecificHasLegalValues.scala new file mode 100644 index 00000000..f63905e0 --- /dev/null +++ b/laws/jvm/src/main/scala/kantan/codecs/laws/PlatformSpecificHasLegalValues.scala @@ -0,0 +1,30 @@ +/* + * Copyright 2016 Nicolas Rinaudo + * + * 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 kantan.codecs.laws + +import java.io.File +import java.net.URL +import java.nio.file.Path +import kantan.codecs.laws.HasLegalValues._ + +trait PlatformSpecificHasLegalValues { + + implicit val url: HasLegalStringValues[URL] = fromToString + implicit val path: HasLegalStringValues[Path] = fromToString + implicit val file: HasLegalStringValues[File] = fromToString + +} diff --git a/laws/shared/src/main/scala/kantan/codecs/laws/CodecValue.scala b/laws/shared/src/main/scala/kantan/codecs/laws/CodecValue.scala index c47bcd81..83143070 100644 --- a/laws/shared/src/main/scala/kantan/codecs/laws/CodecValue.scala +++ b/laws/shared/src/main/scala/kantan/codecs/laws/CodecValue.scala @@ -16,33 +16,66 @@ package kantan.codecs.laws -// TODO: investigate what type variance annotations can be usefully applied to CodecValue. +import imp.imp +import org.scalacheck.{Arbitrary, Gen, Shrink} + +/** Represents possible encoded and decoded values. + * + * There are two main categories of values: + * - legal ones: an encoded / decoded couple, where decoding one should yield the other and vice versa. + * - illegal ones: an encoded value that cannot be decoded. + * + * The purpose of this type is to test encoder, decoder and codec laws - which means we'll almost always need + * `Arbitrary` instances for them. These can be tedious to write, but that tedium can be alleviated somewhat through + * the [[HasIllegalValues]] and [[HasLegalValues]] type classes. + */ sealed abstract class CodecValue[Encoded, Decoded, Tag] extends Product with Serializable { def encoded: Encoded def mapEncoded[E](f: Encoded => E): CodecValue[E, Decoded, Tag] def mapDecoded[D](f: Decoded => D): CodecValue[Encoded, D, Tag] - def tag[T]: CodecValue[Encoded, Decoded, T] - def isLegal: Boolean def isIllegal: Boolean = !isLegal + def tag[T]: CodecValue[Encoded, Decoded, T] } object CodecValue { + + /** Represents a legal value: one that can be safely encoded and decoded. */ final case class LegalValue[Encoded, Decoded, Tag](encoded: Encoded, decoded: Decoded) extends CodecValue[Encoded, Decoded, Tag] { override def mapDecoded[D](f: Decoded => D) = LegalValue(encoded, f(decoded)) override def mapEncoded[E](f: Encoded => E) = LegalValue(f(encoded), decoded) + override val isLegal = true @SuppressWarnings(Array("org.wartremover.warts.AsInstanceOf")) - override def tag[T] = this.asInstanceOf[LegalValue[Encoded, Decoded, T]] - override val isLegal = true + override def tag[T]: LegalValue[Encoded, Decoded, T] = this.asInstanceOf[LegalValue[Encoded, Decoded, T]] } + /** Represents an illegal value: one that cannot be decoded. */ final case class IllegalValue[Encoded, Decoded, Tag](encoded: Encoded) extends CodecValue[Encoded, Decoded, Tag] { override def mapDecoded[D](f: Decoded => D) = IllegalValue(encoded) override def mapEncoded[E](f: Encoded => E) = IllegalValue(f(encoded)) + override val isLegal = false @SuppressWarnings(Array("org.wartremover.warts.AsInstanceOf")) - override def tag[T] = this.asInstanceOf[IllegalValue[Encoded, Decoded, T]] - override val isLegal = false + override def tag[T]: IllegalValue[Encoded, Decoded, T] = this.asInstanceOf[IllegalValue[Encoded, Decoded, T]] } + + // - Arbitrary instances --------------------------------------------------------------------------------------------- + // ------------------------------------------------------------------------------------------------------------------- + implicit def arbValue[E, D, T]( + implicit arbLegal: Arbitrary[LegalValue[E, D, T]], + arbIllegal: Arbitrary[IllegalValue[E, D, T]] + ): Arbitrary[CodecValue[E, D, T]] = + Arbitrary(Gen.oneOf(arbLegal.arbitrary, arbIllegal.arbitrary)) + + implicit def arbLegalValue[E, D: Arbitrary, T]( + implicit alv: HasLegalValues[E, D, T] + ): Arbitrary[LegalValue[E, D, T]] = + Arbitrary(imp[Arbitrary[D]].arbitrary.map(alv.asLegalValue)) + + implicit def arbIllegalValue[E: Arbitrary, D, T]( + implicit aiv: HasIllegalValues[E, D, T] + ): Arbitrary[IllegalValue[E, D, T]] = + Arbitrary(imp[Arbitrary[E]].arbitrary.map(aiv.asIllegalValue)) + } diff --git a/laws/shared/src/main/scala/kantan/codecs/laws/HasIllegalValues.scala b/laws/shared/src/main/scala/kantan/codecs/laws/HasIllegalValues.scala new file mode 100644 index 00000000..14e192cd --- /dev/null +++ b/laws/shared/src/main/scala/kantan/codecs/laws/HasIllegalValues.scala @@ -0,0 +1,109 @@ +/* + * Copyright 2016 Nicolas Rinaudo + * + * 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 kantan.codecs.laws + +import imp.imp +import java.net.URI +import java.util.UUID +import kantan.codecs.laws.CodecValue.IllegalValue +import scala.reflect.ClassTag + +/** Type class that describes types for which values in the encoded type exist that cannot be decoded. + * + * The main purpose of this type class is to allow for automatic derivation of `Arbitrary` and `Shrink` instances + * of [[CodecValue.IllegalValue]] for use in laws. It's possible to write these instances manually, but + * `HasIllegalValues` simplifies the process for a lot of scenarios. + * + * Default instances are provided for standard types encoded as strings. In order to re-use them for codecs that are + * essentially wrapper for string codecs, you can stick the following somewhere in the implicit context: + * {{{ + * implicit def hasIllegalValues[D](implicit hiv: HasIllegalStringValues[D]): HasIllegalValues[String, D, mycodec.type] = + * hiv.tag[mycodec..type] + * }}} + */ +trait HasIllegalValues[Encoded, Decoded, Tag] { + + /** Checks whether the corresponding encoded value can be decoded. */ + def isValid(e: Encoded): Boolean + + def isInvalid(e: Encoded): Boolean = !isValid(e) + + /** Makes the specified encoded value illegal. + * + * The specified encoded value is expected to be valid (in the sense of [[isValid]]). + */ + def perturb(e: Encoded): Encoded + + def asIllegalValue(e: Encoded): IllegalValue[Encoded, Decoded, Tag] = IllegalValue( + if(isValid(e)) perturb(e) + else e + ) + + /** Changes the instance's tag type. + * + * This is meant to allow developers of codecs that rely on a pre-existing one (such as CSV relies on String codecs) + * to re-use existing instances at 0 cost. + */ + @SuppressWarnings(Array("org.wartremover.warts.AsInstanceOf")) + def tag[T]: HasIllegalValues[Encoded, Decoded, T] = this.asInstanceOf[HasIllegalValues[Encoded, Decoded, T]] +} + +/** Provides default instances, as well as instance creation helpers. */ +object HasIllegalValues extends PlatformSpecificHasIllegalValues { + + /** Creates a new instance based on the specified (unsafe) decoding function. + * + * If the specified function throws, the corresponding value is considered invalid. + */ + def fromUnsafeString[D, U](decode: String => U): HasIllegalStringValues[D] = + fromString(e => scala.util.Try(decode(e)).isSuccess) + + /** Creates a new instance based on the specified validity checking function. */ + def fromString[D](valid: String => Boolean): HasIllegalStringValues[D] = + new HasIllegalStringValues[D] { + override def isValid(e: String) = valid(e) + override def perturb(e: String) = s"perturbed-$e" + + } + + // - Default instances ----------------------------------------------------------------------------------------------- + // ------------------------------------------------------------------------------------------------------------------- + implicit val int: HasIllegalStringValues[Int] = fromUnsafeString(Integer.parseInt) + implicit val short: HasIllegalStringValues[Short] = fromUnsafeString(java.lang.Short.parseShort) + implicit val uuid: HasIllegalStringValues[UUID] = fromUnsafeString(UUID.fromString) + implicit val long: HasIllegalStringValues[Long] = fromUnsafeString(java.lang.Long.parseLong) + implicit val float: HasIllegalStringValues[Float] = fromUnsafeString(java.lang.Float.parseFloat) + implicit val double: HasIllegalStringValues[Double] = fromUnsafeString(java.lang.Double.parseDouble) + implicit val byte: HasIllegalStringValues[Byte] = fromUnsafeString(java.lang.Byte.parseByte) + implicit val boolean: HasIllegalStringValues[Boolean] = fromUnsafeString(java.lang.Boolean.parseBoolean) + implicit val bigInt: HasIllegalStringValues[BigInt] = fromUnsafeString(BigInt.apply) + implicit val bigDecimal: HasIllegalStringValues[BigDecimal] = fromUnsafeString(BigDecimal.apply) + implicit val uri: HasIllegalStringValues[URI] = fromUnsafeString(e => new URI(e)) + implicit val char: HasIllegalStringValues[Char] = fromString(_.length == 1) + + @SuppressWarnings(Array("org.wartremover.warts.AsInstanceOf")) + implicit def javaEnum[T <: Enum[T]](implicit tag: ClassTag[T]): HasIllegalStringValues[T] = + fromUnsafeString(str => Enum.valueOf(tag.runtimeClass.asInstanceOf[Class[T]], str)) + + implicit def option[D: HasIllegalStringValues]: HasIllegalStringValues[Option[D]] = + fromString(str => str.isEmpty || imp[HasIllegalStringValues[D]].isValid(str)) + + implicit def either[L: HasIllegalStringValues, R: HasIllegalStringValues]: HasIllegalStringValues[Either[L, R]] = + fromString { str => + imp[HasIllegalStringValues[L]].isValid(str) || imp[HasIllegalStringValues[R]].isValid(str) + } +} diff --git a/laws/shared/src/main/scala/kantan/codecs/laws/HasLegalValues.scala b/laws/shared/src/main/scala/kantan/codecs/laws/HasLegalValues.scala new file mode 100644 index 00000000..f2d9a1a9 --- /dev/null +++ b/laws/shared/src/main/scala/kantan/codecs/laws/HasLegalValues.scala @@ -0,0 +1,105 @@ +/* + * Copyright 2016 Nicolas Rinaudo + * + * 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 kantan.codecs.laws + +import java.net.URI +import java.util.UUID +import kantan.codecs.Optional +import kantan.codecs.laws.CodecValue.LegalValue + +/** Type class that describes types for which legal values encoded / decoded couples exist. + * + * The main purpose of this type class is to allow for automatic derivation of `Arbitrary` and `Shrink` instances + * of [[CodecValue.LegalValue]] for use in laws. It's possible to write these instances manually, but `HasLegalValues` + * simplifies the process for a lot of scenarios. + * + * Default instances are provided for standard types encoded as strings. In order to re-use them for codecs that are + * essentially wrapper for string codecs, you can stick the following somewhere in the implicit context: + * {{{ + * implicit def hasLegalValues[D](implicit hlv: HasLegalStringValues[D]): HasLegalValues[String, D, mycodec.type] = + * hlv.tag[mycodec..type] + * }}} + */ +trait HasLegalValues[Encoded, Decoded, Tag] { + + /** Encodes the specified value as a legal `Encoded`. + * + * Note that this might seem like a re-implementation of something that is already available in `Encoder`, but this + * is done on purpose. `HasLegalValues` are typically used to test the behaviour of `Encoder`, and testing an + * implementation against itself is counter-productive. + * + * Note that this might seem to contradict the entire "don't re-implement the system under test" rule of property + * based testing. This is true, *but* in the vast majority of the time, you'll have a default, trivial encoding + * method (`toString`, most of the time), which might be refined by the encoder themselves. We're not exactly + * re-implementing the system under test, but providing a framework for testing against a pre-existing oracle. + */ + def encode(d: Decoded): Encoded + + def asLegalValue(d: Decoded): LegalValue[Encoded, Decoded, Tag] = LegalValue(encode(d), d) + + /** Changes the instance's tag type. + * + * This is meant to allow developers of codecs that rely on a pre-existing one (such as CSV relies on String codecs) + * to re-use existing instances at 0 cost. + */ + @SuppressWarnings(Array("org.wartremover.warts.AsInstanceOf")) + def tag[T]: HasLegalValues[Encoded, Decoded, T] = this.asInstanceOf[HasLegalValues[Encoded, Decoded, T]] +} + +/** Provides default instances, as well as instance creation helpers. */ +object HasLegalValues extends PlatformSpecificHasLegalValues { + + /** Creates a new instance from the specified encoding function. */ + def from[E, D, T](f: D => E): HasLegalValues[E, D, T] = new HasLegalValues[E, D, T] { + override def encode(d: D) = f(d) + } + + /** Creates an instance who considers `toString` to be the encoding method for the specified type. */ + @SuppressWarnings(Array("org.wartremover.warts.ToString")) + def fromToString[D, U]: HasLegalStringValues[D] = from(_.toString) + + // - Default instances ----------------------------------------------------------------------------------------------- + // ------------------------------------------------------------------------------------------------------------------- + implicit val string: HasLegalStringValues[String] = fromToString + implicit val uuid: HasLegalStringValues[UUID] = fromToString + implicit val int: HasLegalStringValues[Int] = fromToString + implicit val short: HasLegalStringValues[Short] = fromToString + implicit val long: HasLegalStringValues[Long] = fromToString + implicit val float: HasLegalStringValues[Float] = fromToString + implicit val double: HasLegalStringValues[Double] = fromToString + implicit val byte: HasLegalStringValues[Byte] = fromToString + implicit val boolean: HasLegalStringValues[Boolean] = fromToString + implicit val bigInt: HasLegalStringValues[BigInt] = fromToString + implicit val bigDecimal: HasLegalStringValues[BigDecimal] = fromToString + implicit val uri: HasLegalStringValues[URI] = fromToString + implicit val char: HasLegalStringValues[Char] = fromToString + implicit def javaEnum[T <: Enum[T]]: HasLegalStringValues[T] = from(_.name()) + + implicit def option[E: Optional, D, T](implicit hl: HasLegalValues[E, D, T]): HasLegalValues[E, Option[D], T] = from { + _.map(hl.encode) + .getOrElse(Optional[E].empty) + } + + implicit def either[E, L, R, T]( + implicit hll: HasLegalValues[E, L, T], + hlr: HasLegalValues[E, R, T] + ): HasLegalValues[E, Either[L, R], T] = from { + case Left(l) => hll.encode(l) + case Right(r) => hlr.encode(r) + } + +} diff --git a/laws/shared/src/main/scala/kantan/codecs/laws/discipline/arbitrary.scala b/laws/shared/src/main/scala/kantan/codecs/laws/discipline/arbitrary.scala index 70f27a93..6f6d7c38 100644 --- a/laws/shared/src/main/scala/kantan/codecs/laws/discipline/arbitrary.scala +++ b/laws/shared/src/main/scala/kantan/codecs/laws/discipline/arbitrary.scala @@ -21,7 +21,6 @@ import java.io._ import java.util.{Date, UUID} import java.util.regex.Pattern import kantan.codecs.{Decoder, Encoder} -import kantan.codecs.laws.CodecValue import kantan.codecs.laws.CodecValue.{IllegalValue, LegalValue} import kantan.codecs.strings.DecodeError import org.scalacheck.{Arbitrary, Cogen, Gen} @@ -67,26 +66,14 @@ trait CommonArbitraryInstances extends ArbitraryArities { // - CodecValue ------------------------------------------------------------------------------------------------------ // ------------------------------------------------------------------------------------------------------------------- - implicit def arbValue[E, D, T]( - implicit arbL: Arbitrary[LegalValue[E, D, T]], - arbI: Arbitrary[IllegalValue[E, D, T]] - ): Arbitrary[CodecValue[E, D, T]] = - Arbitrary(Gen.oneOf(arbL.arbitrary, arbI.arbitrary)) - - implicit def arbLegalValueFromEnc[E, A: Arbitrary, T](implicit ea: Encoder[E, A, T]): Arbitrary[LegalValue[E, A, T]] = - arbLegalValue(ea.encode) - - implicit def arbIllegalValueFromDec[E: Arbitrary, A, T]( - implicit da: Decoder[E, A, _, T] - ): Arbitrary[IllegalValue[E, A, T]] = arbIllegalValue(e => da.decode(e).isLeft) def arbLegalValue[E, A, T](encode: A => E)(implicit arbA: Arbitrary[A]): Arbitrary[LegalValue[E, A, T]] = Arbitrary { arbA.arbitrary.map(a => LegalValue(encode(a), a)) } - def arbIllegalValue[E: Arbitrary, A, T](illegal: E => Boolean): Arbitrary[IllegalValue[E, A, T]] = + def arbIllegalValue[E: Arbitrary, A, T](isIllegal: E => Boolean): Arbitrary[IllegalValue[E, A, T]] = Arbitrary { - imp[Arbitrary[E]].arbitrary.suchThat(illegal).map(e => IllegalValue(e)) + imp[Arbitrary[E]].arbitrary.suchThat(isIllegal).map(e => IllegalValue(e)) } // - Codecs ---------------------------------------------------------------------------------------------------------- diff --git a/laws/shared/src/main/scala/kantan/codecs/laws/package.scala b/laws/shared/src/main/scala/kantan/codecs/laws/package.scala index 8b27ab99..54354f44 100644 --- a/laws/shared/src/main/scala/kantan/codecs/laws/package.scala +++ b/laws/shared/src/main/scala/kantan/codecs/laws/package.scala @@ -21,9 +21,11 @@ import kantan.codecs.strings.{codecs, DecodeError} package object laws { - type StringValue[A] = CodecValue[String, A, codecs.type] - type LegalString[A] = LegalValue[String, A, codecs.type] - type IllegalString[A] = IllegalValue[String, A, codecs.type] + type StringValue[A] = CodecValue[String, A, codecs.type] + type LegalString[A] = LegalValue[String, A, codecs.type] + type IllegalString[A] = IllegalValue[String, A, codecs.type] + type HasLegalStringValues[D] = HasLegalValues[String, D, codecs.type] + type HasIllegalStringValues[D] = HasIllegalValues[String, D, codecs.type] type StringEncoderLaws[A] = EncoderLaws[String, A, codecs.type] type StringDecoderLaws[A] = DecoderLaws[String, A, DecodeError, codecs.type] diff --git a/shapeless/laws/shared/src/main/scala/kantan/codecs/shapeless/laws/Or.scala b/shapeless/laws/shared/src/main/scala/kantan/codecs/shapeless/laws/Or.scala index 6c34747e..c6b5810b 100644 --- a/shapeless/laws/shared/src/main/scala/kantan/codecs/shapeless/laws/Or.scala +++ b/shapeless/laws/shared/src/main/scala/kantan/codecs/shapeless/laws/Or.scala @@ -16,6 +16,30 @@ package kantan.codecs.shapeless.laws +import kantan.codecs.laws.{HasIllegalValues, HasLegalValues} + sealed trait Or[+A, +B] extends Product with Serializable final case class Left[A](a: A) extends Or[A, Nothing] final case class Right[B](b: B) extends Or[Nothing, B] + +object Or { + def orToEither[L, R](or: Or[L, R]): Either[L, R] = or match { + case Left(l) => scala.util.Left(l) + case Right(r) => scala.util.Right(r) + } + + implicit def hasLegalValues[E, L, R, T]( + implicit hl: HasLegalValues[E, Either[L, R], T] + ): HasLegalValues[E, Or[L, R], T] = + HasLegalValues.from { or => + hl.encode(orToEither(or)) + } + + implicit def hasIllegalValues[E, L, R, T]( + implicit hi: HasIllegalValues[E, Either[L, R], T] + ): HasIllegalValues[E, Or[L, R], T] = + new HasIllegalValues[E, Or[L, R], T] { + override def isValid(e: E) = hi.isValid(e) + override def perturb(e: E) = hi.perturb(e) + } +}