diff --git a/.gitignore b/.gitignore index 000e39be24..dfb87cca6d 100644 --- a/.gitignore +++ b/.gitignore @@ -16,7 +16,7 @@ !.idea/misc.xml # Gradle interim configs -.gradle/** +**/.gradle/** # Generated source code **/generated/** @@ -77,5 +77,7 @@ pubspec.lock **/*.pbjson.dart **/types.dart **/validators.dart +**/*.proto.dart +**/*.g.dart **/spine-dev.json diff --git a/.travis.yml b/.travis.yml index ec7c69ba07..414abea1db 100644 --- a/.travis.yml +++ b/.travis.yml @@ -35,7 +35,7 @@ script: deploy: skip_cleanup: false provider: script - script: bash config/publish-artifacts.sh + script: bash config/scripts/publish-artifacts.sh on: branch: master condition: $TRAVIS_PULL_REQUEST == "false" diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000000..2185ef6d5e --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,37 @@ +How to contribute +================== +Thank you for wanting to contribute to Spine. The following links will help you get started: + * [Wiki home][wiki-home] — the home of the framework developer's documentation. + * [Getting started with Spine in Java][quick-start] — this guide will walk you through + a minimal client-server “Hello World!” application in Java. + * [Introduction][docs-intro] — this section of the Spine Documentation will help you understand + the foundation of the framework. + +Pull requests +------------- +The work on an improvement starts with creating an issue that describes a bug or a feature. The issue will be used for communications on the proposed improvements. +If code changes are going to be introduced, the issue should also have a link to the corresponding Pull Request. + +Code contributions should: + * Be accompanied by tests. + * Be licensed under the Apache v2.0 license with the appropriate copyright header for each file. + * Formatted according to the code style. See [Wiki home][wiki-home] for the links to + style guides of the programming languages used in the framework. + +Contributor License Agreement +----------------------------- +Contributions to the code of Spine Event Engine framework and its libraries must be accompanied by +Contributor License Agreement (CLA). + + * If you are an individual writing original source code and you're sure you own + the intellectual property, then you'll need to sign an individual CLA. + + * If you work for a company which wants you to contribute your work, + then an authorized person from your company will need to sign a corporate CLA. + +Please [contact us][legal-email] for arranging the paper formalities. + +[wiki-home]: https://github.com/SpineEventEngine/SpineEventEngine.github.io/wiki +[quick-start]: https://spine.io/docs/quick-start +[docs-intro]: https://spine.io/docs/introduction +[legal-email]: mailto:legal@teamdev.com diff --git a/build.gradle.kts b/build.gradle.kts index fa481aeed5..57e59fa8ed 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,5 +1,5 @@ /* - * Copyright 2019, TeamDev. All rights reserved. + * Copyright 2020, TeamDev. All rights reserved. * * Redistribution and use in source and/or binary forms, with or without * modification, must retain the above copyright notice and the following diff --git a/buildSrc/src/main/kotlin/codegen.gradle.kts b/buildSrc/src/main/kotlin/codegen.gradle.kts index d4b9cd9ed0..37e11d62f1 100644 --- a/buildSrc/src/main/kotlin/codegen.gradle.kts +++ b/buildSrc/src/main/kotlin/codegen.gradle.kts @@ -39,14 +39,24 @@ if (!file(command).exists()) { logger.warn("Cannot locate `dart_code_gen` under `$command`.") } -fun composeCommandLine(descriptor: File, targetDir: String, standardTypesPackage: String) = - listOf( - command, - "--descriptor", "${file(descriptor)}", - "--destination", "$targetDir/types.dart", - "--standard-types", standardTypesPackage, - "--import-prefix", "." - ) +fun composeCommandLine(descriptor: File, + targetDir: String, + standardTypesPackage: String, + generateImmutableTypes: Boolean): List { + val args = mutableListOf( + command, + "--descriptor", descriptor.path, + "--destination", "$targetDir/types.dart", + "--standard-types", standardTypesPackage, + "--import-prefix", "." + ) + if (generateImmutableTypes) { + args.add("--immutable-types") + args.add(targetDir) + } + return args +} + /** * Task which launches Dart code generation from Protobuf. @@ -59,19 +69,36 @@ open class GenerateDart : Exec() { var target: String = "" @Internal var standardTypesPackage: String = "" + @Internal + var generateImmutableTypes: Boolean = true } -tasks.create("generateDart", GenerateDart::class) { +val pub = "pub" + if (Os.isFamily(Os.FAMILY_WINDOWS)) ".bat" else "" + +val pubGet by tasks.creating(Exec::class) { + commandLine(pub, "get") +} + +val generateDart by tasks.creating(GenerateDart::class) { @Suppress("UNCHECKED_CAST") descriptor = project.extensions["protoDart"].withGroovyBuilder { getProperty("mainDescriptorSet") } as Property target = "$projectDir/lib" standardTypesPackage = "spine_client" } +val launchBuilder by tasks.creating(Exec::class) { + commandLine(pub, "run", "build_runner", "build") + dependsOn(generateDart, pubGet) + generateDart.finalizedBy(this) +} + afterEvaluate { tasks.withType(GenerateDart::class) { inputs.file(descriptor) - commandLine(composeCommandLine(file(descriptor.get()), target, standardTypesPackage)) + commandLine(composeCommandLine(file(descriptor.get()), + target, + standardTypesPackage, + generateImmutableTypes)) dependsOn(":codegen:activateLocally") } } diff --git a/buildSrc/src/main/kotlin/io/spine/gradle/internal/CheckVersionIncrement.kt b/buildSrc/src/main/kotlin/io/spine/gradle/internal/CheckVersionIncrement.kt index 45e02672d6..8f33cde905 100644 --- a/buildSrc/src/main/kotlin/io/spine/gradle/internal/CheckVersionIncrement.kt +++ b/buildSrc/src/main/kotlin/io/spine/gradle/internal/CheckVersionIncrement.kt @@ -27,6 +27,7 @@ import org.gradle.api.GradleException import org.gradle.api.Project import org.gradle.api.tasks.Input import org.gradle.api.tasks.TaskAction +import java.io.FileNotFoundException import java.net.URL /** @@ -52,13 +53,13 @@ open class CheckVersionIncrement : DefaultTask() { val artifact = "${project.artifactPath()}/${MavenMetadata.FILE_NAME}" val repoUrl = repository.releases val metadata = fetch(repoUrl, artifact) - val versions = metadata.versioning.versions - val versionExists = versions.contains(version) + val versions = metadata?.versioning?.versions + val versionExists = versions?.contains(version) ?: false if (versionExists) { throw GradleException(""" Version `$version` is already published to maven repository `$repoUrl`. Try incrementing the library version. - All available versions are: ${versions.joinToString(separator = ", ")}. + All available versions are: ${versions?.joinToString(separator = ", ")}. To disable this check, run Gradle with `-x $name`. """.trimIndent() @@ -66,7 +67,7 @@ open class CheckVersionIncrement : DefaultTask() { } } - private fun fetch(repository: String, artifact: String): MavenMetadata { + private fun fetch(repository: String, artifact: String): MavenMetadata? { val url = URL("$repository/$artifact") return MavenMetadata.fetchAndParse(url) } @@ -94,9 +95,19 @@ private data class MavenMetadata(var versioning: Versioning = Versioning()) { mapper.configure(FAIL_ON_UNKNOWN_PROPERTIES, false) } - fun fetchAndParse(url: URL): MavenMetadata { - val metadata = mapper.readValue(url, MavenMetadata::class.java) - return metadata + /** + * Fetches the metadata for the repository and parses the document. + * + *

