diff --git a/docs/README.md b/docs/README.md index de0b45b4..aad8bec6 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,5 +1,13 @@ +[comment]: <> (Don't edit this file!) +[comment]: <> (It is automatically updated after every release of https://github.com/47degrees/.github) +[comment]: <> (If you want to suggest a change, please open a PR or issue in that repository) -[![codecov.io](http://codecov.io/gh/higherkindness/skeuomorph/branch/master/graph/badge.svg)](http://codecov.io/gh/higherkindness/skeuomorph) [![Maven Central](https://img.shields.io/badge/maven%20central-0.0.22-green.svg)](https://oss.sonatype.org/#nexus-search;gav~io.higherkindness~skeuomorph*) [![Latest version](https://img.shields.io/badge/skeuomorph-0.0.22-green.svg)](https://index.scala-lang.org/higherkindness/skeuomorph) [![License](https://img.shields.io/badge/license-Apache%202-blue.svg)](https://raw.githubusercontent.com/higherkindness/skeuomorph/master/LICENSE) [![Join the chat at https://gitter.im/higherkindness/skeuomorph](https://badges.gitter.im/higherkindness/skeuomorph.svg)](https://gitter.im/higherkindness/skeuomorph?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [![GitHub Issues](https://img.shields.io/github/issues/higherkindness/skeuomorph.svg)](https://github.com/higherkindness/skeuomorph/issues) +[![codecov.io](http://codecov.io/gh/higherkindness/skeuomorph/branch/master/graph/badge.svg)](http://codecov.io/gh/higherkindness/skeuomorph) +[![Maven Central](https://img.shields.io/badge/maven%20central-0.0.25-green.svg)](https://oss.sonatype.org/#nexus-search;gav~io.higherkindness~skeuomorph*) +[![Latest version](https://img.shields.io/badge/skeuomorph-0.0.25-green.svg)](https://index.scala-lang.org/higherkindness/skeuomorph) +[![License](https://img.shields.io/badge/license-Apache%202-blue.svg)](https://raw.githubusercontent.com/higherkindness/skeuomorph/master/LICENSE) +[![Join the chat at https://gitter.im/higherkindness/skeuomorph](https://badges.gitter.im/higherkindness/skeuomorph.svg)](https://gitter.im/higherkindness/skeuomorph?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) +[![GitHub Issues](https://img.shields.io/github/issues/higherkindness/skeuomorph.svg)](https://github.com/higherkindness/skeuomorph/issues) # @NAME@ @@ -18,7 +26,7 @@ More information can be found at the [microsite][]. For installing this library, add the following line to your `build.sbt` file: -```scala +```sbt libraryDependencies += "io.higherkindness" %% "skeuomorph" % "@VERSION@" ``` @@ -26,7 +34,9 @@ The full documentation is available at the [skeuomorph](https://higherkindness.i ## NOTICE -The following files `api-with-examples.yaml`, `petstore-expanded.yaml`, `callback-example.yaml`, `petstore.yaml`, `link-example.yaml` and `uspto.yaml` inside the folder (`test/resources/openapi/yaml`) were copied from [**OpenAPI Specification**](https://github.com/OAI/OpenAPI-Specification/) project under the terms of the licence [*Apache License Version 2.0, January 2004*](https://github.com/OAI/OpenAPI-Specification/blob/master/LICENSE). +The following files `api-with-examples.yaml`, `petstore-expanded.yaml`, `callback-example.yaml`, `petstore.yaml`, `link-example.yaml` and `uspto.yaml` +inside the folder (`test/resources/openapi/yaml`) were copied from [**OpenAPI Specification**](https://github.com/OAI/OpenAPI-Specification/) +project under the terms of the licence [*Apache License Version 2.0, January 2004*](https://github.com/OAI/OpenAPI-Specification/blob/master/LICENSE). ## Skeuomorph in the wild diff --git a/microsite/docs/docs/index.md b/microsite/docs/docs/index.md index e3cb5971..65e64b68 100644 --- a/microsite/docs/docs/index.md +++ b/microsite/docs/docs/index.md @@ -4,7 +4,6 @@ title: Intro permalink: docs/ --- - # Skeuomorph Skeuomorph is a library for transforming different schemas in Scala. @@ -32,7 +31,7 @@ example. Or to a mu service description. You can install skeuomorph as follows: -```scala +```sbt libraryDependencies += "io.higherkindness" %% "skeuomorph" % "@VERSION@" ``` @@ -102,11 +101,9 @@ println("=====") It would generate the following output: ```scala mdoc:passthrough -println("```scala") (toMuSchema >>> println)(avroSchema) println("=====") (toMuSchema >>> printSchemaAsScala >>> println)(avroSchema) -println("```") ``` ## Protobuf @@ -117,8 +114,9 @@ Given the proto file below: _user.proto_ -```protobuf +```proto syntax = "proto3"; + package example.proto; message User { @@ -145,7 +143,7 @@ val source = ParseProto.ProtoSource("user.proto", new java.io.File(".").getAbsol val protobufProtocol: Protocol[Mu[ProtobufF]] = ParseProto.parseProto[IO, Mu[ProtobufF]].parse(source).unsafeRunSync() val toMuProtocol: Protocol[Mu[ProtobufF]] => mu.Protocol[Mu[MuF]] = { p: Protocol[Mu[ProtobufF]] => - mu.Protocol.fromProtobufProto(CompressionType.Identity, true)(p) + mu.Protocol.fromProtobufProto(CompressionType.Identity)(p) } val printProtocolAsScala: mu.Protocol[Mu[MuF]] => Either[String, String] = { p => @@ -162,13 +160,10 @@ println("=====") It would generate the following output: - ```scala mdoc:passthrough -println("```scala") (toMuProtocol >>> println)(protobufProtocol) println("=====") (toMuProtocol >>> printProtocolAsScala >>> println)(protobufProtocol) -println("```") ``` #### Proto2 Incompatibility diff --git a/microsite/docs/docs/optimizations.md b/microsite/docs/docs/optimizations.md index ed8fbb40..738fce4f 100644 --- a/microsite/docs/docs/optimizations.md +++ b/microsite/docs/docs/optimizations.md @@ -49,33 +49,25 @@ they're inside a product themselves. And we do this with the def nestedNamedTypesTrans[T](implicit T: Basis[MuF, T]): Trans[MuF, MuF, T] = Trans { case TProduct(name, fields, np, nc) => def nameTypes(f: Field[T]): Field[T] = f.copy(tpe = namedTypes(T)(f.tpe)) - TProduct[T]( - name, - fields.map(nameTypes), - np, - nc - ) + TProduct[T](name, fields.map(nameTypes), np, nc) case other => other } def namedTypesTrans[T]: Trans[MuF, MuF, T] = Trans { case TProduct(name, _, _, _) => TNamedType[T](Nil, name) - case TSum(name, _) => TNamedType[T](Nil, name) - case other => other + case TSum(name, _) => TNamedType[T](Nil, name) + case other => other } def namedTypes[T: Basis[MuF, ?]]: T => T = scheme.cata(namedTypesTrans.algebra) def nestedNamedTypes[T: Basis[MuF, ?]]: T => T = scheme.cata(nestedNamedTypesTrans.algebra) - ``` and then apply the `namedTypes` combinator to the AST: -```scala mdoc:invisible +```scala mdoc def ast = Mu(TNull[Mu[MuF]]()) -``` -```scala mdoc val optimization = Optimize.namedTypes[Mu[MuF]] optimization(ast) diff --git a/microsite/docs/docs/schemas.md b/microsite/docs/docs/schemas.md index cbfafa6d..705a0599 100644 --- a/microsite/docs/docs/schemas.md +++ b/microsite/docs/docs/schemas.md @@ -25,12 +25,12 @@ Currently in skeuomorph there are schemas defined for different cases: Currently, Skeuomorph only supports proto3 compliance, and the recommended approach when using skeuomorph with [mu][] is to use proto3 for all gRPC communications. While it is still possible to generate valid Scala code from a proto2 spec, -Skeuomorph will _not_ generate case classes for optional fields. For example, given a schema that looks like this: +Skeuomorph will _not_ generate case classes for optional fields. For example, given a `hello.proto` schema that looks like this: -```protobuf mdoc:silent +```proto syntax = "proto2"; -package src.main.hello; +package src.main; message SayHelloRequest { optional string name = 1; @@ -44,15 +44,17 @@ service HelloWorldService { } ``` -Skeuomorph (with mu) will generate the following Scala code: +Skeuomorph (with mu and default plugin options) will generate the following Scala code: ```scala object hello { - final case class SayHelloRequest(name: String) - final case class SayHelloResponse(message: String) - @service(Protobuf, Identity, namespace = Some("src.main.hello"), methodNameStyle = Capitalize) + + final case class SayHelloRequest(name: _root_.java.lang.String) + final case class SayHelloResponse(message: _root_.java.lang.String) + + @service(Protobuf, compressionType = Identity, namespace = Some("src.main")) trait HelloWorldService[F[_]] { - def SayHello(req: src.main.hello.hello.SayHelloRequest): F[src.main.hello.hello.SayHelloResponse] + def SayHello(req: _root_.src.main.hello.SayHelloRequest): F[_root_.src.main.hello.SayHelloResponse] } } ``` diff --git a/microsite/docs/index.md b/microsite/docs/index.md index feaa66a4..e94d86b7 100644 --- a/microsite/docs/index.md +++ b/microsite/docs/index.md @@ -2,6 +2,6 @@ layout: homeFeatures features: - first: ["Non-recursive ADTs", "Declare languages as constructors in a simple way"] - - second: ["Transformations & optimizations", "Apply nanopass optimizations to the Abstract Syntax Trees of your program"] - - third: ["Recursion Schemes", "Leverage the power of Recursion Schemes to make performant and easy to write programs on your ADTs"] + - second: ["Transformations & optimizations", "Apply nanopass optimizations to the Abstract Syntax Trees of your program", "schemas"] + - third: ["Recursion Schemes", "Leverage the power of Recursion Schemes to make performant and easy to write programs on your ASTs", "optimizations"] --- diff --git a/src/main/scala/higherkindness/skeuomorph/mu/codegen.scala b/src/main/scala/higherkindness/skeuomorph/mu/codegen.scala index 8323e720..c813c2ad 100644 --- a/src/main/scala/higherkindness/skeuomorph/mu/codegen.scala +++ b/src/main/scala/higherkindness/skeuomorph/mu/codegen.scala @@ -234,13 +234,11 @@ object codegen { val serializationType = Term.Name(srv.serializationType.toString) val compressionType = Term.Name(srv.compressionType.toString) - val serviceAnnotation = srv.idiomaticEndpoints match { - case IdiomaticEndpoints(Some(pkg), true) => - mod"@service($serializationType, $compressionType, namespace = Some($pkg), methodNameStyle = Capitalize)" - case IdiomaticEndpoints(None, true) => - mod"@service($serializationType, $compressionType, methodNameStyle = Capitalize)" + val serviceAnnotation = srv.namespace match { + case Some(namespace) if srv.useIdiomaticEndpoints => + mod"@service($serializationType, compressionType = $compressionType, namespace = Some($namespace))" case _ => - mod"@service($serializationType, $compressionType)" + mod"@service($serializationType, compressionType = $compressionType, namespace = None)" } srv.operations.traverse(op => operation(op, streamCtor)).map { ops => diff --git a/src/main/scala/higherkindness/skeuomorph/mu/protocol.scala b/src/main/scala/higherkindness/skeuomorph/mu/protocol.scala index 88112229..b0c7df8c 100644 --- a/src/main/scala/higherkindness/skeuomorph/mu/protocol.scala +++ b/src/main/scala/higherkindness/skeuomorph/mu/protocol.scala @@ -16,17 +16,15 @@ package higherkindness.skeuomorph.mu -import higherkindness.skeuomorph.protobuf -import higherkindness.skeuomorph.protobuf.ProtobufF -import higherkindness.skeuomorph.avro +import higherkindness.droste._ import higherkindness.skeuomorph.avro.AvroF import higherkindness.skeuomorph.mu.Service.OperationType -import higherkindness.skeuomorph.mu.Transform.transformAvro -import higherkindness.skeuomorph.mu.Transform.transformProto -import higherkindness.droste._ +import higherkindness.skeuomorph.mu.Transform._ +import higherkindness.skeuomorph._ +import higherkindness.skeuomorph.protobuf.ProtobufF -sealed trait SerializationType extends Product with Serializable -object SerializationType { +private[skeuomorph] sealed trait SerializationType extends Product with Serializable +private[skeuomorph] object SerializationType { case object Protobuf extends SerializationType case object Avro extends SerializationType case object AvroWithSchema extends SerializationType @@ -38,8 +36,6 @@ object CompressionType { case object Identity extends CompressionType } -final case class IdiomaticEndpoints(pkg: Option[String], value: Boolean) - final case class Protocol[T]( name: Option[String], pkg: Option[String], @@ -53,7 +49,7 @@ object Protocol { /** * create a [[higherkindness.skeuomorph.mu.Protocol]] from a [[higherkindness.skeuomorph.avro.Protocol]] */ - def fromAvroProtocol[T, U](compressionType: CompressionType, useIdiomaticEndpoints: Boolean)( + def fromAvroProtocol[T, U](compressionType: CompressionType, useIdiomaticEndpoints: Boolean = true)( proto: avro.Protocol[T] )(implicit T: Basis[AvroF, T], U: Basis[MuF, U]): Protocol[U] = { @@ -76,7 +72,8 @@ object Protocol { proto.name, SerializationType.Avro, compressionType, - IdiomaticEndpoints(proto.namespace, useIdiomaticEndpoints), + proto.namespace, + useIdiomaticEndpoints, proto.messages.map(toOperation) ) ), @@ -84,7 +81,7 @@ object Protocol { ) } - def fromProtobufProto[T, U](compressionType: CompressionType, useIdiomaticEndpoints: Boolean)( + def fromProtobufProto[T, U](compressionType: CompressionType, useIdiomaticEnpoints: Boolean = true)( protocol: protobuf.Protocol[T] )(implicit T: Basis[ProtobufF, T], U: Basis[MuF, U]): Protocol[U] = { val toMu: T => U = scheme.cata(transformProto[U].algebra) @@ -99,18 +96,19 @@ object Protocol { val toImports: DependentImport[T] => DependentImport[U] = imp => DependentImport(imp.pkg, imp.protocol, toMu(imp.tpe)) - new Protocol[U]( + Protocol[U]( name = Some(protocol.name), pkg = Option(protocol.pkg), options = protocol.options, declarations = protocol.declarations.map(toMu), services = protocol.services .map(s => - new Service[U]( + Service( s.name, SerializationType.Protobuf, compressionType, - IdiomaticEndpoints(Option(protocol.pkg), useIdiomaticEndpoints), + Option(protocol.pkg), + useIdiomaticEnpoints, s.operations.map(toOperation) ) ), @@ -125,7 +123,8 @@ final case class Service[T]( name: String, serializationType: SerializationType, compressionType: CompressionType, - idiomaticEndpoints: IdiomaticEndpoints, + namespace: Option[String], + useIdiomaticEndpoints: Boolean, operations: List[Service.Operation[T]] ) object Service { diff --git a/src/test/resources/avro/GreeterService.avdl b/src/test/resources/avro/GreeterService.avdl index 1f5d5513..124639ec 100644 --- a/src/test/resources/avro/GreeterService.avdl +++ b/src/test/resources/avro/GreeterService.avdl @@ -1,4 +1,4 @@ -@namespace("foo.bar") +@namespace("com.acme") protocol MyGreeterService { record HelloRequest { @@ -13,7 +13,7 @@ protocol MyGreeterService { array arg3; } - foo.bar.HelloResponse sayHelloAvro(foo.bar.HelloRequest arg); + com.acme.HelloResponse sayHelloAvro(com.acme.HelloRequest arg); void sayNothingAvro(); } diff --git a/src/test/scala/higherkindness/skeuomorph/protobuf/models/author.proto b/src/test/resources/protobuf/models/author.proto similarity index 100% rename from src/test/scala/higherkindness/skeuomorph/protobuf/models/author.proto rename to src/test/resources/protobuf/models/author.proto diff --git a/src/test/scala/higherkindness/skeuomorph/protobuf/models/hyphenated-name.proto b/src/test/resources/protobuf/models/hyphenated-name.proto similarity index 100% rename from src/test/scala/higherkindness/skeuomorph/protobuf/models/hyphenated-name.proto rename to src/test/resources/protobuf/models/hyphenated-name.proto diff --git a/src/test/scala/higherkindness/skeuomorph/protobuf/models/integer_types.proto b/src/test/resources/protobuf/models/integer_types.proto similarity index 100% rename from src/test/scala/higherkindness/skeuomorph/protobuf/models/integer_types.proto rename to src/test/resources/protobuf/models/integer_types.proto diff --git a/src/test/scala/higherkindness/skeuomorph/protobuf/models/opencensus/resource.proto b/src/test/resources/protobuf/models/opencensus/resource.proto similarity index 100% rename from src/test/scala/higherkindness/skeuomorph/protobuf/models/opencensus/resource.proto rename to src/test/resources/protobuf/models/opencensus/resource.proto diff --git a/src/test/scala/higherkindness/skeuomorph/protobuf/models/opencensus/trace.proto b/src/test/resources/protobuf/models/opencensus/trace.proto similarity index 100% rename from src/test/scala/higherkindness/skeuomorph/protobuf/models/opencensus/trace.proto rename to src/test/resources/protobuf/models/opencensus/trace.proto diff --git a/src/test/scala/higherkindness/skeuomorph/protobuf/models/type/date.proto b/src/test/resources/protobuf/models/type/date.proto similarity index 100% rename from src/test/scala/higherkindness/skeuomorph/protobuf/models/type/date.proto rename to src/test/resources/protobuf/models/type/date.proto diff --git a/src/test/scala/higherkindness/skeuomorph/protobuf/packages/test_java_package.proto b/src/test/resources/protobuf/packages/test_java_package.proto similarity index 100% rename from src/test/scala/higherkindness/skeuomorph/protobuf/packages/test_java_package.proto rename to src/test/resources/protobuf/packages/test_java_package.proto diff --git a/src/test/scala/higherkindness/skeuomorph/protobuf/packages/test_no_package.proto b/src/test/resources/protobuf/packages/test_no_package.proto similarity index 100% rename from src/test/scala/higherkindness/skeuomorph/protobuf/packages/test_no_package.proto rename to src/test/resources/protobuf/packages/test_no_package.proto diff --git a/src/test/scala/higherkindness/skeuomorph/protobuf/packages/test_only_java_package.proto b/src/test/resources/protobuf/packages/test_only_java_package.proto similarity index 100% rename from src/test/scala/higherkindness/skeuomorph/protobuf/packages/test_only_java_package.proto rename to src/test/resources/protobuf/packages/test_only_java_package.proto diff --git a/src/test/scala/higherkindness/skeuomorph/protobuf/rating.proto b/src/test/resources/protobuf/rating.proto similarity index 100% rename from src/test/scala/higherkindness/skeuomorph/protobuf/rating.proto rename to src/test/resources/protobuf/rating.proto diff --git a/src/test/scala/higherkindness/skeuomorph/protobuf/service/book.proto b/src/test/resources/protobuf/service/book.proto similarity index 100% rename from src/test/scala/higherkindness/skeuomorph/protobuf/service/book.proto rename to src/test/resources/protobuf/service/book.proto diff --git a/src/test/scala/higherkindness/skeuomorph/avro/AvroCatsLawsSpec.scala b/src/test/scala/higherkindness/skeuomorph/avro/AvroCatsLawsSpec.scala new file mode 100644 index 00000000..51e635a3 --- /dev/null +++ b/src/test/scala/higherkindness/skeuomorph/avro/AvroCatsLawsSpec.scala @@ -0,0 +1,36 @@ +/* + * Copyright 2018-2020 47 Degrees Open Source + * + * 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 higherkindness.skeuomorph.avro + +import higherkindness.skeuomorph.instances._ +import org.typelevel.discipline.specs2.Discipline +import cats.laws.discipline.{FoldableTests, FunctorTests, TraverseTests} +import cats.implicits._ +import org.specs2._ + +class AvroCatsLawsSpec extends Specification with ScalaCheck with Discipline { + + def is = s2""" + $traverse + $functor + $foldable + """ + + val traverse = checkAll("Traverse[AvroF]", TraverseTests[AvroF].traverse[Int, Int, Int, Set[Int], Option, Option]) + val functor = checkAll("Functor[AvroF]", FunctorTests[AvroF].functor[Int, Int, String]) + val foldable = checkAll("Foldable[AvroF]", FoldableTests[AvroF].foldable[Int, Int]) +} diff --git a/src/test/scala/higherkindness/skeuomorph/avro/AvroProtocolSpec.scala b/src/test/scala/higherkindness/skeuomorph/avro/AvroProtocolSpec.scala new file mode 100644 index 00000000..e231a85c --- /dev/null +++ b/src/test/scala/higherkindness/skeuomorph/avro/AvroProtocolSpec.scala @@ -0,0 +1,107 @@ +/* + * Copyright 2018-2020 47 Degrees Open Source + * + * 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 higherkindness.skeuomorph.avro + +import higherkindness.droste.data.Mu +import higherkindness.skeuomorph.mu.{CompressionType, SerializationType, codegen, Protocol => MuProtocol} +import org.apache.avro.compiler.idl._ +import org.scalacheck._ +import org.specs2._ +import scala.meta._ +import scala.meta.contrib._ + +class AvroProtocolSpec extends Specification with ScalaCheck { + + implicit val arbCompressionType: Arbitrary[CompressionType] = Arbitrary { + Gen.oneOf(CompressionType.Identity, CompressionType.Gzip) + } + + def is = s2""" + Avro Protocol + + It should be possible to create a Protocol from org.apache.avro.Protocol and then generate Scala code from it. $codegenAvroProtocol + """ + + def codegenAvroProtocol = + prop { (compressionType: CompressionType, useIdiomaticEndpoints: Boolean) => + val idl = new Idl(getClass.getClassLoader.getResourceAsStream("avro/GreeterService.avdl")) + val avroProto = idl.CompilationUnit() + + val skeuoAvroProto = Protocol.fromProto[Mu[AvroF]](avroProto) + + val muProto = MuProtocol.fromAvroProtocol(compressionType, useIdiomaticEndpoints)(skeuoAvroProto) + + val streamCtor: (Type, Type) => Type.Apply = { case (f: Type, a: Type) => + t"Stream[$f, $a]" + } + + val actual = codegen.protocol(muProto, streamCtor).right.get + + val expected = codegenExpectation(compressionType, muProto.pkg, useIdiomaticEndpoints) + .parse[Source] + .get + .children + .head + .asInstanceOf[Pkg] + + actual.isEqual(expected) :| s""" + |Actual output: + |$actual + | + | + |Expected output: + |$expected" + """.stripMargin + } + + // TODO test for more complex schemas, importing other files, etc. + + private def codegenExpectation( + compressionType: CompressionType, + namespace: Option[String], + useIdiomaticEndpoints: Boolean + ): String = { + + val serviceParams: String = Seq( + SerializationType.Avro, + s"compressionType = $compressionType", + s"namespace = ${if (useIdiomaticEndpoints) namespace.map("\"" + _ + "\"") else None}" + ).mkString(", ") + + s"""package com.acme + | + |import _root_.higherkindness.mu.rpc.protocol._ + | + |final case class HelloRequest( + | arg1: _root_.java.lang.String, + | arg2: _root_.scala.Option[_root_.java.lang.String], + | arg3: _root_.scala.List[_root_.java.lang.String] + |) + |final case class HelloResponse( + | arg1: _root_.java.lang.String, + | arg2: _root_.scala.Option[_root_.java.lang.String], + | arg3: _root_.scala.List[_root_.java.lang.String] + |) + | + |@service($serviceParams) trait MyGreeterService[F[_]] { + | def sayHelloAvro(req: _root_.com.acme.HelloRequest): F[_root_.com.acme.HelloResponse] + | def sayNothingAvro(req: _root_.higherkindness.mu.rpc.protocol.Empty.type): F[_root_.higherkindness.mu.rpc.protocol.Empty.type] + |} + """.stripMargin + } + +} diff --git a/src/test/scala/higherkindness/skeuomorph/avro/AvroSchemaSpec.scala b/src/test/scala/higherkindness/skeuomorph/avro/AvroSchemaSpec.scala index b4d692ab..04e4b253 100644 --- a/src/test/scala/higherkindness/skeuomorph/avro/AvroSchemaSpec.scala +++ b/src/test/scala/higherkindness/skeuomorph/avro/AvroSchemaSpec.scala @@ -16,21 +16,54 @@ package higherkindness.skeuomorph.avro +import higherkindness.droste._ import higherkindness.skeuomorph.instances._ -import org.typelevel.discipline.specs2.Discipline -import cats.laws.discipline.{FoldableTests, FunctorTests, TraverseTests} -import cats.implicits._ +import org.apache.avro.Schema +import org.scalacheck._ import org.specs2._ -class AvroSchemaSpec extends Specification with ScalaCheck with Discipline { +import scala.collection.JavaConverters._ + +class AvroSchemaSpec extends Specification with ScalaCheck { def is = s2""" - $traverse - $functor - $foldable + Avro Schema + + It should be possible to create a Schema from org.apache.avro.Schema. $convertSchema """ - val traverse = checkAll("Traverse[AvroF]", TraverseTests[AvroF].traverse[Int, Int, Int, Set[Int], Option, Option]) - val functor = checkAll("Functor[AvroF]", FunctorTests[AvroF].functor[Int, Int, String]) - val foldable = checkAll("Foldable[AvroF]", FoldableTests[AvroF].foldable[Int, Int]) + def convertSchema = + Prop.forAll { (schema: Schema) => + val test = scheme.hylo(checkSchema(schema), AvroF.fromAvro) + + test(schema) + } + + def checkSchema(sch: Schema): Algebra[AvroF, Boolean] = + Algebra { + case AvroF.TNull() => sch.getType should_== Schema.Type.NULL + case AvroF.TBoolean() => sch.getType should_== Schema.Type.BOOLEAN + case AvroF.TInt() => sch.getType should_== Schema.Type.INT + case AvroF.TLong() => sch.getType should_== Schema.Type.LONG + case AvroF.TFloat() => sch.getType should_== Schema.Type.FLOAT + case AvroF.TDouble() => sch.getType should_== Schema.Type.DOUBLE + case AvroF.TBytes() => sch.getType should_== Schema.Type.BYTES + case AvroF.TString() => sch.getType should_== Schema.Type.STRING + + case AvroF.TNamedType(_, _) => false + case AvroF.TArray(_) => sch.getType should_== Schema.Type.ARRAY + case AvroF.TMap(_) => sch.getType should_== Schema.Type.MAP + case AvroF.TRecord(name, namespace, _, doc, fields) => + (sch.getName should_== name) + .and(sch.getNamespace should_== namespace.getOrElse("")) + .and(sch.getDoc should_== doc.getOrElse("")) + .and( + sch.getFields.asScala.toList.map(f => (f.name, f.doc)) should_== fields + .map(f => (f.name, f.doc.getOrElse(""))) + ) + + case AvroF.TEnum(_, _, _, _, _) => true + case AvroF.TUnion(_) => true + case AvroF.TFixed(_, _, _, _) => true + } } diff --git a/src/test/scala/higherkindness/skeuomorph/avro/AvroSpec.scala b/src/test/scala/higherkindness/skeuomorph/avro/AvroSpec.scala deleted file mode 100644 index 9c26e6b7..00000000 --- a/src/test/scala/higherkindness/skeuomorph/avro/AvroSpec.scala +++ /dev/null @@ -1,141 +0,0 @@ -/* - * Copyright 2018-2020 47 Degrees Open Source - * - * 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 higherkindness.skeuomorph.avro - -import higherkindness.skeuomorph.instances._ - -import org.apache.avro.Schema -import org.apache.avro.compiler.idl._ -import org.scalacheck._ -import org.specs2._ -import higherkindness.skeuomorph.mu.{Protocol => MuProtocol} -import higherkindness.skeuomorph.mu.CompressionType -import higherkindness.skeuomorph.mu.codegen -import higherkindness.droste._ -import higherkindness.droste.data.Mu -import scala.meta._ -import scala.meta.contrib._ - -import scala.jdk.CollectionConverters._ - -class AvroSpec extends Specification with ScalaCheck { - - def is = s2""" - Avro Schema - - It should be possible to create a Schema from org.apache.avro.Schema. $convertSchema - - It should be possible to create a Protocol from org.apache.avro.Protocol and then generate Scala code from it. $convertAndPrintProtocol - """ - - def convertSchema = - Prop.forAll { (schema: Schema) => - val test = scheme.hylo(checkSchema(schema), AvroF.fromAvro) - - test(schema) - } - - def checkSchema(sch: Schema): Algebra[AvroF, Boolean] = - Algebra { - case AvroF.TNull() => sch.getType should_== Schema.Type.NULL - case AvroF.TBoolean() => sch.getType should_== Schema.Type.BOOLEAN - case AvroF.TInt() => sch.getType should_== Schema.Type.INT - case AvroF.TLong() => sch.getType should_== Schema.Type.LONG - case AvroF.TFloat() => sch.getType should_== Schema.Type.FLOAT - case AvroF.TDouble() => sch.getType should_== Schema.Type.DOUBLE - case AvroF.TBytes() => sch.getType should_== Schema.Type.BYTES - case AvroF.TString() => sch.getType should_== Schema.Type.STRING - - case AvroF.TNamedType(_, _) => false - case AvroF.TArray(_) => sch.getType should_== Schema.Type.ARRAY - case AvroF.TMap(_) => sch.getType should_== Schema.Type.MAP - case AvroF.TRecord(name, namespace, _, doc, fields) => - (sch.getName should_== name) - .and(sch.getNamespace should_== namespace.getOrElse("")) - .and(sch.getDoc should_== doc.getOrElse("")) - .and( - sch.getFields.asScala.toList.map(f => (f.name, f.doc)) should_== fields - .map(f => (f.name, f.doc.getOrElse(""))) - ) - - case AvroF.TEnum(_, _, _, _, _) => true - case AvroF.TUnion(_) => true - case AvroF.TFixed(_, _, _, _) => true - } - - def convertAndPrintProtocol = { - val idl = new Idl(getClass.getClassLoader.getResourceAsStream("avro/GreeterService.avdl")) - val avroProto = idl.CompilationUnit() - - val skeuoAvroProto = Protocol.fromProto[Mu[AvroF]](avroProto) - - val muProto = MuProtocol.fromAvroProtocol(CompressionType.Identity, useIdiomaticEndpoints = true)(skeuoAvroProto) - - val streamCtor: (Type, Type) => Type.Apply = { case (f: Type, a: Type) => - t"Stream[$f, $a]" - } - - val actual = codegen.protocol(muProto, streamCtor).right.get - - val expected = codegenExpectation(CompressionType.Identity, useIdiomaticEndpoints = true) - .parse[Source] - .get - .children - .head - .asInstanceOf[Pkg] - - actual.isEqual(expected) :| s""" - |Actual output: - |$actual - | - | - |Expected output: - |$expected" - """.stripMargin - } - - // TODO test for more complex schemas, importing other files, etc. - - def codegenExpectation(compressionType: CompressionType, useIdiomaticEndpoints: Boolean): String = { - - val serviceParams: String = "Avro" + - (if (compressionType == CompressionType.Gzip) ", Gzip" else ", Identity") + - (if (useIdiomaticEndpoints) ", namespace = Some(\"foo.bar\"), methodNameStyle = Capitalize" else "") - - s"""package foo.bar - | - |import _root_.higherkindness.mu.rpc.protocol._ - | - |final case class HelloRequest( - | arg1: _root_.java.lang.String, - | arg2: _root_.scala.Option[_root_.java.lang.String], - | arg3: _root_.scala.List[_root_.java.lang.String] - |) - |final case class HelloResponse( - | arg1: _root_.java.lang.String, - | arg2: _root_.scala.Option[_root_.java.lang.String], - | arg3: _root_.scala.List[_root_.java.lang.String] - |) - | - |@service($serviceParams) trait MyGreeterService[F[_]] { - | def sayHelloAvro(req: _root_.foo.bar.HelloRequest): F[_root_.foo.bar.HelloResponse] - | def sayNothingAvro(req: _root_.higherkindness.mu.rpc.protocol.Empty.type): F[_root_.higherkindness.mu.rpc.protocol.Empty.type] - |} - """.stripMargin - } - -} diff --git a/src/test/scala/higherkindness/skeuomorph/protobuf/ProtoSchemaSpec.scala b/src/test/scala/higherkindness/skeuomorph/protobuf/ProtobufCatsLawsSpec.scala similarity index 93% rename from src/test/scala/higherkindness/skeuomorph/protobuf/ProtoSchemaSpec.scala rename to src/test/scala/higherkindness/skeuomorph/protobuf/ProtobufCatsLawsSpec.scala index 9e604398..74fc7b84 100644 --- a/src/test/scala/higherkindness/skeuomorph/protobuf/ProtoSchemaSpec.scala +++ b/src/test/scala/higherkindness/skeuomorph/protobuf/ProtobufCatsLawsSpec.scala @@ -22,7 +22,7 @@ import cats.laws.discipline.{FoldableTests, FunctorTests, TraverseTests} import cats.implicits._ import org.specs2._ -class ProtoSchemaSpec extends Specification with ScalaCheck with Discipline { +class ProtobufCatsLawsSpec extends Specification with ScalaCheck with Discipline { def is = s2""" $traverse diff --git a/src/test/scala/higherkindness/skeuomorph/protobuf/ProtobufProtocolSpec.scala b/src/test/scala/higherkindness/skeuomorph/protobuf/ProtobufProtocolSpec.scala index 397262e4..d7bdd4a8 100644 --- a/src/test/scala/higherkindness/skeuomorph/protobuf/ProtobufProtocolSpec.scala +++ b/src/test/scala/higherkindness/skeuomorph/protobuf/ProtobufProtocolSpec.scala @@ -17,25 +17,24 @@ package higherkindness.skeuomorph.protobuf import cats.effect.IO -import higherkindness.skeuomorph.mu.MuF -import higherkindness.skeuomorph.protobuf.ProtobufF._ -import higherkindness.skeuomorph.protobuf.ParseProto._ -import higherkindness.skeuomorph.mu.CompressionType -import org.scalacheck.{Arbitrary, Gen} -import org.specs2.{ScalaCheck, Specification} import higherkindness.droste.data.Mu import higherkindness.droste.data.Mu._ - +import higherkindness.skeuomorph._ +import higherkindness.skeuomorph.mu._ +import higherkindness.skeuomorph.protobuf.ParseProto._ +import higherkindness.skeuomorph.protobuf.ProtobufF._ +import org.scalacheck._ +import org.specs2._ import scala.meta._ import scala.meta.contrib._ class ProtobufProtocolSpec extends Specification with ScalaCheck { val workingDirectory: String = new java.io.File(".").getCanonicalPath - val testDirectory = "/src/test/scala/higherkindness/skeuomorph/protobuf" + val testDirectory = "/src/test/resources/protobuf" val importRoot: Option[String] = Some(workingDirectory + testDirectory) - val bookProtocol: Protocol[Mu[ProtobufF]] = { + val bookProtocol: protobuf.Protocol[Mu[ProtobufF]] = { val path = workingDirectory + s"$testDirectory/service" val source = ProtoSource(s"book.proto", path, importRoot) parseProto[IO, Mu[ProtobufF]].parse(source).unsafeRunSync() @@ -63,24 +62,28 @@ class ProtobufProtocolSpec extends Specification with ScalaCheck { The generated Scala code should use the filename as a package option when neither `package` nor `java_package` are present in a file. $codegenProtobufNoPackage """ - private def codegenProtobufProtocol = - prop { (ct: CompressionType, useIdiom: Boolean) => - val toMuProtocol: Protocol[Mu[ProtobufF]] => higherkindness.skeuomorph.mu.Protocol[Mu[MuF]] = { - p: Protocol[Mu[ProtobufF]] => higherkindness.skeuomorph.mu.Protocol.fromProtobufProto(ct, useIdiom)(p) + def codegenProtobufProtocol = + prop { (compressionType: CompressionType, useIdiomaticEndpoints: Boolean) => + val toMuProtocol: protobuf.Protocol[Mu[ProtobufF]] => mu.Protocol[Mu[MuF]] = { p => + mu.Protocol.fromProtobufProto(compressionType, useIdiomaticEndpoints)(p) } val streamCtor: (Type, Type) => Type.Apply = { case (f: Type, a: Type) => t"Stream[$f, $a]" } - val codegen: higherkindness.skeuomorph.mu.Protocol[Mu[MuF]] => Pkg = { - p: higherkindness.skeuomorph.mu.Protocol[Mu[MuF]] => - higherkindness.skeuomorph.mu.codegen.protocol(p, streamCtor).right.get + val codegen: mu.Protocol[Mu[MuF]] => Pkg = { p => + mu.codegen.protocol(p, streamCtor).right.get } val actual = (toMuProtocol andThen codegen)(bookProtocol) - val expected = bookExpectation(ct, useIdiom).parse[Source].get.children.head.asInstanceOf[Pkg] + val expected = codegenExpectation(compressionType, Some("com.acme"), useIdiomaticEndpoints) + .parse[Source] + .get + .children + .head + .asInstanceOf[Pkg] actual.isEqual(expected) :| s""" |Actual output: @@ -92,11 +95,17 @@ class ProtobufProtocolSpec extends Specification with ScalaCheck { """.stripMargin } - private def bookExpectation(compressionType: CompressionType, useIdiomaticEndpoints: Boolean): String = { + def codegenExpectation( + compressionType: CompressionType, + namespace: Option[String], + useIdiomaticEndpoints: Boolean + ): String = { - val serviceParams: String = "Protobuf" + - (if (compressionType == CompressionType.Gzip) ", Gzip" else ", Identity") + - (if (useIdiomaticEndpoints) ", namespace = Some(\"com.acme\"), methodNameStyle = Capitalize" else "") + val serviceParams: String = Seq( + SerializationType.Protobuf, + s"compressionType = $compressionType", + s"namespace = ${if (useIdiomaticEndpoints) namespace.map("\"" + _ + "\"") else None}" + ).mkString(", ") s"""package com.acme | @@ -181,20 +190,17 @@ class ProtobufProtocolSpec extends Specification with ScalaCheck { |}""".stripMargin } - private def check(protobufProtocol: Protocol[Mu[ProtobufF]], expectedOutput: String) = { - val toMuProtocol: Protocol[Mu[ProtobufF]] => higherkindness.skeuomorph.mu.Protocol[Mu[MuF]] = { - p: Protocol[Mu[ProtobufF]] => - higherkindness.skeuomorph.mu.Protocol - .fromProtobufProto(CompressionType.Identity, useIdiomaticEndpoints = true)(p) + private def check(protobufProtocol: protobuf.Protocol[Mu[ProtobufF]], expectedOutput: String) = { + val toMuProtocol: protobuf.Protocol[Mu[ProtobufF]] => mu.Protocol[Mu[MuF]] = { p => + mu.Protocol.fromProtobufProto(CompressionType.Identity)(p) } val streamCtor: (Type, Type) => Type.Apply = { case (f: Type, a: Type) => t"Stream[$f, $a]" } - val codegen: higherkindness.skeuomorph.mu.Protocol[Mu[MuF]] => Pkg = { - p: higherkindness.skeuomorph.mu.Protocol[Mu[MuF]] => - higherkindness.skeuomorph.mu.codegen.protocol(p, streamCtor).right.get + val codegen: mu.Protocol[Mu[MuF]] => Pkg = { p => + mu.codegen.protocol(p, streamCtor).right.get } val actual = (toMuProtocol andThen codegen)(protobufProtocol) @@ -212,7 +218,7 @@ class ProtobufProtocolSpec extends Specification with ScalaCheck { } private def codegenOpencensus = { - val opencensusProtocol: Protocol[Mu[ProtobufF]] = { + val opencensusProtocol: protobuf.Protocol[Mu[ProtobufF]] = { val path = workingDirectory + s"$testDirectory/models/opencensus" val source = ProtoSource(s"trace.proto", path, importRoot) parseProto[IO, Mu[ProtobufF]].parse(source).unsafeRunSync() @@ -348,7 +354,7 @@ class ProtobufProtocolSpec extends Specification with ScalaCheck { |}""".stripMargin def codegenTaggedIntegers = { - val integerTypesProtocol: Protocol[Mu[ProtobufF]] = { + val integerTypesProtocol: protobuf.Protocol[Mu[ProtobufF]] = { val path = workingDirectory + s"$testDirectory/models" val source = ProtoSource(s"integer_types.proto", path, importRoot) parseProto[IO, Mu[ProtobufF]].parse(source).unsafeRunSync() @@ -379,7 +385,7 @@ class ProtobufProtocolSpec extends Specification with ScalaCheck { |""".stripMargin private def codegenGoogleApi = { - val googleApiProtocol: Protocol[Mu[ProtobufF]] = { + val googleApiProtocol: protobuf.Protocol[Mu[ProtobufF]] = { val path = workingDirectory + s"$testDirectory/models/type" val source = ProtoSource(s"date.proto", path, importRoot) parseProto[IO, Mu[ProtobufF]].parse(source).unsafeRunSync() @@ -395,7 +401,7 @@ class ProtobufProtocolSpec extends Specification with ScalaCheck { |""".stripMargin private def codeGenProtobufOnlyJavaPackage = { - val optionalPackage: Protocol[Mu[ProtobufF]] = { + val optionalPackage: protobuf.Protocol[Mu[ProtobufF]] = { val path = workingDirectory + s"$testDirectory/packages" val source = ProtoSource(s"test_only_java_package.proto", path, importRoot) parseProto[IO, Mu[ProtobufF]].parse(source).unsafeRunSync() @@ -411,12 +417,14 @@ class ProtobufProtocolSpec extends Specification with ScalaCheck { |object test_only_java_package { | final case class MyRequest(@_root_.pbdirect.pbIndex(1) value: _root_.java.lang.String) | final case class MyResponse(@_root_.pbdirect.pbIndex(1) value: _root_.java.lang.String) - | @service(Protobuf, Identity, namespace = Some("my_package"), methodNameStyle = Capitalize) trait MyService[F[_]] { def Check(req: _root_.my_package.test_only_java_package.MyRequest): F[_root_.my_package.test_only_java_package.MyResponse] } + | @service(Protobuf, compressionType = Identity, namespace = Some("my_package")) trait MyService[F[_]] { + | def Check(req: _root_.my_package.test_only_java_package.MyRequest): F[_root_.my_package.test_only_java_package.MyResponse] + | } |} |""".stripMargin private def codegenProtobufNoPackage = { - val optionalPackage: Protocol[Mu[ProtobufF]] = { + val optionalPackage: protobuf.Protocol[Mu[ProtobufF]] = { val path = workingDirectory + s"$testDirectory/packages" val source = ProtoSource(s"test_no_package.proto", path, importRoot) parseProto[IO, Mu[ProtobufF]].parse(source).unsafeRunSync() @@ -432,12 +440,14 @@ class ProtobufProtocolSpec extends Specification with ScalaCheck { |object test_no_package { | final case class MyRequest(@_root_.pbdirect.pbIndex(1) value: _root_.java.lang.String) | final case class MyResponse(@_root_.pbdirect.pbIndex(1) value: _root_.java.lang.String) - | @service(Protobuf, Identity, namespace = Some("test_no_package"), methodNameStyle = Capitalize) trait MyService[F[_]] { def Check(req: _root_.test_no_package.test_no_package.MyRequest): F[_root_.test_no_package.test_no_package.MyResponse] } + | @service(Protobuf, compressionType = Identity, namespace = Some("test_no_package")) trait MyService[F[_]] { + | def Check(req: _root_.test_no_package.test_no_package.MyRequest): F[_root_.test_no_package.test_no_package.MyResponse] + | } |} |""".stripMargin private def codeGenProtobufJavaPackage = { - val javaPackageAndRegularPackage: Protocol[Mu[ProtobufF]] = { + val javaPackageAndRegularPackage: protobuf.Protocol[Mu[ProtobufF]] = { val path = workingDirectory + s"$testDirectory/packages" val source = ProtoSource(s"test_java_package.proto", path, importRoot) parseProto[IO, Mu[ProtobufF]].parse(source).unsafeRunSync() @@ -453,7 +463,9 @@ class ProtobufProtocolSpec extends Specification with ScalaCheck { |object test_java_package { | final case class MyRequest(@_root_.pbdirect.pbIndex(1) value: _root_.java.lang.String) | final case class MyResponse(@_root_.pbdirect.pbIndex(1) value: _root_.java.lang.String) - | @service(Protobuf, Identity, namespace = Some("my_package"), methodNameStyle = Capitalize) trait MyService[F[_]] { def Check(req: _root_.my_package.test_java_package.MyRequest): F[_root_.my_package.test_java_package.MyResponse] } + | @service(Protobuf, compressionType = Identity, namespace = Some("my_package")) trait MyService[F[_]] { + | def Check(req: _root_.my_package.test_java_package.MyRequest): F[_root_.my_package.test_java_package.MyResponse] + | } |} |""".stripMargin