If the document could not be found, assumes that the module was never + * released and thus has no metadata. + */ + fun fetchAndParse(url: URL): MavenMetadata? { + return try { + val metadata = mapper.readValue(url, MavenMetadata::class.java) + metadata + } catch (e: FileNotFoundException) { + null + } } } } diff --git a/buildSrc/src/main/kotlin/io/spine/gradle/internal/deps.kt b/buildSrc/src/main/kotlin/io/spine/gradle/internal/deps.kt index 6846093b1f..3e9250e751 100644 --- a/buildSrc/src/main/kotlin/io/spine/gradle/internal/deps.kt +++ b/buildSrc/src/main/kotlin/io/spine/gradle/internal/deps.kt @@ -70,8 +70,8 @@ object Versions { val checkerFramework = "3.3.0" val errorProne = "2.3.4" val errorProneJavac = "9+181-r4173-1" // taken from here: https://github.com/tbroyer/gradle-errorprone-plugin/blob/v0.8/build.gradle.kts - val errorPronePlugin = "1.1.1" - val pmd = "6.20.0" + val errorPronePlugin = "1.2.1" + val pmd = "6.24.0" val checkstyle = "8.29" val protobufPlugin = "0.8.12" val appengineApi = "1.9.79" @@ -94,14 +94,19 @@ object Versions { val javaPoet = "1.12.1" val autoService = "1.0-rc6" val autoCommon = "0.10" - val jackson = "2.9.10.4" + val jackson = "2.9.10.5" val animalSniffer = "1.18" val apiguardian = "1.1.0" + val javaxAnnotation = "1.3.2" + val klaxon = "5.4" + val ouathJwt = "3.10.3" + val bouncyCastlePkcs = "1.66" + val assertK = "0.22" /** * Version of the SLF4J library. * - * Spine used to log with SLF4J. Now we use Flogger. Whenever a coice comes up, we recommend to + * Spine used to log with SLF4J. Now we use Flogger. Whenever a choice comes up, we recommend to * use the latter. * * Some third-party libraries may clash with different versions of the library. Thus, we specify @@ -158,12 +163,20 @@ object Build { object AutoService { val annotations = "com.google.auto.service:auto-service-annotations:${Versions.autoService}" - val processor = "com.google.auto.service:auto-service:${Versions.autoService}" + val processor = "com.google.auto.service:auto-service:${Versions.autoService}" } } object Gen { - val javaPoet = "com.squareup:javapoet:${Versions.javaPoet}" + val javaPoet = "com.squareup:javapoet:${Versions.javaPoet}" + val javaxAnnotation = "javax.annotation:javax.annotation-api:${Versions.javaxAnnotation}" +} + +object Publishing { + val klaxon = "com.beust:klaxon:${Versions.klaxon}" + val oauthJwt = "com.auth0:java-jwt:${Versions.ouathJwt}" + val bouncyCastlePkcs = "org.bouncycastle:bcpkix-jdk15on:${Versions.bouncyCastlePkcs}" + val assertK = "com.willowtreeapps.assertk:assertk-jvm:${Versions.assertK}" } object Grpc { @@ -227,7 +240,7 @@ object Test { "com.google.truth.extensions:truth-proto-extension:${Versions.truth}" ) @Deprecated("Use Flogger over SLF4J.", - replaceWith = ReplaceWith("Deps.runtime.floggerSystemBackend")) + replaceWith = ReplaceWith("Deps.runtime.floggerSystemBackend")) @Suppress("DEPRECATION") // Version of SLF4J. val slf4j = "org.slf4j:slf4j-jdk14:${Versions.slf4j}" } @@ -273,6 +286,7 @@ object Deps { val test = Test val versions = Versions val scripts = Scripts + val publishing = Publishing } object DependencyResolution { diff --git a/client/build.gradle.kts b/client/build.gradle.kts index 594b121094..b90b9d4e11 100644 --- a/client/build.gradle.kts +++ b/client/build.gradle.kts @@ -1,5 +1,5 @@ /* - * Copyright 2019, TeamDev. All rights reserved. + * Copyright 2020, TeamDev. All rights reserved. * * Redistribution and use in source and/or binary forms, with or without * modification, must retain the above copyright notice and the following diff --git a/client/build.yaml b/client/build.yaml new file mode 100644 index 0000000000..6b83f55cb9 --- /dev/null +++ b/client/build.yaml @@ -0,0 +1,14 @@ +targets: + $default: + builders: + "built_value": + +builders: + built_value: + target: ":client" + import: "package:built_value_generator/builder.dart" + builder_factories: + - "builtValue" + build_extensions: + ".proto.dart": [".g.dart"] + build_to: "source" diff --git a/client/lib/actor_request_factory.dart b/client/lib/actor_request_factory.dart index 4dec4b9ef5..d1616dc678 100644 --- a/client/lib/actor_request_factory.dart +++ b/client/lib/actor_request_factory.dart @@ -1,5 +1,5 @@ /* - * Copyright 2019, TeamDev. All rights reserved. + * Copyright 2020, TeamDev. All rights reserved. * * Redistribution and use in source and/or binary forms, with or without * modification, must retain the above copyright notice and the following @@ -61,14 +61,13 @@ class ActorRequestFactory { } ActorContext _context() { - var ctx = ActorContext(); - ctx + return ActorContext((b) => b ..actor = this.actor ..timestamp = time.now() - ..tenantId = this.tenant ?? TenantId.getDefault() + ..tenantId = this.tenant ?? TenantId.defaultInstance ..zoneOffset = zoneOffset ?? time.zoneOffset() - ..zoneId = zoneId ?? time.guessZoneId(); - return ctx; + ..zoneId = zoneId ?? time.guessZoneId() + ); } } diff --git a/client/lib/backend_client.dart b/client/lib/backend_client.dart index 9afda994fb..514cd111eb 100644 --- a/client/lib/backend_client.dart +++ b/client/lib/backend_client.dart @@ -1,5 +1,5 @@ /* - * Copyright 2019, TeamDev. All rights reserved. + * Copyright 2020, TeamDev. All rights reserved. * * Redistribution and use in source and/or binary forms, with or without * modification, must retain the above copyright notice and the following @@ -23,6 +23,7 @@ import 'dart:async'; import 'package:http/http.dart' as http; import 'package:protobuf/protobuf.dart'; import 'package:spine_client/firebase_client.dart'; +import 'package:spine_client/message.dart'; import 'package:spine_client/src/http_endpoint.dart'; import 'package:spine_client/spine/client/query.pb.dart'; import 'package:spine_client/spine/client/subscription.pb.dart' as pb; @@ -153,27 +154,18 @@ class BackendClient { return subscription; } - Ack _parseAck(http.Response response) { - var ack = Ack(); - _parseInto(ack, response); - return ack; - } + Ack _parseAck(http.Response response) => + _parse(Ack.defaultInstance, response); - FirebaseQueryResponse _parseQueryResponse(http.Response response) { - var queryResponse = FirebaseQueryResponse(); - _parseInto(queryResponse, response); - return queryResponse; - } + FirebaseQueryResponse _parseQueryResponse(http.Response response) => + _parse(FirebaseQueryResponse.defaultInstance, response); - FirebaseSubscription _parseFirebaseSubscription(http.Response response) { - var firebaseSubscription = FirebaseSubscription(); - _parseInto(firebaseSubscription, response); - return firebaseSubscription; - } + FirebaseSubscription _parseFirebaseSubscription(http.Response response) => + _parse(FirebaseSubscription.defaultInstance, response); - void _parseInto(GeneratedMessage message, http.Response response) { + M _parse, P extends GeneratedMessage>(M defaultInstance, http.Response response) { var json = response.body; - parseInto(message, json); + return parseAs(defaultInstance, json); } void _keepUpSubscriptions() { diff --git a/client/lib/command_factory.dart b/client/lib/command_factory.dart index 7a3824f9e3..92487e4dfb 100644 --- a/client/lib/command_factory.dart +++ b/client/lib/command_factory.dart @@ -1,5 +1,5 @@ /* - * Copyright 2019, TeamDev. All rights reserved. + * Copyright 2020, TeamDev. All rights reserved. * * Redistribution and use in source and/or binary forms, with or without * modification, must retain the above copyright notice and the following @@ -40,18 +40,18 @@ class CommandFactory { ..id = _newId() ..message = pack(message) ..context = _buildContext(); - return cmd; + return cmd.freeze(); } CommandContext _buildContext() { var ctx = CommandContext(); ctx.actorContext = _context(); - return ctx; + return ctx.freeze(); } CommandId _newId() { var id = CommandId(); id.uuid = newUuid(); - return id; + return id.freeze(); } } diff --git a/client/lib/firebase_client.dart b/client/lib/firebase_client.dart index f243ec69d3..fd47881c06 100644 --- a/client/lib/firebase_client.dart +++ b/client/lib/firebase_client.dart @@ -1,5 +1,5 @@ /* - * Copyright 2019, TeamDev. All rights reserved. + * Copyright 2020, TeamDev. All rights reserved. * * Redistribution and use in source and/or binary forms, with or without * modification, must retain the above copyright notice and the following diff --git a/client/lib/message.dart b/client/lib/message.dart new file mode 100644 index 0000000000..b3e91c68c2 --- /dev/null +++ b/client/lib/message.dart @@ -0,0 +1,55 @@ +/* + * Copyright 2020, TeamDev. All rights reserved. + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import 'dart:typed_data'; + +import 'package:protobuf/protobuf.dart'; +import 'package:spine_client/src/known_types.dart'; +import 'package:spine_client/src/validate.dart'; + +abstract class Message, M extends GeneratedMessage> { + + M getAsMutable() { + return null; + } + + String getTypeUrl() { + return theKnownTypes.typeUrlOf(getAsMutable()); + } + + Uint8List writeToBuffer() => getAsMutable().writeToBuffer(); +} + +abstract class ValidatingBuilder, M extends GeneratedMessage> { + + M mutableMessage() => build().getAsMutable(); + + void validate() { + var msg = mutableMessage(); + checkValid(msg); + } + + T build(); + + T vBuild() { + validate(); + return build(); + } +} diff --git a/client/lib/query_factory.dart b/client/lib/query_factory.dart index 1bdea2df0e..48748de12e 100644 --- a/client/lib/query_factory.dart +++ b/client/lib/query_factory.dart @@ -1,5 +1,5 @@ /* - * Copyright 2019, TeamDev. All rights reserved. + * Copyright 2020, TeamDev. All rights reserved. * * Redistribution and use in source and/or binary forms, with or without * modification, must retain the above copyright notice and the following @@ -34,27 +34,29 @@ class QueryFactory { /// Creates a query which matches all entities of the given type with the given IDs. Query byIds(GeneratedMessage instance, List ids) { + instance.freeze(); var query = Query(); query ..id = _newId() ..target = targetByIds(instance, ids) ..context = _context(); - return query; + return query.freeze(); } /// Creates a query which matches all entities of the given type. Query all(GeneratedMessage instance) { + instance.freeze(); var query = Query(); query ..id = _newId() ..target = targetAll(instance) ..context = _context(); - return query; + return query.freeze(); } QueryId _newId() { var id = QueryId(); id.value = newUuid(prefix: 'q-'); - return id; + return id.freeze(); } } diff --git a/client/lib/rest_firebase_client.dart b/client/lib/rest_firebase_client.dart index b083bd299a..228a132dc5 100644 --- a/client/lib/rest_firebase_client.dart +++ b/client/lib/rest_firebase_client.dart @@ -1,5 +1,5 @@ /* - * Copyright 2019, TeamDev. All rights reserved. + * Copyright 2020, TeamDev. All rights reserved. * * Redistribution and use in source and/or binary forms, with or without * modification, must retain the above copyright notice and the following diff --git a/client/lib/spine_client.dart b/client/lib/spine_client.dart index 79c832e190..2a1becc47e 100644 --- a/client/lib/spine_client.dart +++ b/client/lib/spine_client.dart @@ -1,5 +1,5 @@ /* - * Copyright 2019, TeamDev. All rights reserved. + * Copyright 2020, TeamDev. All rights reserved. * * Redistribution and use in source and/or binary forms, with or without * modification, must retain the above copyright notice and the following diff --git a/client/lib/src/any_packer.dart b/client/lib/src/any_packer.dart index c17a6e08e8..f535a56b51 100644 --- a/client/lib/src/any_packer.dart +++ b/client/lib/src/any_packer.dart @@ -1,5 +1,5 @@ /* - * Copyright 2019, TeamDev. All rights reserved. + * Copyright 2020, TeamDev. All rights reserved. * * Redistribution and use in source and/or binary forms, with or without * modification, must retain the above copyright notice and the following @@ -20,6 +20,7 @@ import 'package:protobuf/protobuf.dart'; import 'package:spine_client/google/protobuf/any.pb.dart'; +import 'package:spine_client/message.dart'; import 'package:spine_client/src/known_types.dart'; /// Separates the type URL prefix from the type name. @@ -30,14 +31,15 @@ const _prefixSeparator = '/'; /// The message type is inferred from the [Any.typeUrl] via the [KnownTypes]. If the type is /// unknown, an error is thrown. /// -GeneratedMessage unpack(Any any) { +Message unpack(Any any) { var typeUrl = any.typeUrl; var builder = theKnownTypes.findBuilderInfo(typeUrl); if (builder == null) { throw ArgumentError('Cannot unpack unknown type `$typeUrl`.'); } var emptyInstance = builder.createEmptyInstance(); - return any.unpackInto(emptyInstance); + var generatedMessage = any.getAsMutable().unpackInto(emptyInstance); + return theKnownTypes.fromMutable(generatedMessage); } /// Packs the given [message] into an [Any]. @@ -46,10 +48,12 @@ GeneratedMessage unpack(Any any) { /// thrown. /// Any pack(GeneratedMessage message) { - return Any.pack(message, typeUrlPrefix: _typeUrlPrefix(message)); + message.freeze(); + return Any.pack(message, typeUrlPrefix: _typeUrlPrefix(message)).freeze(); } String _typeUrlPrefix(GeneratedMessage message) { + message.freeze(); var typeUrl = theKnownTypes.typeUrlOf(message); if (typeUrl == null) { throw ArgumentError('Cannot pack message of unknown type `${message.runtimeType}`.'); diff --git a/client/lib/src/http_endpoint.dart b/client/lib/src/http_endpoint.dart index 40c2f9c577..db934c73fb 100644 --- a/client/lib/src/http_endpoint.dart +++ b/client/lib/src/http_endpoint.dart @@ -1,5 +1,5 @@ /* - * Copyright 2019, TeamDev. All rights reserved. + * Copyright 2020, TeamDev. All rights reserved. * * Redistribution and use in source and/or binary forms, with or without * modification, must retain the above copyright notice and the following @@ -21,7 +21,7 @@ import 'dart:convert'; import 'package:http/http.dart' as http; -import 'package:protobuf/protobuf.dart'; +import 'package:spine_client/message.dart'; import 'package:spine_client/src/url.dart'; const _base64 = Base64Codec(); @@ -39,7 +39,7 @@ class HttpEndpoint { /// /// The given [path] will be concatenated with the [_baseUrl]. /// - Future postMessage(String path, GeneratedMessage message) async { + Future postMessage(String path, Message message) async { var bytes = message.writeToBuffer(); var url = Url.from(_baseUrl, path).stringUrl; var response = await http.post(url, diff --git a/client/lib/src/json.dart b/client/lib/src/json.dart index 7c0f4e3d24..eafac6db9f 100644 --- a/client/lib/src/json.dart +++ b/client/lib/src/json.dart @@ -1,5 +1,5 @@ /* - * Copyright 2019, TeamDev. All rights reserved. + * Copyright 2020, TeamDev. All rights reserved. * * Redistribution and use in source and/or binary forms, with or without * modification, must retain the above copyright notice and the following @@ -21,17 +21,25 @@ import 'dart:convert'; import 'package:protobuf/protobuf.dart'; +import 'package:spine_client/message.dart'; import 'known_types.dart'; const _json = JsonCodec(); +M parseAs, P extends GeneratedMessage>(M defaultInstance, String json) { + P mutable = defaultInstance.getAsMutable(); + parseInto(mutable, json); + return theKnownTypes.fromMutable(mutable); +} + /// Parses the given JSON string into a message. void parseInto(GeneratedMessage message, String json) { var jsonMap = _json.decode(json); message.mergeFromProto3Json(jsonMap, ignoreUnknownFields: true, typeRegistry: theKnownTypes.registry()); + message.freeze(); } /// Parses the given JSON string into a new instance of the message described by the [builder]. diff --git a/client/lib/src/known_types.dart b/client/lib/src/known_types.dart index 20c4aef77f..f25d5e396d 100644 --- a/client/lib/src/known_types.dart +++ b/client/lib/src/known_types.dart @@ -1,5 +1,5 @@ /* - * Copyright 2019, TeamDev. All rights reserved. + * Copyright 2020, TeamDev. All rights reserved. * * Redistribution and use in source and/or binary forms, with or without * modification, must retain the above copyright notice and the following @@ -19,6 +19,7 @@ */ import 'package:protobuf/protobuf.dart'; +import 'package:spine_client/message.dart'; import 'package:spine_client/spine/validate/validation_error.pb.dart'; import 'package:spine_client/types.dart' as standardTypes; @@ -61,10 +62,14 @@ class KnownTypes { /// Obtains a validator function for the given message. ValidationError Function(GeneratedMessage) validatorFor(GeneratedMessage message) { - var typeUrl = typeUrlOf(message); + var typeUrl = typeUrlOf(message.freeze()); return _validators[typeUrl]; } + Message fromMutable, P extends GeneratedMessage>(P message) { + return null; // TODO:2020-09-21:dmytro.dashenkov: Implement. + } + /// Constructs a registry for JSON parsing. TypeRegistry registry() { return TypeRegistry(_msgToTypeUrl.keys); diff --git a/client/lib/src/url.dart b/client/lib/src/url.dart index afc8ad7087..9dd210c316 100644 --- a/client/lib/src/url.dart +++ b/client/lib/src/url.dart @@ -1,5 +1,5 @@ /* - * Copyright 2019, TeamDev. All rights reserved. + * Copyright 2020, TeamDev. All rights reserved. * * Redistribution and use in source and/or binary forms, with or without * modification, must retain the above copyright notice and the following diff --git a/client/lib/src/validate.dart b/client/lib/src/validate.dart index cce2fab41c..cf08a865fd 100644 --- a/client/lib/src/validate.dart +++ b/client/lib/src/validate.dart @@ -1,5 +1,5 @@ /* - * Copyright 2019, TeamDev. All rights reserved. + * Copyright 2020, TeamDev. All rights reserved. * * Redistribution and use in source and/or binary forms, with or without * modification, must retain the above copyright notice and the following @@ -18,6 +18,7 @@ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ +import 'package:built_collection/built_collection.dart'; import 'package:optional/optional.dart'; import 'package:protobuf/protobuf.dart'; import 'package:spine_client/spine/validate/validation_error.pb.dart'; @@ -30,6 +31,7 @@ import 'package:sprintf/sprintf.dart'; /// Optional validate(GeneratedMessage message) { ArgumentError.checkNotNull(message, 'message'); + message.freeze(); var validate = theKnownTypes.validatorFor(message); if (validate == null) { return Optional.empty(); @@ -44,6 +46,7 @@ Optional validate(GeneratedMessage message) { /// Validates the given message according to the constrains defined in Protobuf and throws /// an [InvalidMessageError] if the [message] is invalid. void checkValid(GeneratedMessage message) { + message.freeze(); var error = validate(message); error.ifPresent((e) => throw InvalidMessageError._(e)); } @@ -59,11 +62,9 @@ class InvalidMessageError extends Error { final ValidationError asValidationError; /// The constraint violations which caused the error. - List get violations => asValidationError.constraintViolation; + BuiltList get violations => asValidationError.constraintViolation; - InvalidMessageError._(this.asValidationError) { - asValidationError.freeze(); - } + InvalidMessageError._(this.asValidationError); @override String toString() => diff --git a/client/lib/subscription.dart b/client/lib/subscription.dart index 0cea0cc512..afe0ad7379 100644 --- a/client/lib/subscription.dart +++ b/client/lib/subscription.dart @@ -1,5 +1,5 @@ /* - * Copyright 2019, TeamDev. All rights reserved. + * Copyright 2020, TeamDev. All rights reserved. * * Redistribution and use in source and/or binary forms, with or without * modification, must retain the above copyright notice and the following @@ -52,11 +52,14 @@ class Subscription { : itemAdded = _checkIsBroadCast(itemAdded), itemChanged = _checkIsBroadCast(itemChanged), itemRemoved = _checkIsBroadCast(itemRemoved), - _closed = false; + _closed = false { + this.subscription.freeze(); + } /// Creates a new instance which broadcasts updates from under the given Firebase node. factory Subscription.of(FirebaseSubscription firebaseSubscription, FirebaseClient database) { + firebaseSubscription.freeze(); var subscription = firebaseSubscription.subscription; var typeUrl = subscription.topic.target.type; var builderInfo = theKnownTypes.findBuilderInfo(typeUrl); diff --git a/client/lib/target_builder.dart b/client/lib/target_builder.dart index fbbc834185..b76368ad05 100644 --- a/client/lib/target_builder.dart +++ b/client/lib/target_builder.dart @@ -1,5 +1,5 @@ /* - * Copyright 2019, TeamDev. All rights reserved. + * Copyright 2020, TeamDev. All rights reserved. * * Redistribution and use in source and/or binary forms, with or without * modification, must retain the above copyright notice and the following @@ -25,23 +25,34 @@ import 'package:spine_client/src/known_types.dart'; /// Creates a target which matches all messages of type. Target targetAll(GeneratedMessage instance) { + instance.freeze(); var target = Target(); target ..type = _typeUrl(instance) ..includeAll = true; - return target; + return target.freeze(); } /// Creates a target which matches messages with the given IDs. Target targetByIds(GeneratedMessage instance, List ids) { + instance.freeze(); + _freeze(ids); var target = Target(); target.type = _typeUrl(instance); var filters = TargetFilters(); var idFilter = IdFilter(); idFilter.id.addAll(ids); - filters.idFilter = idFilter; - target.filters = filters; - return target; + filters.idFilter = idFilter.freeze(); + target.filters = filters.freeze(); + return target.freeze(); +} + +_freeze(List ids) { + ids.forEach(_freezeAny); +} + +_freezeAny(Any any) { + any.freeze(); } String _typeUrl(GeneratedMessage message) { diff --git a/client/lib/time.dart b/client/lib/time.dart index 445d50fb86..4950d72f12 100644 --- a/client/lib/time.dart +++ b/client/lib/time.dart @@ -1,5 +1,5 @@ /* - * Copyright 2019, TeamDev. All rights reserved. + * Copyright 2020, TeamDev. All rights reserved. * * Redistribution and use in source and/or binary forms, with or without * modification, must retain the above copyright notice and the following @@ -23,15 +23,16 @@ import 'package:spine_client/spine/time/time.pb.dart'; /// Obtains a [Timestamp] with the current time. Timestamp now() { - return Timestamp.fromDateTime(DateTime.now()); + return Timestamp.from( + MutableTimestamp.fromDateTime(DateTime.now()) + ); } /// Obtains the current time zone offset. ZoneOffset zoneOffset() { var dateTime = DateTime.now(); var zoneOffset = dateTime.timeZoneOffset; - var offset = ZoneOffset(); - offset.amountSeconds = zoneOffset.inSeconds; + var offset = ZoneOffset((b) => b.amountSeconds = zoneOffset.inSeconds); return offset; } @@ -45,7 +46,6 @@ ZoneOffset zoneOffset() { ZoneId guessZoneId() { var dateTime = DateTime.now(); var zoneName = dateTime.timeZoneName; - var zoneId = ZoneId(); - zoneId.value = zoneName; + var zoneId = ZoneId((b) => b.value = zoneName); return zoneId; } diff --git a/client/lib/topic_factory.dart b/client/lib/topic_factory.dart index 657dc614ae..ce56189a91 100644 --- a/client/lib/topic_factory.dart +++ b/client/lib/topic_factory.dart @@ -1,5 +1,5 @@ /* - * Copyright 2019, TeamDev. All rights reserved. + * Copyright 2020, TeamDev. All rights reserved. * * Redistribution and use in source and/or binary forms, with or without * modification, must retain the above copyright notice and the following @@ -34,27 +34,29 @@ class TopicFactory { /// Creates a topic which matches entities of the given type with the specified IDs. Topic byIds(GeneratedMessage instance, List ids) { + instance.freeze(); var topic = Topic(); topic ..id = _newId() ..target = targetByIds(instance, ids) ..context = _context(); - return topic; + return topic.freeze(); } /// Creates a topic which matches all entities of the given type. Topic all(GeneratedMessage instance) { + instance.freeze(); var topic = Topic(); topic ..id = _newId() ..target = targetAll(instance) ..context = _context(); - return topic; + return topic.freeze(); } TopicId _newId() { var id = TopicId(); id.value = newUuid(prefix: 'q-'); - return id; + return id.freeze(); } } diff --git a/client/lib/uuids.dart b/client/lib/uuids.dart index 51b0be409e..38b9591c4a 100644 --- a/client/lib/uuids.dart +++ b/client/lib/uuids.dart @@ -1,5 +1,5 @@ /* - * Copyright 2019, TeamDev. All rights reserved. + * Copyright 2020, TeamDev. All rights reserved. * * Redistribution and use in source and/or binary forms, with or without * modification, must retain the above copyright notice and the following diff --git a/client/lib/web_firebase_client.dart b/client/lib/web_firebase_client.dart index 6fb69a14f1..309be66fb1 100644 --- a/client/lib/web_firebase_client.dart +++ b/client/lib/web_firebase_client.dart @@ -1,5 +1,5 @@ /* - * Copyright 2019, TeamDev. All rights reserved. + * Copyright 2020, TeamDev. All rights reserved. * * Redistribution and use in source and/or binary forms, with or without * modification, must retain the above copyright notice and the following @@ -63,7 +63,7 @@ class WebFirebaseClient implements FirebaseClient { .map(_toJsonString); } - String _toJsonString(event) { + String _toJsonString(fb.QueryEvent event) { return event.snapshot.toJson().toString(); } } diff --git a/client/pubspec.yaml b/client/pubspec.yaml index fc4f978cb2..996e427477 100644 --- a/client/pubspec.yaml +++ b/client/pubspec.yaml @@ -7,15 +7,17 @@ environment: sdk: '>=2.5.0 <3.0.0' dependencies: - protobuf: ^1.0.0 + protobuf: ^1.0.1 http: ^0.12.0+2 - uuid: ^2.0.2 - firebase: ^5.0.4 - fixnum: ^0.10.9 - sprintf: ^4.0.2 - optional: ^3.1.0 + uuid: ^2.2.2 + firebase: ^7.3.0 + fixnum: ^0.10.11 + sprintf: ^4.1.0 + optional: ^5.0.0 dev_dependencies: + build_runner: ^1.10.1 + built_value_generator: ^7.1.0 pedantic: ^1.7.0 test: ^1.6.0 - dartdoc: ^0.30.2 + dartdoc: ^0.32.3 diff --git a/client/test/actor_request_factory_test.dart b/client/test/actor_request_factory_test.dart index 7c6f010128..cbacc712d1 100644 --- a/client/test/actor_request_factory_test.dart +++ b/client/test/actor_request_factory_test.dart @@ -1,5 +1,5 @@ /* - * Copyright 2019, TeamDev. All rights reserved. + * Copyright 2020, TeamDev. All rights reserved. * * Redistribution and use in source and/or binary forms, with or without * modification, must retain the above copyright notice and the following diff --git a/client/test/any_packer_test.dart b/client/test/any_packer_test.dart index 5acc5241db..0fa0e9302b 100644 --- a/client/test/any_packer_test.dart +++ b/client/test/any_packer_test.dart @@ -1,5 +1,5 @@ /* - * Copyright 2019, TeamDev. All rights reserved. + * Copyright 2020, TeamDev. All rights reserved. * * Redistribution and use in source and/or binary forms, with or without * modification, must retain the above copyright notice and the following diff --git a/client/test/json_test.dart b/client/test/json_test.dart index 4faffc6543..9e39ed69fa 100644 --- a/client/test/json_test.dart +++ b/client/test/json_test.dart @@ -1,5 +1,5 @@ /* - * Copyright 2019, TeamDev. All rights reserved. + * Copyright 2020, TeamDev. All rights reserved. * * Redistribution and use in source and/or binary forms, with or without * modification, must retain the above copyright notice and the following diff --git a/client/test/time_test.dart b/client/test/time_test.dart index d0484d68c5..b0a4403ec9 100644 --- a/client/test/time_test.dart +++ b/client/test/time_test.dart @@ -1,5 +1,5 @@ /* - * Copyright 2019, TeamDev. All rights reserved. + * Copyright 2020, TeamDev. All rights reserved. * * Redistribution and use in source and/or binary forms, with or without * modification, must retain the above copyright notice and the following diff --git a/codegen/bin/main.dart b/codegen/bin/main.dart index ea3a8b661a..d8f07af7f9 100644 --- a/codegen/bin/main.dart +++ b/codegen/bin/main.dart @@ -1,5 +1,5 @@ /* - * Copyright 2019, TeamDev. All rights reserved. + * Copyright 2020, TeamDev. All rights reserved. * * Redistribution and use in source and/or binary forms, with or without * modification, must retain the above copyright notice and the following @@ -18,15 +18,19 @@ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ +import 'dart:convert'; import 'dart:io'; import 'package:args/args.dart'; import 'package:dart_code_gen/dart_code_gen.dart' as dart_code_gen; import 'package:dart_code_gen/google/protobuf/descriptor.pb.dart'; +import 'package:dart_code_gen/prebuilt_types.dart' as prebuilt; +import 'package:dart_code_gen/prebuilt_types.dart'; import 'package:dart_code_gen/spine/options.pb.dart'; import 'package:protobuf/protobuf.dart'; const String descriptorArgument = 'descriptor'; +const String immutableTypesArgument = 'immutable-types'; const String destinationArgument = 'destination'; const String stdPackageArgument = 'standard-types'; const String importPrefixArgument = 'import-prefix'; @@ -34,6 +38,9 @@ const String importPrefixArgument = 'import-prefix'; const String stdoutFlag = 'stdout'; const String helpFlag = 'help'; +final RegExp _importCore = RegExp('^import [\'"]dart:core[\'"] as \\\$core.+'); +final RegExp _coreImportPrefix = RegExp('\\\$core\.'); + /// Launches the Dart code generator. /// main(List arguments) { @@ -45,11 +52,12 @@ main(List arguments) { 'registries and validation code.'); stdout.writeln(parser.usage); } else { - _launch_code_gen(args); + _launch_validation_gen(args); + _launch_proto_gen(args); } } -void _launch_code_gen(ArgResults args) { +void _launch_validation_gen(ArgResults args) { var descriptorPath = _getRequired(args, descriptorArgument); var destinationPath = _getRequired(args, destinationArgument); var stdPackage = args[stdPackageArgument]; @@ -67,10 +75,71 @@ void _launch_code_gen(ArgResults args) { var dartCode = dart_code_gen.generate(properties); destinationFile.writeAsStringSync(dartCode, flush: true); if (shouldPrint) { - stdout.write(dartCode); + stdout.writeln(dartCode); + } +} + +void _launch_proto_gen(ArgResults args) { + if (!args.options.contains(immutableTypesArgument)) { + return; + } + var path = args[immutableTypesArgument]; + var descriptorPath = _getRequired(args, descriptorArgument); + var descFile = File(descriptorPath); + _checkExists(descFile); + + var shouldPrint = args[stdoutFlag]; + FileDescriptorSet descriptors = _parseDescriptors(descFile); + var files = prebuilt.generate(descriptors); + for (var file in files) { + _process_file(path, file, descriptors, shouldPrint); + } +} + +void _process_file(path, PrebuiltFile file, FileDescriptorSet descriptors, shouldPrint) { + var destinationFile = File('${path}/${file.name}'); + _checkExists(destinationFile); + var generatedContent = destinationFile.readAsStringSync(); + for (var sub in file.substitutions.entries) { + var pattern = RegExp('\\b${sub.key}\\b'); + generatedContent = generatedContent.replaceAll(pattern, sub.value); + } + var newContent = generatedContent + file.additions; + var lines = LineSplitter().convert(newContent); + var sortedLines = _sortStatements(lines); + destinationFile.writeAsStringSync(sortedLines.join('\n'), flush: true); + if (shouldPrint) { + stdout.writeln(file.additions); } } +List _sortStatements(List codeLines) { + List imports = []; + List parts = []; + List otherCode = []; + for (var line in codeLines) { + if (line.startsWith('import') || line.startsWith('export')) { + if (!_importCore.hasMatch(line)) { + imports.add(line); + } + + } else if (line.startsWith('part')) { + parts.add(line); + } else { + otherCode.add(_cleanOfCorePrefix(line)); + } + } + return List() + ..addAll(imports) + ..add('') + ..addAll(parts) + ..addAll(otherCode); +} + +String _cleanOfCorePrefix(String line) { + return line.replaceAll(_coreImportPrefix, ''); +} + dynamic _getRequired(ArgResults args, String name) { var result = args[name]; if (result == null) { @@ -109,6 +178,8 @@ ArgParser _createParser() { var parser = ArgParser(); parser.addOption(descriptorArgument, help: 'Path to the file descriptor set file. This argument is required.'); + parser.addOption(immutableTypesArgument, + help: 'Path to the `lib/src` directory. This argument is required'); parser.addOption(destinationArgument, help: 'Path to the destination file. This argument is required.'); parser.addOption(stdPackageArgument, @@ -116,7 +187,7 @@ ArgParser _createParser() { 'and basic Spine types.', defaultsTo: 'spine_client'); parser.addOption(importPrefixArgument, - help: 'Path prefix for imports of types which are vaidated.', + help: 'Path prefix for imports of types which are validated.', defaultsTo: ''); parser.addFlag(stdoutFlag, defaultsTo: false, diff --git a/codegen/build.gradle.kts b/codegen/build.gradle.kts index aab2a02646..ff892d14fe 100644 --- a/codegen/build.gradle.kts +++ b/codegen/build.gradle.kts @@ -1,5 +1,5 @@ /* - * Copyright 2019, TeamDev. All rights reserved. + * Copyright 2020, TeamDev. All rights reserved. * * Redistribution and use in source and/or binary forms, with or without * modification, must retain the above copyright notice and the following diff --git a/codegen/build.yaml b/codegen/build.yaml new file mode 100644 index 0000000000..501d4a1441 --- /dev/null +++ b/codegen/build.yaml @@ -0,0 +1,14 @@ +targets: + $default: + builders: + "built_value": + +builders: + built_value: + target: ":codegen" + import: "package:built_value_generator/builder.dart" + builder_factories: + - "builtValue" + build_extensions: + ".proto.dart": [".g.dart"] + build_to: "source" diff --git a/codegen/lib/dart_code_gen.dart b/codegen/lib/dart_code_gen.dart index 3d2f5e3546..cac197c9e3 100644 --- a/codegen/lib/dart_code_gen.dart +++ b/codegen/lib/dart_code_gen.dart @@ -1,5 +1,5 @@ /* - * Copyright 2019, TeamDev. All rights reserved. + * Copyright 2020, TeamDev. All rights reserved. * * Redistribution and use in source and/or binary forms, with or without * modification, must retain the above copyright notice and the following diff --git a/codegen/lib/prebuilt_types.dart b/codegen/lib/prebuilt_types.dart new file mode 100644 index 0000000000..57b76bb375 --- /dev/null +++ b/codegen/lib/prebuilt_types.dart @@ -0,0 +1,99 @@ +/* + * Copyright 2020, TeamDev. All rights reserved. + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import 'package:code_builder/code_builder.dart'; +import 'package:dart_code_gen/src/immutable_type_factory.dart'; +import 'package:dart_style/dart_style.dart'; + +import 'google/protobuf/descriptor.pb.dart'; +import 'src/type.dart'; + +/// The comment inserted into generated files. The comment precedes the code generated by Spine and +/// separates it from code generated by the Protobuf compiler. +/// +const String _generatedBySpine = +''' + +// The code beneath is added by the Spine Dart code generation tool. +// Please do not edit this manually. + +'''; + +const _BUILT_VALUE = 'package:built_value/built_value.dart'; + +String renameClasses(String generatedCode, String fileName, FileDescriptorSet descriptors) { + var file = descriptors.file.firstWhere( + (file) => file.name == fileName, + orElse: () => throw Exception('File `$fileName` is not in the descriptor set.') + ); + var types = TypeSet.fromFile(file); + for (var type in types.messageTypes) { + var pattern = RegExp('\b${type.dartClassName}\b'); + generatedCode.replaceAll(pattern, type.dartMutableClassName); + } + return generatedCode; +} + +List generate(FileDescriptorSet descriptors) { + var files = List(); + var knownTypes = TypeSet.of(descriptors); + var substitutions = Map(); + for (MessageType type in knownTypes.messageTypes) { + substitutions[type.dartClassName] = type.dartMutableClassName; + } + for (var file in descriptors.file) { + var types = TypeSet.fromFile(file).messageTypes; + if (types.isNotEmpty) { + var classes = types.map((t) => _generate(t, knownTypes)); + var library = Library((lib) { + lib.directives + ..add(Directive.import(_BUILT_VALUE)) + ..add(Directive.part('${types.first.fileNameNoExtension}.pb.g.dart')); + lib.body.addAll(classes); + }); + var content = _generatedBySpine + _emit(library); + files.add(PrebuiltFile(types.first.dartFilePath, content, substitutions)); + } + } + return files; +} + +String _emit(Library lib) { + var emitter = DartEmitter(Allocator.simplePrefixing()); + var formatter = DartFormatter(); + var code = lib.accept(emitter).toString(); + var content = formatter.format(code); + return content; +} + +Class _generate(MessageType type, TypeSet knownTypes) { + var factory = ImmutableTypeFactory(type, knownTypes); + var cls = factory.generate(); + return cls; +} + +class PrebuiltFile { + + final String name; + final String additions; + final Map substitutions; + + PrebuiltFile(this.name, this.additions, this.substitutions); +} diff --git a/codegen/lib/src/bytes_validator_factory.dart b/codegen/lib/src/bytes_validator_factory.dart index 64856b4ea8..16d5e01fad 100644 --- a/codegen/lib/src/bytes_validator_factory.dart +++ b/codegen/lib/src/bytes_validator_factory.dart @@ -1,5 +1,5 @@ /* - * Copyright 2019, TeamDev. All rights reserved. + * Copyright 2020, TeamDev. All rights reserved. * * Redistribution and use in source and/or binary forms, with or without * modification, must retain the above copyright notice and the following @@ -18,8 +18,8 @@ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ -import 'package:dart_code_gen/google/protobuf/descriptor.pb.dart'; import 'package:dart_code_gen/src/field_validator_factory.dart'; +import 'package:dart_code_gen/src/type.dart'; import 'package:dart_code_gen/src/validator_factory.dart'; import 'field_validator_factory.dart'; @@ -29,7 +29,7 @@ import 'validator_factory.dart'; /// class BytesValidatorFactory extends SingularFieldValidatorFactory { - BytesValidatorFactory(ValidatorFactory validatorFactory, FieldDescriptorProto field) + BytesValidatorFactory(ValidatorFactory validatorFactory, FieldDeclaration field) : super(validatorFactory, field); @override diff --git a/codegen/lib/src/constraint_violation.dart b/codegen/lib/src/constraint_violation.dart index 7d0f68b448..185f8816dd 100644 --- a/codegen/lib/src/constraint_violation.dart +++ b/codegen/lib/src/constraint_violation.dart @@ -1,5 +1,5 @@ /* - * Copyright 2019, TeamDev. All rights reserved. + * Copyright 2020, TeamDev. All rights reserved. * * Redistribution and use in source and/or binary forms, with or without * modification, must retain the above copyright notice and the following diff --git a/codegen/lib/src/enum_validator_factory.dart b/codegen/lib/src/enum_validator_factory.dart index cf3c9bef8e..ab5f5e1be2 100644 --- a/codegen/lib/src/enum_validator_factory.dart +++ b/codegen/lib/src/enum_validator_factory.dart @@ -1,5 +1,5 @@ /* - * Copyright 2019, TeamDev. All rights reserved. + * Copyright 2020, TeamDev. All rights reserved. * * Redistribution and use in source and/or binary forms, with or without * modification, must retain the above copyright notice and the following @@ -19,8 +19,8 @@ */ import 'package:code_builder/code_builder.dart'; -import 'package:dart_code_gen/google/protobuf/descriptor.pb.dart'; import 'package:dart_code_gen/src/field_validator_factory.dart'; +import 'package:dart_code_gen/src/type.dart'; import 'package:dart_code_gen/src/validator_factory.dart'; import 'field_validator_factory.dart'; @@ -33,7 +33,7 @@ const _minNonEmptyEnumValue = 1; /// class EnumValidatorFactory extends SingularFieldValidatorFactory { - EnumValidatorFactory(ValidatorFactory validatorFactory, FieldDescriptorProto field) + EnumValidatorFactory(ValidatorFactory validatorFactory, FieldDeclaration field) : super(validatorFactory, field); @override diff --git a/codegen/lib/src/field_validator_factory.dart b/codegen/lib/src/field_validator_factory.dart index 18b93117a8..5fb2614bc3 100644 --- a/codegen/lib/src/field_validator_factory.dart +++ b/codegen/lib/src/field_validator_factory.dart @@ -1,5 +1,5 @@ /* - * Copyright 2019, TeamDev. All rights reserved. + * Copyright 2020, TeamDev. All rights reserved. * * Redistribution and use in source and/or binary forms, with or without * modification, must retain the above copyright notice and the following @@ -22,6 +22,7 @@ import 'package:code_builder/code_builder.dart'; import 'package:dart_code_gen/google/protobuf/descriptor.pb.dart'; import 'package:dart_code_gen/spine/options.pb.dart'; import 'package:dart_code_gen/src/bytes_validator_factory.dart'; +import 'package:dart_code_gen/src/type.dart'; import 'constraint_violation.dart'; import 'enum_validator_factory.dart'; @@ -38,7 +39,7 @@ class FieldValidatorFactory { final ValidatorFactory validatorFactory; /// The field to validate. - final FieldDescriptorProto field; + final FieldDeclaration field; FieldValidatorFactory(this.validatorFactory, this.field); @@ -46,9 +47,9 @@ class FieldValidatorFactory { /// /// May return `null` to signify that no validation is required for the given field. /// - factory FieldValidatorFactory.forField(FieldDescriptorProto field, ValidatorFactory factory) { + factory FieldValidatorFactory.forField(FieldDeclaration field, ValidatorFactory factory) { var singularFactory = SingularFieldValidatorFactory._forType(field, factory); - var repeated = field.label == FieldDescriptorProto_Label.LABEL_REPEATED; + var repeated = field.descriptor.label == FieldDescriptorProto_Label.LABEL_REPEATED; if (repeated) { return RepeatedFieldValidatorFactory(factory, field, singularFactory); } else { @@ -70,7 +71,7 @@ class FieldValidatorFactory { /// Returns `true` if the field is required and `false` if it is optional. /// bool isRequired() { - var options = field.options; + var options = field.descriptor.options; return options.hasExtension(Options.required) && options.getExtension(Options.required); } @@ -103,7 +104,7 @@ class FieldValidatorFactory { Expression _requiredMissing() { return violationRef.call([literalString('Field must be set.'), literalString(validatorFactory.fullTypeName), - literalList([field.name])]); + literalList([field.protoName])]); } } @@ -111,12 +112,12 @@ class FieldValidatorFactory { /// class SingularFieldValidatorFactory extends FieldValidatorFactory { - SingularFieldValidatorFactory(ValidatorFactory validatorFactory, FieldDescriptorProto field) + SingularFieldValidatorFactory(ValidatorFactory validatorFactory, FieldDeclaration field) : super(validatorFactory, field); - factory SingularFieldValidatorFactory._forType(FieldDescriptorProto field, + factory SingularFieldValidatorFactory._forType(FieldDeclaration field, ValidatorFactory factory) { - var type = field.type; + var type = field.descriptor.type; switch (type) { case FieldDescriptorProto_Type.TYPE_STRING: return StringValidatorFactory(factory, field); @@ -176,7 +177,7 @@ class RepeatedFieldValidatorFactory extends FieldValidatorFactory { final FieldValidatorFactory _singular; RepeatedFieldValidatorFactory(ValidatorFactory validatorFactory, - FieldDescriptorProto field, + FieldDeclaration field, this._singular) : super(validatorFactory, field); @@ -187,7 +188,7 @@ class RepeatedFieldValidatorFactory extends FieldValidatorFactory { var requiredRule = createRequiredRule(); validation.add(requiredRule._eval(field)); } - var values = 'values_${this.field.name}'; + var values = 'values_${this.field.protoName}'; var valuesRef = refer(values); var validateDistinctList = _validateDistinct(valuesRef); var validateElements = _validateEachElement(valuesRef); @@ -210,7 +211,7 @@ class RepeatedFieldValidatorFactory extends FieldValidatorFactory { LazyCondition notSetCondition() => (v) => v.property('isEmpty'); Expression _validateDistinct(Reference valuesRef) { - var options = field.options; + var options = field.descriptor.options; var option = Options.distinct; if (options.hasExtension(option) && options.getExtension(option)) { var length = 'length'; @@ -220,7 +221,7 @@ class RepeatedFieldValidatorFactory extends FieldValidatorFactory { LazyViolation violation = (v) => violationRef.call([literalString('Collection must be distinct.'), literalString(validatorFactory.fullTypeName), - literalList([field.name])]); + literalList([field.protoName])]); var distinctRule = newRule(condition, violation); return distinctRule._eval(valuesRef); } else { diff --git a/codegen/lib/src/immutable_type_factory.dart b/codegen/lib/src/immutable_type_factory.dart new file mode 100644 index 0000000000..8c0fbb1012 --- /dev/null +++ b/codegen/lib/src/immutable_type_factory.dart @@ -0,0 +1,209 @@ +/* + * Copyright 2020, TeamDev. All rights reserved. + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +import 'package:code_builder/code_builder.dart'; +import 'package:dart_code_gen/google/protobuf/descriptor.pb.dart'; +import 'package:dart_code_gen/src/type.dart'; + +const String _builtCollection = 'package:built_collection/built_collection.dart'; +const String _int64 = 'package:fixnum/fixnum.dart'; +const String _message = 'package:spine_client/message.dart'; + +class ImmutableTypeFactory { + + final MessageType _type; + final TypeSet _knownTypes; + final String _className; + + ImmutableTypeFactory(this._type, this._knownTypes) : _className = _type.dartClassName; + + Class generate() { + var getters = _type.fields.map(_buildField); + var message = TypeReference((b) => b + ..symbol = 'Message' + ..url = _message + ..types.add(refer(_type.dartClassName)) + ..types.add(refer(_type.dartMutableClassName)) + ); + var builderRef = refer('${_className}Builder'); + var cls = Class((b) { + b.name = _className; + b.abstract = true; + b.extend = message; + b.implements.add(_builtRef(builderRef)); + b.fields.add(_defaultInstance()); + b.constructors + ..add(_privateCtor()) + ..add(_builderCtor(builderRef)) + ..add(_copyCtor()); + b.methods.addAll(getters); + }); + return cls; + } + + Method _buildField(FieldDeclaration field) { + return Method((b) { + b.name = field.escapedDartName; + b.annotations.add(refer('nullable')); + b.type = MethodType.getter; + b.returns = _typeOf(field, _knownTypes); + }); + } + + Reference _builtRef(Reference builderRef) { + return TypeReference((ref) { + ref.symbol = 'Built'; + ref.types + ..add(refer(_className)) + ..add(builderRef); + }); + } + + Field _defaultInstance() { + return Field((b) => b + ..name = 'defaultInstance' + ..type = refer(_className) + ..static = true + ..assignment = Code('$_className()') + ); + } + + Constructor _privateCtor() { + var privateCtor = Constructor((b) { + b.name = '_'; + }); + return privateCtor; + } + + Constructor _builderCtor(Reference builderRef) { + var builderCtor = Constructor((b) { + b.factory = true; + b.optionalParameters.add(Parameter((param) { + param.type = _updatesFuncType(builderRef); + param.name = ''; + })); + b.redirect = refer('_\$$_className'); + }); + return builderCtor; + } + + Constructor _copyCtor() { + var paramName = 'message'; + var builderParamName = 'b'; + var builderCtor = Constructor((b) { + b.factory = true; + b.name = 'from'; + b.requiredParameters.add(Parameter((param) { + param.type = refer(_type.dartMutableClassName); + param.name = paramName; + })); + b.body = Code(''' + return ${_type.dartClassName}(($builderParamName) => $builderParamName + ${_initFields('b', paramName)} + ); + '''); + }); + return builderCtor; + } + + String _initFields(String target, String source) { + var code = StringBuffer(); + for (var field in _type.fields) { + String operator; + if (field.isMap) { + operator = '.putAll'; + } else if (field.isRepeated) { + operator = '.addAll'; + } else { + operator = ' = '; + } + code.writeln( + '..$target.${field.escapedDartName}$operator($source.${field.escapedDartName})' + ); + } + return code.toString(); + } + + Reference _updatesFuncType(Reference builderRef) { + return FunctionType((type) { + type.requiredParameters.add(builderRef); + }); + } + + Reference _typeOf(FieldDeclaration field, TypeSet knownTypes) { + var descriptor = field.descriptor; + var type = descriptor.type; + Reference ref = null; + if (field.isMap) { + ref = refer('BuiltMap', _builtCollection); + } else { + switch (type) { + case FieldDescriptorProto_Type.TYPE_BOOL: + ref = refer('bool'); + break; + case FieldDescriptorProto_Type.TYPE_BYTES: + ref = refer('BuiltList', _builtCollection); + break; + case FieldDescriptorProto_Type.TYPE_DOUBLE: + case FieldDescriptorProto_Type.TYPE_FLOAT: + ref = refer('double'); + break; + case FieldDescriptorProto_Type.TYPE_INT32: + case FieldDescriptorProto_Type.TYPE_UINT32: + case FieldDescriptorProto_Type.TYPE_SINT32: + case FieldDescriptorProto_Type.TYPE_FIXED32: + case FieldDescriptorProto_Type.TYPE_SFIXED32: + ref = refer('int'); + break; + case FieldDescriptorProto_Type.TYPE_INT64: + case FieldDescriptorProto_Type.TYPE_UINT64: + case FieldDescriptorProto_Type.TYPE_SINT64: + case FieldDescriptorProto_Type.TYPE_FIXED64: + case FieldDescriptorProto_Type.TYPE_SFIXED64: + ref = refer('Int64', _int64); + break; + case FieldDescriptorProto_Type.TYPE_STRING: + ref = refer('String'); + break; + case FieldDescriptorProto_Type.TYPE_MESSAGE: + case FieldDescriptorProto_Type.TYPE_ENUM: + var messageClass = knownTypes.findByName(descriptor.typeName); + var file = messageClass.dartFilePath; + if (file == field.declaringType.dartFilePath) { + ref = refer(messageClass.dartClassName); + } else { + var path = messageClass.dartPathRelativeTo(field.declaringType); + ref = refer(messageClass.dartClassName, path); + } + break; + } + if (field.isRepeated) { + ref = TypeReference((type) => type + ..symbol = 'BuiltList' + ..types.add(ref) + ..url = _builtCollection); + } + } + if (ref == null) { + throw Exception('Unknown type `${type}`.'); + } + return ref; + } +} diff --git a/codegen/lib/src/imports.dart b/codegen/lib/src/imports.dart index 070d48c92d..63fdf24d35 100644 --- a/codegen/lib/src/imports.dart +++ b/codegen/lib/src/imports.dart @@ -1,5 +1,5 @@ /* - * Copyright 2019, TeamDev. All rights reserved. + * Copyright 2020, TeamDev. All rights reserved. * * Redistribution and use in source and/or binary forms, with or without * modification, must retain the above copyright notice and the following diff --git a/codegen/lib/src/known_types_factory.dart b/codegen/lib/src/known_types_factory.dart index eba5743d84..eb66ce16a2 100644 --- a/codegen/lib/src/known_types_factory.dart +++ b/codegen/lib/src/known_types_factory.dart @@ -1,5 +1,5 @@ /* - * Copyright 2019, TeamDev. All rights reserved. + * Copyright 2020, TeamDev. All rights reserved. * * Redistribution and use in source and/or binary forms, with or without * modification, must retain the above copyright notice and the following @@ -54,7 +54,7 @@ class KnownTypesFactory { var importPrefix = _properties.importPrefix; var urlToBuilderMap = {}; var defaultToUrlMap = {}; - for (var type in typeSet.types) { + for (var type in typeSet.messageTypes) { var typeRef = refer(type.dartClassName, "${importPrefix}/${type.dartFilePath}"); var ctorCall = typeRef.newInstance([]); var builderInfoAccessor = ctorCall.property('info_'); @@ -78,7 +78,7 @@ class KnownTypesFactory { Field _createValidatorMap() { var validatorMap = Map(); var typeSet = TypeSet.topLevelOnly(_properties.types); - for (var type in typeSet.types) { + for (var type in typeSet.messageTypes) { var factory = ValidatorFactory(type.file, type, _properties); var typeUrl = literalString(type.typeUrl); validatorMap[typeUrl] = factory.createValidator(); diff --git a/codegen/lib/src/message_validator_factory.dart b/codegen/lib/src/message_validator_factory.dart index cfbdc99037..3b11afeb11 100644 --- a/codegen/lib/src/message_validator_factory.dart +++ b/codegen/lib/src/message_validator_factory.dart @@ -1,5 +1,5 @@ /* - * Copyright 2019, TeamDev. All rights reserved. + * Copyright 2020, TeamDev. All rights reserved. * * Redistribution and use in source and/or binary forms, with or without * modification, must retain the above copyright notice and the following @@ -18,8 +18,8 @@ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ -import 'package:dart_code_gen/google/protobuf/descriptor.pb.dart'; import 'package:dart_code_gen/src/field_validator_factory.dart'; +import 'package:dart_code_gen/src/type.dart'; import 'package:dart_code_gen/src/validator_factory.dart'; import 'field_validator_factory.dart'; @@ -29,7 +29,7 @@ import 'validator_factory.dart'; /// class MessageValidatorFactory extends SingularFieldValidatorFactory { - MessageValidatorFactory(ValidatorFactory validatorFactory, FieldDescriptorProto field) + MessageValidatorFactory(ValidatorFactory validatorFactory, FieldDeclaration field) : super(validatorFactory, field); @override diff --git a/codegen/lib/src/number_validator_factory.dart b/codegen/lib/src/number_validator_factory.dart index d77944c44a..02004db696 100644 --- a/codegen/lib/src/number_validator_factory.dart +++ b/codegen/lib/src/number_validator_factory.dart @@ -1,5 +1,5 @@ /* - * Copyright 2019, TeamDev. All rights reserved. + * Copyright 2020, TeamDev. All rights reserved. * * Redistribution and use in source and/or binary forms, with or without * modification, must retain the above copyright notice and the following @@ -21,6 +21,7 @@ import 'package:code_builder/code_builder.dart'; import 'package:dart_code_gen/google/protobuf/descriptor.pb.dart'; import 'package:dart_code_gen/spine/options.pb.dart'; +import 'package:dart_code_gen/src/type.dart'; import 'constraint_violation.dart'; import 'field_validator_factory.dart'; @@ -38,7 +39,7 @@ class NumberValidatorFactory extends SingularFieldValidatorFactor final String _wrapperType; NumberValidatorFactory(ValidatorFactory validatorFactory, - FieldDescriptorProto field, + FieldDeclaration field, this._wrapperType) : super(validatorFactory, field); @@ -49,7 +50,7 @@ class NumberValidatorFactory extends SingularFieldValidatorFactor @override Iterable rules() { var rules = []; - var options = field.options; + var options = field.descriptor.options; if (options.hasExtension(Options.min)) { Rule min = _minRule(options); rules.add(min); @@ -139,7 +140,7 @@ class NumberValidatorFactory extends SingularFieldValidatorFactor var any = refer('Any', protoAnyImport(standardPackage)).property('pack').call([floatValue]); return violationRef.call([literalString('Number is out of bound.'), literalString(validatorFactory.fullTypeName), - literalList([field.name]), + literalList([field.protoName]), any]); } } @@ -149,19 +150,19 @@ class NumberValidatorFactory extends SingularFieldValidatorFactor class DoubleValidatorFactory extends NumberValidatorFactory { DoubleValidatorFactory._(ValidatorFactory validatorFactory, - FieldDescriptorProto field, + FieldDeclaration field, String wrapperType) : super(validatorFactory, field, wrapperType); /// Creates a new validator factory for a `float` field. factory DoubleValidatorFactory.forFloat(ValidatorFactory validatorFactory, - FieldDescriptorProto field) { + FieldDeclaration field) { return DoubleValidatorFactory._(validatorFactory, field, 'FloatValue'); } /// Creates a new validator factory for a `double` field. factory DoubleValidatorFactory.forDouble(ValidatorFactory validatorFactory, - FieldDescriptorProto field) { + FieldDeclaration field) { return DoubleValidatorFactory._(validatorFactory, field, 'DoubleValue'); } @@ -174,7 +175,7 @@ class DoubleValidatorFactory extends NumberValidatorFactory { class IntValidatorFactory extends NumberValidatorFactory { IntValidatorFactory._(ValidatorFactory validatorFactory, - FieldDescriptorProto field, + FieldDeclaration field, String wrapperType) : super(validatorFactory, field, wrapperType); @@ -184,7 +185,7 @@ class IntValidatorFactory extends NumberValidatorFactory { /// by this factory. /// factory IntValidatorFactory.forInt32(ValidatorFactory validatorFactory, - FieldDescriptorProto field) { + FieldDeclaration field) { return IntValidatorFactory._(validatorFactory, field, 'Int32Value'); } @@ -194,19 +195,19 @@ class IntValidatorFactory extends NumberValidatorFactory { /// by this factory. /// factory IntValidatorFactory.forInt64(ValidatorFactory validatorFactory, - FieldDescriptorProto field) { + FieldDeclaration field) { return IntValidatorFactory._(validatorFactory, field, 'Int64Value'); } /// Creates a new validator factory for a unsigned 32-bit integer. factory IntValidatorFactory.forUInt32(ValidatorFactory validatorFactory, - FieldDescriptorProto field) { + FieldDeclaration field) { return IntValidatorFactory._(validatorFactory, field, 'UInt32Value'); } /// Creates a new validator factory for a unsigned 64-bit integer. factory IntValidatorFactory.forUInt64(ValidatorFactory validatorFactory, - FieldDescriptorProto field) { + FieldDeclaration field) { return IntValidatorFactory._(validatorFactory, field, 'UInt64Value'); } diff --git a/codegen/lib/src/required_field_validation_factory.dart b/codegen/lib/src/required_field_validation_factory.dart index 7fdcccad4f..025e33bfa0 100644 --- a/codegen/lib/src/required_field_validation_factory.dart +++ b/codegen/lib/src/required_field_validation_factory.dart @@ -1,5 +1,5 @@ /* - * Copyright 2019, TeamDev. All rights reserved. + * Copyright 2020, TeamDev. All rights reserved. * * Redistribution and use in source and/or binary forms, with or without * modification, must retain the above copyright notice and the following @@ -19,8 +19,8 @@ */ import 'package:code_builder/code_builder.dart'; +import 'package:dart_code_gen/src/type.dart'; -import '../google/protobuf/descriptor.pb.dart'; import 'constraint_violation.dart'; import 'field_validator_factory.dart'; import 'validator_factory.dart'; @@ -103,7 +103,7 @@ class _Combination { return factory.notSetCondition()(fieldAccess); } - FieldValidatorFactory _validatorFactory(FieldDescriptorProto field) { + FieldValidatorFactory _validatorFactory(FieldDeclaration field) { var factory = FieldValidatorFactory.forField(field, _validator); if (factory == null || !factory.supportsRequired()) { throw StateError('Field `${_validator.type.fullName}.${field}` cannot be required.'); @@ -111,12 +111,11 @@ class _Combination { return factory; } - FieldDescriptorProto _field(String name) { + FieldDeclaration _field(String name) { var type = _validator.type; - FieldDescriptorProto field = type - .descriptor - .field - .where((t) => t.name == name) + FieldDeclaration field = type + .fields + .where((t) => t.protoName == name) .first; ArgumentError.checkNotNull(field, '`${type.fullName}` does not declare field `${name}`.'); return field; diff --git a/codegen/lib/src/string_validator_factory.dart b/codegen/lib/src/string_validator_factory.dart index 834866d18b..e6342e575d 100644 --- a/codegen/lib/src/string_validator_factory.dart +++ b/codegen/lib/src/string_validator_factory.dart @@ -1,5 +1,5 @@ /* - * Copyright 2019, TeamDev. All rights reserved. + * Copyright 2020, TeamDev. All rights reserved. * * Redistribution and use in source and/or binary forms, with or without * modification, must retain the above copyright notice and the following @@ -21,6 +21,7 @@ import 'package:code_builder/code_builder.dart'; import 'package:dart_code_gen/google/protobuf/descriptor.pb.dart'; import 'package:dart_code_gen/spine/options.pb.dart'; +import 'package:dart_code_gen/src/type.dart'; import 'constraint_violation.dart'; import 'field_validator_factory.dart'; @@ -32,12 +33,12 @@ import 'validator_factory.dart'; /// class StringValidatorFactory extends SingularFieldValidatorFactory { - StringValidatorFactory(ValidatorFactory validatorFactory, FieldDescriptorProto field) + StringValidatorFactory(ValidatorFactory validatorFactory, FieldDeclaration field) : super(validatorFactory, field); @override Iterable rules() { - var options = field.options; + var options = field.descriptor.options; var rules = []; if (isRequired()) { rules.add(createRequiredRule()); @@ -73,6 +74,6 @@ class StringValidatorFactory extends SingularFieldValidatorFactory { var message = 'String must match the regular expression `$pattern`'; return violationRef.call([literalString(message, raw: true), literalString(validatorFactory.fullTypeName), - literalList([field.name])]); + literalList([field.protoName])]); } } diff --git a/codegen/lib/src/type.dart b/codegen/lib/src/type.dart index 3898cd38e2..24e3e43fae 100644 --- a/codegen/lib/src/type.dart +++ b/codegen/lib/src/type.dart @@ -1,5 +1,5 @@ /* - * Copyright 2019, TeamDev. All rights reserved. + * Copyright 2020, TeamDev. All rights reserved. * * Redistribution and use in source and/or binary forms, with or without * modification, must retain the above copyright notice and the following @@ -20,20 +20,19 @@ import 'package:dart_code_gen/google/protobuf/descriptor.pb.dart'; import 'package:dart_code_gen/spine/options.pb.dart'; +import 'package:path/path.dart'; -const _protoExtension = 'proto'; -const _pbDartExtension = 'pb.dart'; +const _protoExtension = '.proto'; +const _pbDartExtension = '.pb.dart'; const _standardTypeUrlPrefix = 'type.googleapis.com'; -/// A Protobuf message type. -class MessageType { +const _libraryPathSeparator = '/'; + +abstract class Type { /// The file which defines this type. final FileDescriptorProto file; - /// The descriptor of this type. - final DescriptorProto descriptor; - /// The full Protobuf name of this type. /// /// For example, `spine.net.Uri.Protocol`. @@ -46,18 +45,7 @@ class MessageType { /// final String dartClassName; - MessageType._(this.file, this.descriptor, this.fullName, this.dartClassName); - - /// Creates a `MessageType` from the given descriptor and file assuming the message declaration - /// is top-level, i.e. the type is not nested within another type. - MessageType._fromFile(FileDescriptorProto file, DescriptorProto descriptor) : - file = file, - descriptor = descriptor, - fullName = _fullName(file, descriptor), - dartClassName = descriptor.name; - - static String _fullName(FileDescriptorProto file, DescriptorProto descriptor) => - "${file.package}.${descriptor.name}"; + Type._(this.file, this.fullName, this.dartClassName); /// Relative path to the file which contains the Dart class for this message type. /// @@ -70,6 +58,18 @@ class MessageType { return dartPath; } + /// The name of the declaring Proto file, without an extension. + /// + /// For example, if this type is declared in `spine/net/url.proto`, returns `url`. + /// + String get fileNameNoExtension { + var protoFilePath = file.name; + var nameStartIndex = protoFilePath.lastIndexOf('/') + 1; + var extensionIndex = protoFilePath.length - _protoExtension.length; + var dartPath = protoFilePath.substring(nameStartIndex, extensionIndex); + return dartPath; + } + /// Type URL of the message. /// /// For example, `type.spine.io/spine.net.Uri.Protocol` @@ -80,26 +80,101 @@ class MessageType { return "$prefix/$fullName"; } + String dartPathRelativeTo(Type otherType) { + var relativeDirPath = relative(_dirPath, from: otherType._dirPath); + return relativeDirPath + _libraryPathSeparator + _dartFileName; + } + + String get _dirPath { + var thisPath = dartFilePath; + var thisPathElements = thisPath.split(_libraryPathSeparator); + if (thisPathElements.length <= 1) { + return thisPath; + } else { + return thisPathElements.sublist(0, thisPathElements.length - 1) + .join(_libraryPathSeparator); + } + } + + String get _dartFileName => dartFilePath.split(_libraryPathSeparator).last; + + @override + String toString() { + return fullName; + } +} + +/// A Protobuf message type. +class MessageType extends Type { + + /// The descriptor of this type. + final DescriptorProto descriptor; + + /// The name of the Dart class generated for this type. + /// + /// For example, `MutableUri_Protocol`. + /// + final String dartMutableClassName; + + MessageType._(file, this.descriptor, fullName, dartClassName, this.dartMutableClassName) : + super._(file, fullName, dartClassName); + + /// Creates a `MessageType` from the given descriptor and file assuming the message declaration + /// is top-level, i.e. the type is not nested within another type. + MessageType._fromFile(FileDescriptorProto file, DescriptorProto descriptor) + : this._(file, + descriptor, + _fullName(file, descriptor), + descriptor.name, + _mutableClassName(descriptor)); + + static String _fullName(FileDescriptorProto file, DescriptorProto descriptor) => + '${file.package}.${descriptor.name}'; + + static String _mutableClassName(DescriptorProto descriptor) => 'Mutable${descriptor.name}'; + + List get fields { + var fields = descriptor.field.map((descriptor) => FieldDeclaration(this, descriptor)); + return List.from(fields); + } + /// Obtains all the nested declarations of this type, including deeper levels of nesting. TypeSet allChildDeclarations() { - var children = {}; - for (var child in _nestedDeclarations()) { - children.add(child); - children.addAll(child.allChildDeclarations() - .types); + var messages = {}; + var enums = {}; + enums.addAll(_nestedEnumDeclarations()); + for (var child in _nestedMessageDeclarations()) { + messages.add(child); + var grandchildren = child.allChildDeclarations(); + messages.addAll(grandchildren.messageTypes); + enums.addAll(grandchildren.enumTypes); } - return TypeSet._(children); + return TypeSet._(messages, enums); } /// Obtains the message declarations nested in this type. - Iterable _nestedDeclarations() => + Iterable _nestedMessageDeclarations() => descriptor.nestedType .where((desc) => !desc.options.mapEntry) - .map((desc) => _child(desc)); + .map((desc) => _childMessage(desc)); + + /// Obtains the message declarations nested in this type. + Iterable _nestedEnumDeclarations() => + descriptor.enumType + .map((desc) => _childEnum(desc)); + + MessageType _childMessage(DescriptorProto descriptor) { + var name = descriptor.name; + return MessageType._(file, + descriptor, + _childProtoName(name), + _childDartName(name), + _privateChildDartName(name)); + } - MessageType _child(DescriptorProto descriptor) { + EnumType _childEnum(EnumDescriptorProto descriptor) { var name = descriptor.name; - return MessageType._(file, descriptor, _childProtoName(name), _childDartName(name)); + return EnumType._(file, descriptor, _childProtoName(name), _childDartName(name)); } String _childProtoName(String simpleName) { @@ -109,40 +184,234 @@ class MessageType { String _childDartName(String simpleName) { return '${dartClassName}_${simpleName}'; } + + String _privateChildDartName(String simpleName) { + return '${dartMutableClassName}_${simpleName}'; + } +} + +class EnumType extends Type { + + /// The descriptor of this type. + final EnumDescriptorProto descriptor; + + EnumType._(file, this.descriptor, fullName, dartClassName) : + super._(file, fullName, dartClassName); + + /// Creates a `MessageType` from the given descriptor and file assuming the message declaration + /// is top-level, i.e. the type is not nested within another type. + EnumType._fromFile(FileDescriptorProto file, this.descriptor) : + super._(file, _fullName(file, descriptor), descriptor.name); + + static String _fullName(FileDescriptorProto file, EnumDescriptorProto descriptor) => + "${file.package}.${descriptor.name}"; +} + +class FieldDeclaration { + + // A list of all Dart keywords. + // + // See https://dart.dev/guides/language/language-tour#keywords. + // + static const List _DART_KEYWORDS = [ + 'abstract', + 'else', + 'import', + 'super', + 'as', + 'enum', + 'in', + 'switch', + 'assert', + 'export', + 'interface', + 'sync', + 'async', + 'extends', + 'is', + 'this', + 'await', + 'extension', + 'library', + 'throw', + 'break', + 'external', + 'mixin', + 'true', + 'case', + 'factory', + 'new', + 'try', + 'catch', + 'false', + 'null', + 'typedef', + 'class', + 'final', + 'on', + 'var', + 'const', + 'finally', + 'operator', + 'void', + 'continue', + 'for', + 'part', + 'while', + 'covariant', + 'Function', + 'rethrow', + 'with', + 'default', + 'get', + 'return', + 'yield', + 'deferred', + 'hide', + 'set', + 'do', + 'if', + 'show', + 'dynamic', + 'implements', + 'static' + ]; + + static const List _BUILT_VALUE_RESERVED = [ + 'update' + ]; + + final MessageType declaringType; + final FieldDescriptorProto descriptor; + final String protoName; + final String dartName; + + FieldDeclaration(this.declaringType, this.descriptor) : + protoName = descriptor.name, + dartName = _dartName(descriptor); + + static String _dartName(FieldDescriptorProto descriptor) { + var protoName = descriptor.name; + var words = protoName.split('_'); + var first = words[0]; + var capitalized = List.of(words.map(_capitalize)); + capitalized[0] = first; + return capitalized.join(''); + } + + static String _capitalize(String word) { + return word.isEmpty + ? word + : '${word[0].toUpperCase()}${word.substring(1)}'; + } + + String get escapedDartName { + if (_DART_KEYWORDS.contains(dartName) || _BUILT_VALUE_RESERVED.contains(dartName)) { + return '${dartName}_${descriptor.number}'; + } else { + return dartName; + } + } + + bool get isRepeated => + descriptor.label == FieldDescriptorProto_Label.LABEL_REPEATED; + + bool get isMap { + if (!isRepeated) { + return false; + } + if (descriptor.type != FieldDescriptorProto_Type.TYPE_MESSAGE) { + return false; + } + var mapEntryTypeName = _capitalize(dartName) + 'Entry'; + return _simpleName(descriptor.typeName) == mapEntryTypeName; + } + + String _simpleName(String fullName) { + var start = fullName.lastIndexOf('.') + 1; + return fullName.substring(start); + } } /// A set of Protobuf message types. class TypeSet { - final Set types; + final Set messageTypes; + final Set enumTypes; - TypeSet._(this.types); + TypeSet._(this.messageTypes, this.enumTypes); /// Obtains all the message types declared in files of the given [fileSet]. factory TypeSet.of(FileDescriptorSet fileSet) { - var typeSet = {}; + var messages = {}; + var enums = {}; var files = fileSet.file; for (var file in files) { - for (var type in file.messageType) { - var messageType = MessageType._fromFile(file, type); - typeSet.add(messageType); - typeSet.addAll(messageType.allChildDeclarations().types); - } + _collectTypes(file, messages, enums); } - return TypeSet._(typeSet); + return TypeSet._(messages, enums); } /// Obtains all the top-level message types declared in files of the given [fileSet]. factory TypeSet.topLevelOnly(FileDescriptorSet fileSet) { - var typeSet = {}; + var messages = {}; + var enums = {}; var files = fileSet.file; for (var file in files) { - for (var type in file.messageType) { - var messageType = MessageType._fromFile(file, type); - typeSet.add(messageType); - typeSet.addAll(messageType._nestedDeclarations()); - } + var messageTypes = file.messageType.map((type) => MessageType._fromFile(file, type)); + messages.addAll(messageTypes); + enums.addAll(_topLevelEnumTypes(file)); + } + return TypeSet._(messages, enums); + } + + /// Obtains all the message types declared in the given file. + factory TypeSet.fromFile(FileDescriptorProto fileDescriptor) { + var messages = {}; + var enums = {}; + _collectTypes(fileDescriptor, messages, enums); + return TypeSet._(messages, enums); + } + + static void _collectTypes(FileDescriptorProto fileDescriptor, + Set messages, + Set enums) { + for (var type in fileDescriptor.messageType) { + var messageType = MessageType._fromFile(fileDescriptor, type); + messages.add(messageType); + var children = messageType.allChildDeclarations(); + messages.addAll(children.messageTypes); + enums.addAll(children.enumTypes); + } + enums.addAll(_topLevelEnumTypes(fileDescriptor)); + } + + static Iterable _topLevelEnumTypes(FileDescriptorProto file) { + var enumTypes = file.enumType.map((type) => EnumType._fromFile(file, type)); + return enumTypes; + } + + /// Find a `Type` by the given name. + /// + /// Throws an exception if the type cannot be found. + /// + Type findByName(String typeName) { + var criterion = _noLeadingDot(typeName); + var allTypes = {} + ..addAll(messageTypes) + ..addAll(enumTypes); + var type = allTypes.firstWhere( + (element) => _noLeadingDot(element.fullName) == criterion, + orElse: () => throw Exception('Message type `${criterion}` is unknown.') + ); + return type; + } + + String _noLeadingDot(String value) { + if (value.length > 1 && value.startsWith('.')) { + return value.substring(1); + } else { + return value; } - return TypeSet._(typeSet); } } diff --git a/codegen/lib/src/validator_factory.dart b/codegen/lib/src/validator_factory.dart index df57f20af5..af6aa8930b 100644 --- a/codegen/lib/src/validator_factory.dart +++ b/codegen/lib/src/validator_factory.dart @@ -1,5 +1,5 @@ /* - * Copyright 2019, TeamDev. All rights reserved. + * Copyright 2020, TeamDev. All rights reserved. * * Redistribution and use in source and/or binary forms, with or without * modification, must retain the above copyright notice and the following @@ -107,7 +107,7 @@ class ValidatorFactory { Code _createFieldValidators() { var validations = []; - for (var field in type.descriptor.field) { + for (var field in type.fields) { var validator = _createFieldValidator(field); if (validator != null) { validations.add(validator); @@ -137,7 +137,7 @@ class ValidatorFactory { /// /// See [FieldValidatorFactory] for more on field validation. /// - Code _createFieldValidator(FieldDescriptorProto field) { + Code _createFieldValidator(FieldDeclaration field) { var factory = FieldValidatorFactory.forField(field, this); if (factory != null) { var fieldValue = accessField(field); @@ -147,8 +147,8 @@ class ValidatorFactory { } } - Expression accessField(FieldDescriptorProto field) => - _typedMessage.property(_fieldName(field)); + Expression accessField(FieldDeclaration field) => + _typedMessage.property(field.dartName); Expression get _typedMessage => refer(_msg).asA(_typeRef(properties.importPrefix)); diff --git a/codegen/pubspec.yaml b/codegen/pubspec.yaml index 7020a98705..f7188f7091 100644 --- a/codegen/pubspec.yaml +++ b/codegen/pubspec.yaml @@ -7,13 +7,17 @@ environment: sdk: '>=2.5.0 <3.0.0' dependencies: - args: 1.5.2 - protobuf: ^1.0.0 - fixnum: ^0.10.9 - code_builder: ^3.2.0 - dart_style: 1.3.1 + args: ^1.6.0 + protobuf: ^1.0.1 + fixnum: ^0.10.11 + code_builder: ^3.4.1 + dart_style: 1.3.6 + built_value: ^7.1.0 + path: ^1.7.0 dev_dependencies: + build_runner: ^1.10.1 + built_value_generator: ^7.1.0 pedantic: ^1.8.0 test: ^1.6.0 diff --git a/codegen/test/generation_test.dart b/codegen/test/generation_test.dart index eb7e546d9f..77dc473619 100644 --- a/codegen/test/generation_test.dart +++ b/codegen/test/generation_test.dart @@ -1,5 +1,5 @@ /* - * Copyright 2019, TeamDev. All rights reserved. + * Copyright 2020, TeamDev. All rights reserved. * * Redistribution and use in source and/or binary forms, with or without * modification, must retain the above copyright notice and the following diff --git a/codegen/test/validation_test.dart b/codegen/test/validation_test.dart index 09ca933670..8d0262088f 100644 --- a/codegen/test/validation_test.dart +++ b/codegen/test/validation_test.dart @@ -1,5 +1,5 @@ /* - * Copyright 2019, TeamDev. All rights reserved. + * Copyright 2020, TeamDev. All rights reserved. * * Redistribution and use in source and/or binary forms, with or without * modification, must retain the above copyright notice and the following diff --git a/config b/config index 47048aed2a..e2224403b9 160000 --- a/config +++ b/config @@ -1 +1 @@ -Subproject commit 47048aed2a242a17c959577515b0548eb58fb984 +Subproject commit e2224403b91e58670b6c57b4d0ffff194ddb4bab diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 490fda8577..62d4c05355 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index a4f0001d20..12d38de6a4 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.4.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-6.6.1-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew index 2fe81a7d95..fbd7c51583 100755 --- a/gradlew +++ b/gradlew @@ -82,6 +82,7 @@ esac CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + # Determine the Java command to use to start the JVM. if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then @@ -129,6 +130,7 @@ fi if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then APP_HOME=`cygpath --path --mixed "$APP_HOME"` CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` # We build the pattern for arguments to be converted via cygpath diff --git a/gradlew.bat b/gradlew.bat index 62bd9b9cce..5093609d51 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -84,6 +84,7 @@ set CMD_LINE_ARGS=%* set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + @rem Execute Gradle "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% diff --git a/integration-tests/client-test/build.gradle.kts b/integration-tests/client-test/build.gradle.kts index 45b2d09b13..3730046750 100644 --- a/integration-tests/client-test/build.gradle.kts +++ b/integration-tests/client-test/build.gradle.kts @@ -1,5 +1,5 @@ /* - * Copyright 2019, TeamDev. All rights reserved. + * Copyright 2020, TeamDev. All rights reserved. * * Redistribution and use in source and/or binary forms, with or without * modification, must retain the above copyright notice and the following @@ -60,6 +60,7 @@ protoDart { tasks.generateDart { descriptor = protoDart.testDescriptorSet target = "$projectDir/integration-test" + generateImmutableTypes = false } tasks.assemble { diff --git a/integration-tests/client-test/build.yaml b/integration-tests/client-test/build.yaml new file mode 100644 index 0000000000..5ec42fb9d3 --- /dev/null +++ b/integration-tests/client-test/build.yaml @@ -0,0 +1,14 @@ +targets: + $default: + builders: + "built_value": + +builders: + built_value: + target: ":client-test" + import: "package:built_value_generator/builder.dart" + builder_factories: + - "builtValue" + build_extensions: + ".proto.dart": [".g.dart"] + build_to: "source" diff --git a/integration-tests/client-test/integration-test/client_test.dart b/integration-tests/client-test/integration-test/client_test.dart index 90033f8a40..3eeded9d5a 100644 --- a/integration-tests/client-test/integration-test/client_test.dart +++ b/integration-tests/client-test/integration-test/client_test.dart @@ -1,5 +1,5 @@ /* - * Copyright 2019, TeamDev. All rights reserved. + * Copyright 2020, TeamDev. All rights reserved. * * Redistribution and use in source and/or binary forms, with or without * modification, must retain the above copyright notice and the following diff --git a/integration-tests/client-test/integration-test/client_test.html b/integration-tests/client-test/integration-test/client_test.html index 88beec7035..5f546ef27f 100644 --- a/integration-tests/client-test/integration-test/client_test.html +++ b/integration-tests/client-test/integration-test/client_test.html @@ -1,5 +1,5 @@