From b224604bedf270e4c08ba94bbfc1ebfd99c6b9ae Mon Sep 17 00:00:00 2001 From: Andi Date: Tue, 4 Jan 2022 01:30:22 +0100 Subject: [PATCH] DSL based Migrator API and a Updated Change Model (#6) --- .github/workflows/build.yml | 2 +- .github/workflows/pull_request.yml | 2 +- .gitignore | 1 - .swiftlint.yml | 16 +- Package.resolved | 124 +++ Package.swift | 92 +- README.md | 6 +- .../Directories/DirectoryName.swift | 40 - .../Directories/ProjectDirectories.swift | 111 --- Sources/ApodiniMigrator/Exports.swift | 5 +- .../FileFormatter/Indentation.swift | 43 - .../FileFormatter/IndentationFormatter.swift | 95 -- .../FileFormatter/SwiftFileFormatter.swift | 38 - .../Components/Directory.swift | 44 + .../LibraryStructure/Components/Empty.swift | 16 + .../Components/GeneratedFile.swift | 39 + .../Components/ResourceFile.swift | 94 ++ .../Components/Root/ReadMeFile.swift | 20 + .../Components/Root/RootDirectory.swift | 66 ++ .../Components/Root/Sources.swift | 20 + .../Components/Root/StubLinuxMainFile.swift | 33 + .../SwiftPackageFile/PackageDependency.swift | 21 + .../SwiftPackageFile/PackageProduct.swift | 30 + .../Root/SwiftPackageFile/PackageTarget.swift | 99 +++ .../SwiftPackageFile/SwiftPackageFile.swift | 133 +++ .../Root/TargetContainingDirectory.swift | 19 + .../Components/Root/Tests.swift | 20 + .../Components/StringFile.swift | 35 + .../Components/Targets/ExecutableTarget.swift | 20 + .../Components/Targets/Target.swift | 28 + .../Components/Targets/TargetDirectory.swift | 80 ++ .../Components/Targets/TestTarget.swift | 23 + .../LibraryStructure/LibraryComponent.swift | 25 + .../LibraryStructure/LibraryComposite.swift | 60 ++ .../LibraryStructure/LibraryNode.swift | 29 + .../DefaultLibraryComponentBuilder.swift | 49 ++ .../RootLibraryComponentBuilder.swift | 67 ++ .../TargetLibraryComponentBuilder.swift | 55 ++ .../LibraryStructure/SharedNodeStorage.swift | 81 ++ Sources/ApodiniMigrator/Migrator.swift | 67 ++ .../Migrator/Endpoint/APIFile.swift | 56 -- .../Migrator/Endpoint/EndpointFile.swift | 67 -- .../Endpoint/EndpointMethodMigrator.swift | 215 ----- .../Migrator/Endpoint/EndpointsMigrator.swift | 49 -- .../ApodiniMigrator/Migrator/Migrator.swift | 247 ------ .../Models/Enum/EnumEncodeValueMethod.swift | 28 - .../Migrator/Models/Enum/EnumExtensions.swift | 61 -- .../Migrator/Models/Enum/EnumMigrator.swift | 146 ---- .../Migrator/Models/ModelsMigrator.swift | 57 -- .../Models/MultipleFileRenderer.swift | 36 - .../Models/Object/DecoderInitializer.swift | 83 -- .../Models/Object/EncodingMethod.swift | 70 -- .../Models/Object/ObjectMigrator.swift | 139 --- .../Networking/NetworkingMigrator.swift | 46 - .../TestTarget/TestFileTemplate.swift | 97 --- .../NameComponents/GlobalPlaceholders.swift | 25 + .../ApodiniMigrator/NameComponents/Name.swift | 63 ++ .../NameComponents/NameComponent.swift | 18 + .../NameStringInterpolation.swift | 44 + .../Placeholder+NameComponent.swift | 19 + .../NameComponents/Placeholder.swift | 31 + .../String+NameComponent.swift} | 7 +- .../String+ReplacingPlaceholder.swift | 40 + .../SourceCodeBuilder/EmptyLine.swift | 16 + .../SourceCodeBuilder/Group.swift | 31 + .../SourceCodeBuilder/Indent.swift | 43 + .../SourceCodeBuilder/Joined.swift | 68 ++ .../SourceCodeBuilder/SourceCodeBuilder.swift | 88 ++ .../SourceCodeComponent.swift | 27 + .../SourceCodeRenderable.swift | 25 + .../Swift/Annotation.swift} | 52 +- .../Swift/FileHeaderComment.swift | 29 + .../SourceCodeBuilder/Swift/Import.swift | 59 ++ .../SourceCodeBuilder/Swift/Kind.swift | 22 + .../Swift/SwiftFunction.swift | 157 ++++ .../Support/ChangeFilter.swift | 55 -- .../ApodiniMigrator/Support/Renderable.swift | 23 - .../ApodiniMigrator/Support/Resource.swift | 74 -- .../ApodiniMigrator/Support/SwiftFile.swift | 161 ---- .../ApodiniMigrator/Support/Template.swift | 65 -- Sources/ApodiniMigrator/Templates/Readme.md | 2 - Sources/ApodiniMigratorCLI/Compare.swift | 7 +- Sources/ApodiniMigratorCLI/Generate.swift | 14 +- Sources/ApodiniMigratorCLI/Migrate.swift | 15 +- .../ApodiniMigratorCodable+Extensions.swift | 2 +- .../JSScript.swift | 2 +- .../Change/ChangeContextNode.swift | 149 ---- .../Change/ChangeElement.swift | 144 ---- .../Change/Changes/UpdateChange.swift | 203 ----- .../Change/ProviderSupport.swift | 98 --- .../ChangeModel/AnyChange.swift | 43 + .../ChangeModel/Change+Models.swift | 138 +++ .../ChangeModel/Change.swift | 236 +++++ .../ChangeModel/ChangeComparisonContext.swift | 120 +++ .../ChangeModel/ChangeType.swift | 34 + .../ChangeModel/ChangeableElement.swift | 15 + .../ChangeModel/Changes/EndpointChange.swift | 159 ++++ .../Changes/EndpointIdentifierUpdate.swift | 22 + .../ChangeModel/Changes/EnumCaseChange.swift | 65 ++ .../Changes/ExporterConfigurationChange.swift | 22 + .../ChangeModel/Changes/ModelChange.swift | 156 ++++ .../ChangeModel/Changes/ParameterChange.swift | 118 +++ .../ChangeModel/Changes/PropertyChange.swift | 103 +++ .../Changes/ServiceInformationChange.swift | 101 +++ .../ChangeModel/UnsupportedChange.swift | 25 + .../UpdateChangeWithNestedChange.swift | 22 + .../Comparators/Comparator.swift | 39 +- .../Comparators/DocumentComparator.swift | 56 +- .../Endpoint/EndpointComparator.swift | 96 ++- .../Endpoint/EndpointsComparator.swift | 113 +-- .../Endpoint/IdentifiersComparator.swift | 49 ++ .../Endpoint/ParameterComparator.swift | 100 +-- .../Endpoint/ParametersComparator.swift | 133 +-- .../Comparators/MetaDataComparator.swift | 64 -- .../Model/EnumCasesComparator.swift | 68 ++ .../Comparators/Model/EnumComparator.swift | 144 +--- .../Comparators/Model/ModelComparator.swift | 31 +- .../Comparators/Model/ModelsComparator.swift | 106 +-- .../Comparators/Model/ObjectComparator.swift | 135 +-- .../Model/ObjectPropertiesComparator.swift | 118 +++ .../ServiceInformationComparator.swift | 79 ++ .../CompareConfiguration.swift | 9 +- .../JSConvert/JSObjectScript.swift | 21 +- .../JSConvert/JSScriptBuilder.swift | 20 +- .../Changes/LegacyAddChange.swift} | 34 +- .../Changes/LegacyDeleteChange.swift} | 34 +- .../Changes/LegacyUnsupportedChange.swift} | 21 +- .../Changes/LegacyUpdateChange.swift | 68 ++ .../LegacyChange.swift} | 21 +- .../LegacyChangeModel/LegacyChangeArray.swift | 808 ++++++++++++++++++ .../LegacyChangeElement.swift | 66 ++ .../LegacyChangeTargets.swift} | 17 +- .../LegacyChangeType.swift} | 2 +- .../LegacyChangeValue.swift} | 63 +- .../LegacyCompareConfiguration.swift | 30 + .../LegacyProviderSupport.swift | 46 + .../MigrationGuide.swift | 232 +++++ .../MigrationGuide/MigrationGuide.swift | 162 ---- .../RelaxedDeltaIdentifiable.swift | 15 +- .../ServiceInformation+MigrationGuide.swift | 40 + Sources/ApodiniMigratorCore/APIDocument.swift | 136 +++ .../Shared/DeltaIdentifier.swift | 5 +- .../ApodiniMigratorCore/Shared/Value.swift | 2 + .../TypeInformation+FileRenderable.swift | 16 - .../WebService/Document.swift | 137 --- .../WebService/Endpoint.swift | 126 --- .../Endpoint/CommunicationalPattern.swift | 21 + .../WebService/Endpoint/Endpoint.swift | 267 ++++++ .../Endpoint/EndpointIdentifier.swift | 60 ++ .../{ => Endpoint}/EndpointPath.swift | 17 +- .../WebService/{ => Endpoint}/ErrorCode.swift | 0 .../WebService/{ => Endpoint}/Operation.swift | 2 +- .../WebService/{ => Endpoint}/Parameter.swift | 19 +- .../Legacy}/AnyCodableElement.swift | 32 +- .../WebService/Legacy/LegacyEndpoint.swift | 36 + .../Legacy/LegacyServiceInformation.swift | 81 ++ .../ExporterConfiguration.swift | 150 ++++ .../ServiceInformation/HTTPInformation.swift | 37 + .../ServiceInformation.swift | 130 +++ .../WebService/Version.swift | 9 +- .../Extensions/Array+Extensions.swift | 21 +- .../Extensions/Encodable+Extensions.swift | 12 +- .../Extensions/String+Extensions.swift | 69 -- .../ApodiniMigratorShared/FileExtension.swift | 15 - .../CoderConfiguration+Description.swift | 28 + .../DeltaIdentifier+Sanitize.swift | 18 + Sources/RESTMigrator/Endpoint/APIFile.swift | 65 ++ .../Endpoint/DefaultEndpointInput.swift | 0 .../RESTMigrator/Endpoint/EndpointFile.swift | 89 ++ .../Endpoint/EndpointMethodMigrator.swift | 203 +++++ .../Endpoint/EndpointsMigrator.swift | 61 ++ .../Endpoint/MigratedEndpoint.swift | 72 +- .../Endpoint/MigratedParameter.swift | 6 +- .../Exports.swift} | 8 +- .../Models/Enum/DefaultEnumFile.swift | 65 +- .../Models/Enum/EnumDecoderInitializer.swift | 13 +- .../Models/Enum/EnumDeprecatedCases.swift | 13 +- .../Models/Enum/EnumEncodeValueMethod.swift | 29 + .../Models/Enum/EnumEncodingMethod.swift | 19 +- .../Models/Enum/EnumExtensions.swift | 64 ++ .../Models/Enum/EnumMigrator.swift | 153 ++++ .../RESTMigrator/Models/ModelsMigrator.swift | 73 ++ .../Models/Object/DecoderInitializer.swift | 92 ++ .../Models/Object/DefaultObjectFile.swift | 79 +- .../Models/Object/EncodingMethod.swift | 76 ++ .../Models/Object/ObjectCodingKeys.swift | 27 +- .../Models/Object/ObjectInitializer.swift | 41 +- .../Models/Object/ObjectMigrator.swift | 149 ++++ .../Networking/NetworkingMigrator.swift | 68 ++ Sources/RESTMigrator/RESTMigrator.swift | 141 +++ .../Resources/HTTP/ApodiniError.swift} | 0 .../HTTP/ApodiniError.swift.license} | 0 .../Resources/HTTP/HTTPAuthorization.swift} | 0 .../HTTP/HTTPAuthorization.swift.license} | 0 .../Resources/HTTP/HTTPHeaders.swift} | 0 .../Resources/HTTP/HTTPHeaders.swift.license} | 0 .../Resources/HTTP/HTTPMethod.swift} | 0 .../Resources/HTTP/HTTPMethod.swift.license} | 0 .../Resources/HTTP/Parameters.swift} | 0 .../Resources/HTTP/Parameters.swift.license} | 0 .../Resources/Networking/Handler.swift} | 4 +- .../Networking/Handler.swift.license} | 0 .../Networking/NetworkingService.swift} | 44 +- .../NetworkingService.swift.license} | 0 .../Resources/Package.swift} | 0 .../Resources/Package.swift.license} | 0 Sources/RESTMigrator/Resources/Readme.md | 2 + .../Resources}/Readme.md.license | 0 .../Resources/Tests/LinuxMain.swift} | 0 .../Resources/Tests/LinuxMain.swift.license} | 0 .../Resources/Tests/XCTestManifests.swift} | 0 .../Tests/XCTestManifests.swift.license} | 0 .../Resources/Utils/Utils.swift} | 7 +- .../Resources/Utils/Utils.swift.license} | 0 .../TestTarget/ModelTestsFile.swift | 110 +++ .../RESTMigrator/TypeInformation+Unsafe.swift | 41 + ...nyCodableAndRelaxedIdentifiableTests.swift | 26 +- .../EndpointComparatorTests.swift | 446 +++++++--- .../EndpointsComparatorTests.swift | 102 +-- .../EnumComparatorTests.swift | 181 ++-- .../JavaScriptConvertTests.swift | 23 +- .../MetaDataComparatorTests.swift | 95 -- .../ModelsComparatorTests.swift | 155 ++-- .../ObjectComparatorTests.swift | 287 ++++--- .../ServiceInformationComparatorTests.swift | 141 +++ .../ApodiniMigratorModelsTests.swift | 42 +- .../TypeInformationTests.swift | 2 +- .../AuxiliaryFileGeneratorTests.swift | 15 +- .../EndpointMigratorTests.swift | 266 ++++-- .../EnumMigratorTests.swift | 112 ++- .../ObjectMigratorTests.swift | 151 ++-- .../ApodiniMigratorSharedTests.swift | 22 +- ...lientLibraryGenerationMigrationTests.swift | 77 +- .../Auxiliary/{APIFile.md => APIFile.swift} | 2 - ...IFile.md.license => APIFile.swift.license} | 0 ...ModelsTestFile.md => ModelsTestFile.swift} | 14 +- ...d.license => ModelsTestFile.swift.license} | 0 ...pointFile.md => DefaultEndpointFile.swift} | 2 - ...ense => DefaultEndpointFile.swift.license} | 0 ...ge.md => EndpointAddParameterChange.swift} | 2 - ... EndpointAddParameterChange.swift.license} | 0 ...ndpointDeleteContentParameterChange.swift} | 2 - ...eleteContentParameterChange.swift.license} | 0 ...md => EndpointDeleteParameterChange.swift} | 2 - ...dpointDeleteParameterChange.swift.license} | 0 ...dChange.md => EndpointDeletedChange.swift} | 2 - ...se => EndpointDeletedChange.swift.license} | 0 ...anges.md => EndpointMultipleChanges.swift} | 2 - ... => EndpointMultipleChanges.swift.license} | 0 ...hange.md => EndpointOperationChange.swift} | 2 - ... => EndpointOperationChange.swift.license} | 0 ... EndpointParameterKindAndPathChange.swift} | 2 - ...tParameterKindAndPathChange.swift.license} | 0 ...tParameterNecessityToRequiredChange.swift} | 2 - ...erNecessityToRequiredChange.swift.license} | 0 ...e.md => EndpointParameterTypeChange.swift} | 2 - ...EndpointParameterTypeChange.swift.license} | 0 ...PathChange.md => EndpointPathChange.swift} | 2 - ...cense => EndpointPathChange.swift.license} | 0 ...md => EndpointRenameParameterChange.swift} | 2 - ...dpointRenameParameterChange.swift.license} | 0 ...Change.md => EndpointResponseChange.swift} | 2 - ...e => EndpointResponseChange.swift.license} | 0 .../EndpointWrappedContentParameter.swift | 33 + ...ointWrappedContentParameter.swift.license} | 0 ...DefaultIntEnum.md => DefaultIntEnum.swift} | 5 - ...d.license => DefaultIntEnum.swift.license} | 0 ...tObjectFile.md => DefaultObjectFile.swift} | 2 - ...icense => DefaultObjectFile.swift.license} | 0 ...tStringEnum.md => DefaultStringEnum.swift} | 5 - ...icense => DefaultStringEnum.swift.license} | 0 .../{EnumAddedCase.md => EnumAddedCase.swift} | 5 - ...md.license => EnumAddedCase.swift.license} | 0 ...umDeletedCase.md => EnumDeletedCase.swift} | 5 - ....license => EnumDeletedCase.swift.license} | 0 ...umDeletedSelf.md => EnumDeletedSelf.swift} | 5 - ....license => EnumDeletedSelf.swift.license} | 0 ...leChanges.md => EnumMultipleChanges.swift} | 5 - ...ense => EnumMultipleChanges.swift.license} | 0 ...{EnumRenamedCase.md => EnumRawValue.swift} | 5 - ....md.license => EnumRawValue.swift.license} | 0 .../Enum/EnumUnsupportedChange.md | 63 -- .../Enum/EnumUnsupportedChange.swift | 58 ++ .../EnumUnsupportedChange.swift.license} | 0 ...dProperty.md => ObjectAddedProperty.swift} | 2 - ...ense => ObjectAddedProperty.swift.license} | 0 ...tedChange.md => ObjectDeletedChange.swift} | 2 - ...ense => ObjectDeletedChange.swift.license} | 0 ...roperty.md => ObjectDeletedProperty.swift} | 2 - ...se => ObjectDeletedProperty.swift.license} | 0 ...leChange.md => ObjectMultipleChange.swift} | 2 - ...nse => ObjectMultipleChange.swift.license} | 0 ...ctPropertyNecessityToOptionalChange.swift} | 2 - ...tyNecessityToOptionalChange.swift.license} | 0 ...ctPropertyNecessityToRequiredChange.swift} | 2 - ...tyNecessityToRequiredChange.swift.license} | 0 ...ange.md => ObjectPropertyTypeChange.swift} | 2 - ...=> ObjectPropertyTypeChange.swift.license} | 0 ...roperty.md => ObjectRenamedProperty.swift} | 2 - ...se => ObjectRenamedProperty.swift.license} | 0 ...hange.md => ObjectUnsupportedChange.swift} | 4 +- .../ObjectUnsupportedChange.swift.license | 7 + .../Utils/ApodiniMigratorXCTestCase.swift | 55 +- .../Utils/Documents.swift | 19 + .../{Resources.swift => OutputFiles.swift} | 36 +- .../Utils/TestResource.swift | 48 ++ .../Utils/TestTypes.swift | 1 + .../Utils/XCTAssertRuntimeFailure.swift | 45 + codecov.yml | 1 + 309 files changed, 9984 insertions(+), 5426 deletions(-) create mode 100644 Package.resolved delete mode 100644 Sources/ApodiniMigrator/Directories/DirectoryName.swift delete mode 100644 Sources/ApodiniMigrator/Directories/ProjectDirectories.swift delete mode 100644 Sources/ApodiniMigrator/FileFormatter/Indentation.swift delete mode 100644 Sources/ApodiniMigrator/FileFormatter/IndentationFormatter.swift delete mode 100644 Sources/ApodiniMigrator/FileFormatter/SwiftFileFormatter.swift create mode 100644 Sources/ApodiniMigrator/LibraryStructure/Components/Directory.swift create mode 100644 Sources/ApodiniMigrator/LibraryStructure/Components/Empty.swift create mode 100644 Sources/ApodiniMigrator/LibraryStructure/Components/GeneratedFile.swift create mode 100644 Sources/ApodiniMigrator/LibraryStructure/Components/ResourceFile.swift create mode 100644 Sources/ApodiniMigrator/LibraryStructure/Components/Root/ReadMeFile.swift create mode 100644 Sources/ApodiniMigrator/LibraryStructure/Components/Root/RootDirectory.swift create mode 100644 Sources/ApodiniMigrator/LibraryStructure/Components/Root/Sources.swift create mode 100644 Sources/ApodiniMigrator/LibraryStructure/Components/Root/StubLinuxMainFile.swift create mode 100644 Sources/ApodiniMigrator/LibraryStructure/Components/Root/SwiftPackageFile/PackageDependency.swift create mode 100644 Sources/ApodiniMigrator/LibraryStructure/Components/Root/SwiftPackageFile/PackageProduct.swift create mode 100644 Sources/ApodiniMigrator/LibraryStructure/Components/Root/SwiftPackageFile/PackageTarget.swift create mode 100644 Sources/ApodiniMigrator/LibraryStructure/Components/Root/SwiftPackageFile/SwiftPackageFile.swift create mode 100644 Sources/ApodiniMigrator/LibraryStructure/Components/Root/TargetContainingDirectory.swift create mode 100644 Sources/ApodiniMigrator/LibraryStructure/Components/Root/Tests.swift create mode 100644 Sources/ApodiniMigrator/LibraryStructure/Components/StringFile.swift create mode 100644 Sources/ApodiniMigrator/LibraryStructure/Components/Targets/ExecutableTarget.swift create mode 100644 Sources/ApodiniMigrator/LibraryStructure/Components/Targets/Target.swift create mode 100644 Sources/ApodiniMigrator/LibraryStructure/Components/Targets/TargetDirectory.swift create mode 100644 Sources/ApodiniMigrator/LibraryStructure/Components/Targets/TestTarget.swift create mode 100644 Sources/ApodiniMigrator/LibraryStructure/LibraryComponent.swift create mode 100644 Sources/ApodiniMigrator/LibraryStructure/LibraryComposite.swift create mode 100644 Sources/ApodiniMigrator/LibraryStructure/LibraryNode.swift create mode 100644 Sources/ApodiniMigrator/LibraryStructure/ResultBuilder/DefaultLibraryComponentBuilder.swift create mode 100644 Sources/ApodiniMigrator/LibraryStructure/ResultBuilder/RootLibraryComponentBuilder.swift create mode 100644 Sources/ApodiniMigrator/LibraryStructure/ResultBuilder/TargetLibraryComponentBuilder.swift create mode 100644 Sources/ApodiniMigrator/LibraryStructure/SharedNodeStorage.swift create mode 100644 Sources/ApodiniMigrator/Migrator.swift delete mode 100644 Sources/ApodiniMigrator/Migrator/Endpoint/APIFile.swift delete mode 100644 Sources/ApodiniMigrator/Migrator/Endpoint/EndpointFile.swift delete mode 100644 Sources/ApodiniMigrator/Migrator/Endpoint/EndpointMethodMigrator.swift delete mode 100644 Sources/ApodiniMigrator/Migrator/Endpoint/EndpointsMigrator.swift delete mode 100644 Sources/ApodiniMigrator/Migrator/Migrator.swift delete mode 100644 Sources/ApodiniMigrator/Migrator/Models/Enum/EnumEncodeValueMethod.swift delete mode 100644 Sources/ApodiniMigrator/Migrator/Models/Enum/EnumExtensions.swift delete mode 100644 Sources/ApodiniMigrator/Migrator/Models/Enum/EnumMigrator.swift delete mode 100644 Sources/ApodiniMigrator/Migrator/Models/ModelsMigrator.swift delete mode 100644 Sources/ApodiniMigrator/Migrator/Models/MultipleFileRenderer.swift delete mode 100644 Sources/ApodiniMigrator/Migrator/Models/Object/DecoderInitializer.swift delete mode 100644 Sources/ApodiniMigrator/Migrator/Models/Object/EncodingMethod.swift delete mode 100644 Sources/ApodiniMigrator/Migrator/Models/Object/ObjectMigrator.swift delete mode 100644 Sources/ApodiniMigrator/Migrator/Networking/NetworkingMigrator.swift delete mode 100644 Sources/ApodiniMigrator/Migrator/TestTarget/TestFileTemplate.swift create mode 100644 Sources/ApodiniMigrator/NameComponents/GlobalPlaceholders.swift create mode 100644 Sources/ApodiniMigrator/NameComponents/Name.swift create mode 100644 Sources/ApodiniMigrator/NameComponents/NameComponent.swift create mode 100644 Sources/ApodiniMigrator/NameComponents/NameStringInterpolation.swift create mode 100644 Sources/ApodiniMigrator/NameComponents/Placeholder+NameComponent.swift create mode 100644 Sources/ApodiniMigrator/NameComponents/Placeholder.swift rename Sources/{ApodiniMigratorCompare/MigrationGuide/SpecificationType.swift => ApodiniMigrator/NameComponents/String+NameComponent.swift} (67%) create mode 100644 Sources/ApodiniMigrator/NameComponents/String+ReplacingPlaceholder.swift create mode 100644 Sources/ApodiniMigrator/SourceCodeBuilder/EmptyLine.swift create mode 100644 Sources/ApodiniMigrator/SourceCodeBuilder/Group.swift create mode 100644 Sources/ApodiniMigrator/SourceCodeBuilder/Indent.swift create mode 100644 Sources/ApodiniMigrator/SourceCodeBuilder/Joined.swift create mode 100644 Sources/ApodiniMigrator/SourceCodeBuilder/SourceCodeBuilder.swift create mode 100644 Sources/ApodiniMigrator/SourceCodeBuilder/SourceCodeComponent.swift create mode 100644 Sources/ApodiniMigrator/SourceCodeBuilder/SourceCodeRenderable.swift rename Sources/ApodiniMigrator/{Support/MARKComment.swift => SourceCodeBuilder/Swift/Annotation.swift} (53%) create mode 100644 Sources/ApodiniMigrator/SourceCodeBuilder/Swift/FileHeaderComment.swift create mode 100644 Sources/ApodiniMigrator/SourceCodeBuilder/Swift/Import.swift create mode 100644 Sources/ApodiniMigrator/SourceCodeBuilder/Swift/Kind.swift create mode 100644 Sources/ApodiniMigrator/SourceCodeBuilder/Swift/SwiftFunction.swift delete mode 100644 Sources/ApodiniMigrator/Support/ChangeFilter.swift delete mode 100644 Sources/ApodiniMigrator/Support/Renderable.swift delete mode 100644 Sources/ApodiniMigrator/Support/Resource.swift delete mode 100644 Sources/ApodiniMigrator/Support/SwiftFile.swift delete mode 100644 Sources/ApodiniMigrator/Support/Template.swift delete mode 100644 Sources/ApodiniMigrator/Templates/Readme.md delete mode 100644 Sources/ApodiniMigratorCompare/Change/ChangeContextNode.swift delete mode 100644 Sources/ApodiniMigratorCompare/Change/ChangeElement.swift delete mode 100644 Sources/ApodiniMigratorCompare/Change/Changes/UpdateChange.swift delete mode 100644 Sources/ApodiniMigratorCompare/Change/ProviderSupport.swift create mode 100644 Sources/ApodiniMigratorCompare/ChangeModel/AnyChange.swift create mode 100644 Sources/ApodiniMigratorCompare/ChangeModel/Change+Models.swift create mode 100644 Sources/ApodiniMigratorCompare/ChangeModel/Change.swift create mode 100644 Sources/ApodiniMigratorCompare/ChangeModel/ChangeComparisonContext.swift create mode 100644 Sources/ApodiniMigratorCompare/ChangeModel/ChangeType.swift create mode 100644 Sources/ApodiniMigratorCompare/ChangeModel/ChangeableElement.swift create mode 100644 Sources/ApodiniMigratorCompare/ChangeModel/Changes/EndpointChange.swift create mode 100644 Sources/ApodiniMigratorCompare/ChangeModel/Changes/EndpointIdentifierUpdate.swift create mode 100644 Sources/ApodiniMigratorCompare/ChangeModel/Changes/EnumCaseChange.swift create mode 100644 Sources/ApodiniMigratorCompare/ChangeModel/Changes/ExporterConfigurationChange.swift create mode 100644 Sources/ApodiniMigratorCompare/ChangeModel/Changes/ModelChange.swift create mode 100644 Sources/ApodiniMigratorCompare/ChangeModel/Changes/ParameterChange.swift create mode 100644 Sources/ApodiniMigratorCompare/ChangeModel/Changes/PropertyChange.swift create mode 100644 Sources/ApodiniMigratorCompare/ChangeModel/Changes/ServiceInformationChange.swift create mode 100644 Sources/ApodiniMigratorCompare/ChangeModel/UnsupportedChange.swift create mode 100644 Sources/ApodiniMigratorCompare/ChangeModel/UpdateChangeWithNestedChange.swift create mode 100644 Sources/ApodiniMigratorCompare/Comparators/Endpoint/IdentifiersComparator.swift delete mode 100644 Sources/ApodiniMigratorCompare/Comparators/MetaDataComparator.swift create mode 100644 Sources/ApodiniMigratorCompare/Comparators/Model/EnumCasesComparator.swift create mode 100644 Sources/ApodiniMigratorCompare/Comparators/Model/ObjectPropertiesComparator.swift create mode 100644 Sources/ApodiniMigratorCompare/Comparators/ServiceInformationComparator.swift rename Sources/ApodiniMigratorCompare/{Change/Changes/AddChange.swift => LegacyChangeModel/Changes/LegacyAddChange.swift} (54%) rename Sources/ApodiniMigratorCompare/{Change/Changes/DeleteChange.swift => LegacyChangeModel/Changes/LegacyDeleteChange.swift} (53%) rename Sources/ApodiniMigratorCompare/{Change/Changes/UnsupportedChange.swift => LegacyChangeModel/Changes/LegacyUnsupportedChange.swift} (59%) create mode 100644 Sources/ApodiniMigratorCompare/LegacyChangeModel/Changes/LegacyUpdateChange.swift rename Sources/ApodiniMigratorCompare/{Change/Change.swift => LegacyChangeModel/LegacyChange.swift} (53%) create mode 100644 Sources/ApodiniMigratorCompare/LegacyChangeModel/LegacyChangeArray.swift create mode 100644 Sources/ApodiniMigratorCompare/LegacyChangeModel/LegacyChangeElement.swift rename Sources/ApodiniMigratorCompare/{Change/ChangeTargets.swift => LegacyChangeModel/LegacyChangeTargets.swift} (79%) rename Sources/ApodiniMigratorCompare/{Change/ChangeType.swift => LegacyChangeModel/LegacyChangeType.swift} (95%) rename Sources/ApodiniMigratorCompare/{Change/ChangeValue.swift => LegacyChangeModel/LegacyChangeValue.swift} (50%) create mode 100644 Sources/ApodiniMigratorCompare/LegacyChangeModel/LegacyCompareConfiguration.swift create mode 100644 Sources/ApodiniMigratorCompare/LegacyChangeModel/LegacyProviderSupport.swift create mode 100644 Sources/ApodiniMigratorCompare/MigrationGuide.swift delete mode 100644 Sources/ApodiniMigratorCompare/MigrationGuide/MigrationGuide.swift create mode 100644 Sources/ApodiniMigratorCompare/ServiceInformation+MigrationGuide.swift create mode 100644 Sources/ApodiniMigratorCore/APIDocument.swift delete mode 100644 Sources/ApodiniMigratorCore/WebService/Document.swift delete mode 100644 Sources/ApodiniMigratorCore/WebService/Endpoint.swift create mode 100644 Sources/ApodiniMigratorCore/WebService/Endpoint/CommunicationalPattern.swift create mode 100644 Sources/ApodiniMigratorCore/WebService/Endpoint/Endpoint.swift create mode 100644 Sources/ApodiniMigratorCore/WebService/Endpoint/EndpointIdentifier.swift rename Sources/ApodiniMigratorCore/WebService/{ => Endpoint}/EndpointPath.swift (90%) rename Sources/ApodiniMigratorCore/WebService/{ => Endpoint}/ErrorCode.swift (100%) rename Sources/ApodiniMigratorCore/WebService/{ => Endpoint}/Operation.swift (89%) rename Sources/ApodiniMigratorCore/WebService/{ => Endpoint}/Parameter.swift (93%) rename Sources/{ApodiniMigratorCompare => ApodiniMigratorCore/WebService/Legacy}/AnyCodableElement.swift (87%) create mode 100644 Sources/ApodiniMigratorCore/WebService/Legacy/LegacyEndpoint.swift create mode 100644 Sources/ApodiniMigratorCore/WebService/Legacy/LegacyServiceInformation.swift create mode 100644 Sources/ApodiniMigratorCore/WebService/ServiceInformation/ExporterConfiguration.swift create mode 100644 Sources/ApodiniMigratorCore/WebService/ServiceInformation/HTTPInformation.swift create mode 100644 Sources/ApodiniMigratorCore/WebService/ServiceInformation/ServiceInformation.swift create mode 100644 Sources/RESTMigrator/CoderConfiguration+Description.swift create mode 100644 Sources/RESTMigrator/DeltaIdentifier+Sanitize.swift create mode 100644 Sources/RESTMigrator/Endpoint/APIFile.swift rename Sources/{ApodiniMigrator/Migrator => RESTMigrator}/Endpoint/DefaultEndpointInput.swift (100%) create mode 100644 Sources/RESTMigrator/Endpoint/EndpointFile.swift create mode 100644 Sources/RESTMigrator/Endpoint/EndpointMethodMigrator.swift create mode 100644 Sources/RESTMigrator/Endpoint/EndpointsMigrator.swift rename Sources/{ApodiniMigrator/Migrator => RESTMigrator}/Endpoint/MigratedEndpoint.swift (70%) rename Sources/{ApodiniMigrator/Migrator => RESTMigrator}/Endpoint/MigratedParameter.swift (93%) rename Sources/{ApodiniMigratorCompare/MigrationGuide/ServiceType.swift => RESTMigrator/Exports.swift} (68%) rename Sources/{ApodiniMigrator/Migrator => RESTMigrator}/Models/Enum/DefaultEnumFile.swift (73%) rename Sources/{ApodiniMigrator/Migrator => RESTMigrator}/Models/Enum/EnumDecoderInitializer.swift (71%) rename Sources/{ApodiniMigrator/Migrator => RESTMigrator}/Models/Enum/EnumDeprecatedCases.swift (76%) create mode 100644 Sources/RESTMigrator/Models/Enum/EnumEncodeValueMethod.swift rename Sources/{ApodiniMigrator/Migrator => RESTMigrator}/Models/Enum/EnumEncodingMethod.swift (53%) create mode 100644 Sources/RESTMigrator/Models/Enum/EnumExtensions.swift create mode 100644 Sources/RESTMigrator/Models/Enum/EnumMigrator.swift create mode 100644 Sources/RESTMigrator/Models/ModelsMigrator.swift create mode 100644 Sources/RESTMigrator/Models/Object/DecoderInitializer.swift rename Sources/{ApodiniMigrator/Migrator => RESTMigrator}/Models/Object/DefaultObjectFile.swift (63%) create mode 100644 Sources/RESTMigrator/Models/Object/EncodingMethod.swift rename Sources/{ApodiniMigrator/Migrator => RESTMigrator}/Models/Object/ObjectCodingKeys.swift (54%) rename Sources/{ApodiniMigrator/Migrator => RESTMigrator}/Models/Object/ObjectInitializer.swift (63%) create mode 100644 Sources/RESTMigrator/Models/Object/ObjectMigrator.swift create mode 100644 Sources/RESTMigrator/Networking/NetworkingMigrator.swift create mode 100644 Sources/RESTMigrator/RESTMigrator.swift rename Sources/{ApodiniMigrator/Templates/HTTP/ApodiniError.md => RESTMigrator/Resources/HTTP/ApodiniError.swift} (100%) rename Sources/{ApodiniMigrator/Templates/HTTP/ApodiniError.md.license => RESTMigrator/Resources/HTTP/ApodiniError.swift.license} (100%) rename Sources/{ApodiniMigrator/Templates/HTTP/HTTPAuthorization.md => RESTMigrator/Resources/HTTP/HTTPAuthorization.swift} (100%) rename Sources/{ApodiniMigrator/Templates/HTTP/HTTPAuthorization.md.license => RESTMigrator/Resources/HTTP/HTTPAuthorization.swift.license} (100%) rename Sources/{ApodiniMigrator/Templates/HTTP/HTTPHeaders.md => RESTMigrator/Resources/HTTP/HTTPHeaders.swift} (100%) rename Sources/{ApodiniMigrator/Templates/HTTP/HTTPHeaders.md.license => RESTMigrator/Resources/HTTP/HTTPHeaders.swift.license} (100%) rename Sources/{ApodiniMigrator/Templates/HTTP/HTTPMethod.md => RESTMigrator/Resources/HTTP/HTTPMethod.swift} (100%) rename Sources/{ApodiniMigrator/Templates/HTTP/HTTPMethod.md.license => RESTMigrator/Resources/HTTP/HTTPMethod.swift.license} (100%) rename Sources/{ApodiniMigrator/Templates/HTTP/Parameters.md => RESTMigrator/Resources/HTTP/Parameters.swift} (100%) rename Sources/{ApodiniMigrator/Templates/HTTP/Parameters.md.license => RESTMigrator/Resources/HTTP/Parameters.swift.license} (100%) rename Sources/{ApodiniMigrator/Templates/Networking/Handler.md => RESTMigrator/Resources/Networking/Handler.swift} (97%) rename Sources/{ApodiniMigrator/Templates/Networking/Handler.md.license => RESTMigrator/Resources/Networking/Handler.swift.license} (100%) rename Sources/{ApodiniMigrator/Templates/Networking/NetworkingService.md => RESTMigrator/Resources/Networking/NetworkingService.swift} (70%) rename Sources/{ApodiniMigrator/Templates/Networking/NetworkingService.md.license => RESTMigrator/Resources/Networking/NetworkingService.swift.license} (100%) rename Sources/{ApodiniMigrator/Templates/Package.md => RESTMigrator/Resources/Package.swift} (100%) rename Sources/{ApodiniMigrator/Templates/Package.md.license => RESTMigrator/Resources/Package.swift.license} (100%) create mode 100644 Sources/RESTMigrator/Resources/Readme.md rename Sources/{ApodiniMigrator/Templates => RESTMigrator/Resources}/Readme.md.license (100%) rename Sources/{ApodiniMigrator/Templates/Tests/LinuxMain.md => RESTMigrator/Resources/Tests/LinuxMain.swift} (100%) rename Sources/{ApodiniMigrator/Templates/Tests/LinuxMain.md.license => RESTMigrator/Resources/Tests/LinuxMain.swift.license} (100%) rename Sources/{ApodiniMigrator/Templates/Tests/XCTestManifests.md => RESTMigrator/Resources/Tests/XCTestManifests.swift} (100%) rename Sources/{ApodiniMigrator/Templates/Tests/XCTestManifests.md.license => RESTMigrator/Resources/Tests/XCTestManifests.swift.license} (100%) rename Sources/{ApodiniMigrator/Templates/Utils/Utils.md => RESTMigrator/Resources/Utils/Utils.swift} (93%) rename Sources/{ApodiniMigrator/Templates/Utils/Utils.md.license => RESTMigrator/Resources/Utils/Utils.swift.license} (100%) create mode 100644 Sources/RESTMigrator/TestTarget/ModelTestsFile.swift create mode 100644 Sources/RESTMigrator/TypeInformation+Unsafe.swift delete mode 100644 Tests/ApodiniMigratorTests/ApodiniMigratorCompareTests/MetaDataComparatorTests.swift create mode 100644 Tests/ApodiniMigratorTests/ApodiniMigratorCompareTests/ServiceInformationComparatorTests.swift rename Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Auxiliary/{APIFile.md => APIFile.swift} (96%) rename Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Auxiliary/{APIFile.md.license => APIFile.swift.license} (100%) rename Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Auxiliary/{ModelsTestFile.md => ModelsTestFile.swift} (86%) rename Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Auxiliary/{ModelsTestFile.md.license => ModelsTestFile.swift.license} (100%) rename Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Endpoint/{DefaultEndpointFile.md => DefaultEndpointFile.swift} (97%) rename Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Endpoint/{DefaultEndpointFile.md.license => DefaultEndpointFile.swift.license} (100%) rename Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Endpoint/{EndpointAddParameterChange.md => EndpointAddParameterChange.swift} (97%) rename Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Endpoint/{EndpointAddParameterChange.md.license => EndpointAddParameterChange.swift.license} (100%) rename Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Endpoint/{EndpointDeleteContentParameterChange.md => EndpointDeleteContentParameterChange.swift} (97%) rename Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Endpoint/{EndpointDeleteContentParameterChange.md.license => EndpointDeleteContentParameterChange.swift.license} (100%) rename Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Endpoint/{EndpointDeleteParameterChange.md => EndpointDeleteParameterChange.swift} (97%) rename Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Endpoint/{EndpointDeleteParameterChange.md.license => EndpointDeleteParameterChange.swift.license} (100%) rename Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Endpoint/{EndpointDeletedChange.md => EndpointDeletedChange.swift} (95%) rename Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Endpoint/{EndpointDeletedChange.md.license => EndpointDeletedChange.swift.license} (100%) rename Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Endpoint/{EndpointMultipleChanges.md => EndpointMultipleChanges.swift} (97%) rename Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Endpoint/{EndpointMultipleChanges.md.license => EndpointMultipleChanges.swift.license} (100%) rename Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Endpoint/{EndpointOperationChange.md => EndpointOperationChange.swift} (97%) rename Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Endpoint/{EndpointOperationChange.md.license => EndpointOperationChange.swift.license} (100%) rename Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Endpoint/{EndpointParameterKindAndPathChange.md => EndpointParameterKindAndPathChange.swift} (97%) rename Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Endpoint/{EndpointParameterKindAndPathChange.md.license => EndpointParameterKindAndPathChange.swift.license} (100%) rename Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Endpoint/{EndpointParameterNecessityToRequiredChange.md => EndpointParameterNecessityToRequiredChange.swift} (97%) rename Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Endpoint/{EndpointParameterNecessityToRequiredChange.md.license => EndpointParameterNecessityToRequiredChange.swift.license} (100%) rename Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Endpoint/{EndpointParameterTypeChange.md => EndpointParameterTypeChange.swift} (97%) rename Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Endpoint/{EndpointParameterTypeChange.md.license => EndpointParameterTypeChange.swift.license} (100%) rename Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Endpoint/{EndpointPathChange.md => EndpointPathChange.swift} (97%) rename Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Endpoint/{EndpointPathChange.md.license => EndpointPathChange.swift.license} (100%) rename Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Endpoint/{EndpointRenameParameterChange.md => EndpointRenameParameterChange.swift} (97%) rename Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Endpoint/{EndpointRenameParameterChange.md.license => EndpointRenameParameterChange.swift.license} (100%) rename Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Endpoint/{EndpointResponseChange.md => EndpointResponseChange.swift} (97%) rename Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Endpoint/{EndpointResponseChange.md.license => EndpointResponseChange.swift.license} (100%) create mode 100644 Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Endpoint/EndpointWrappedContentParameter.swift rename Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/{Enum/DefaultIntEnum.md.license => Endpoint/EndpointWrappedContentParameter.swift.license} (100%) rename Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Enum/{DefaultIntEnum.md => DefaultIntEnum.swift} (91%) rename Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Enum/{DefaultObjectFile.md.license => DefaultIntEnum.swift.license} (100%) rename Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Enum/{DefaultObjectFile.md => DefaultObjectFile.swift} (99%) rename Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Enum/{DefaultStringEnum.md.license => DefaultObjectFile.swift.license} (100%) rename Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Enum/{DefaultStringEnum.md => DefaultStringEnum.swift} (90%) rename Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Enum/{EnumAddedCase.md.license => DefaultStringEnum.swift.license} (100%) rename Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Enum/{EnumAddedCase.md => EnumAddedCase.swift} (91%) rename Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Enum/{EnumDeletedCase.md.license => EnumAddedCase.swift.license} (100%) rename Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Enum/{EnumDeletedCase.md => EnumDeletedCase.swift} (90%) rename Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Enum/{EnumDeletedSelf.md.license => EnumDeletedCase.swift.license} (100%) rename Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Enum/{EnumDeletedSelf.md => EnumDeletedSelf.swift} (91%) rename Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Enum/{EnumMultipleChanges.md.license => EnumDeletedSelf.swift.license} (100%) rename Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Enum/{EnumMultipleChanges.md => EnumMultipleChanges.swift} (91%) rename Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Enum/{EnumRenamedCase.md.license => EnumMultipleChanges.swift.license} (100%) rename Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Enum/{EnumRenamedCase.md => EnumRawValue.swift} (90%) rename Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Enum/{EnumUnsupportedChange.md.license => EnumRawValue.swift.license} (100%) delete mode 100644 Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Enum/EnumUnsupportedChange.md create mode 100644 Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Enum/EnumUnsupportedChange.swift rename Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/{Object/ObjectAddedProperty.md.license => Enum/EnumUnsupportedChange.swift.license} (100%) rename Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Object/{ObjectAddedProperty.md => ObjectAddedProperty.swift} (99%) rename Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Object/{ObjectDeletedChange.md.license => ObjectAddedProperty.swift.license} (100%) rename Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Object/{ObjectDeletedChange.md => ObjectDeletedChange.swift} (99%) rename Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Object/{ObjectDeletedProperty.md.license => ObjectDeletedChange.swift.license} (100%) rename Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Object/{ObjectDeletedProperty.md => ObjectDeletedProperty.swift} (99%) rename Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Object/{ObjectMultipleChange.md.license => ObjectDeletedProperty.swift.license} (100%) rename Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Object/{ObjectMultipleChange.md => ObjectMultipleChange.swift} (99%) rename Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Object/{ObjectPropertyNecessityToOptionalChange.md.license => ObjectMultipleChange.swift.license} (100%) rename Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Object/{ObjectPropertyNecessityToOptionalChange.md => ObjectPropertyNecessityToOptionalChange.swift} (99%) rename Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Object/{ObjectPropertyNecessityToRequiredChange.md.license => ObjectPropertyNecessityToOptionalChange.swift.license} (100%) rename Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Object/{ObjectPropertyNecessityToRequiredChange.md => ObjectPropertyNecessityToRequiredChange.swift} (99%) rename Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Object/{ObjectPropertyTypeChange.md.license => ObjectPropertyNecessityToRequiredChange.swift.license} (100%) rename Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Object/{ObjectPropertyTypeChange.md => ObjectPropertyTypeChange.swift} (99%) rename Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Object/{ObjectRenamedProperty.md.license => ObjectPropertyTypeChange.swift.license} (100%) rename Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Object/{ObjectRenamedProperty.md => ObjectRenamedProperty.swift} (99%) rename Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Object/{ObjectUnsupportedChange.md.license => ObjectRenamedProperty.swift.license} (100%) rename Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Object/{ObjectUnsupportedChange.md => ObjectUnsupportedChange.swift} (92%) create mode 100644 Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Object/ObjectUnsupportedChange.swift.license create mode 100644 Tests/ApodiniMigratorTests/Utils/Documents.swift rename Tests/ApodiniMigratorTests/Utils/{Resources.swift => OutputFiles.swift} (70%) create mode 100644 Tests/ApodiniMigratorTests/Utils/TestResource.swift create mode 100644 Tests/ApodiniMigratorTests/Utils/XCTAssertRuntimeFailure.swift diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 464b7cdb..47843852 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -20,5 +20,5 @@ jobs: uses: Apodini/.github/.github/workflows/build-and-test.yml@main with: packagename: ApodiniMigrator - usexcodebuild: false + supportsmacos11: true testdocc: false diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 2d2d9c99..4e148370 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -18,7 +18,7 @@ jobs: uses: Apodini/.github/.github/workflows/build-and-test.yml@main with: packagename: ApodiniMigrator - usexcodebuild: false + supportsmacos11: true testdocc: false reuse_action: name: REUSE Compliance Check diff --git a/.gitignore b/.gitignore index 63eb81ea..44776cfc 100644 --- a/.gitignore +++ b/.gitignore @@ -11,7 +11,6 @@ .idea # Swift Package Manager -Package.resolved *.xcodeproj .swiftpm .build/ diff --git a/.swiftlint.yml b/.swiftlint.yml index bfd36563..467b9a0f 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -325,17 +325,16 @@ only_rules: excluded: # paths to ignore during linting. Takes precedence over `included`. - .build - .swiftpm - - Tests - - Sources/ApodiniMigratorCompare/Change/ProviderSupport.swift - - Sources/ApodiniMigratorCompare/AnyCodableElement.swift + - Sources/RESTMigrator/Resources + - Tests/ApodiniMigratorTests/Resources closure_body_length: # Closure bodies should not span too many lines. - 35 # warning - default: 20 - 35 # error - default: 100 enum_case_associated_values_count: # Number of associated values in an enum case should be low - - 5 # warning - default: 5 - - 5 # error - default: 6 + - 8 # warning - default: 5 + - 8 # error - default: 6 file_length: # Files should not span too many lines. - 500 # warning - default: 400 @@ -351,18 +350,21 @@ function_parameter_count: # Number of function parameters should be low. identifier_name: excluded: # excluded names + - v1 + - v2 - id - ok - or - to + - at large_tuple: # Tuples shouldn't have too many members. Create a custom type instead. - 2 # warning - default: 2 - 2 # error - default: 3 line_length: # Lines should not span too many characters. - warning: 200 # default: 120 - error: 200 + warning: 150 # default: 120 + error: 150 ignores_comments: true # default: false ignores_urls: true # default: false ignores_function_declarations: false # default: false diff --git a/Package.resolved b/Package.resolved new file mode 100644 index 00000000..35822b09 --- /dev/null +++ b/Package.resolved @@ -0,0 +1,124 @@ +{ + "object": { + "pins": [ + { + "package": "ApodiniTypeInformation", + "repositoryURL": "https://github.com/Apodini/ApodiniTypeInformation.git", + "state": { + "branch": null, + "revision": "b4e73a6f92bd0930c9f1f8a857504b85b943dd88", + "version": "0.3.0" + } + }, + { + "package": "AssociatedTypeRequirementsKit", + "repositoryURL": "https://github.com/nerdsupremacist/AssociatedTypeRequirementsKit.git", + "state": { + "branch": null, + "revision": "2e4c49c21ffb2135f1c99fbfcf2119c9d24f5e8c", + "version": "0.3.2" + } + }, + { + "package": "FineJSON", + "repositoryURL": "https://github.com/omochi/FineJSON.git", + "state": { + "branch": null, + "revision": "05101709243cb66d80c92e645210a3b80cf4e17f", + "version": "1.14.0" + } + }, + { + "package": "MetadataSystem", + "repositoryURL": "https://github.com/Apodini/MetadataSystem.git", + "state": { + "branch": null, + "revision": "06cac46a958fdf700054f12f682b5c8f27577c9f", + "version": "0.1.1" + } + }, + { + "package": "PathKit", + "repositoryURL": "https://github.com/kylef/PathKit.git", + "state": { + "branch": null, + "revision": "3bfd2737b700b9a36565a8c94f4ad2b050a5e574", + "version": "1.0.1" + } + }, + { + "package": "RichJSONParser", + "repositoryURL": "https://github.com/omochi/RichJSONParser.git", + "state": { + "branch": null, + "revision": "263e2ecfe88d0500fa99e4cbc8c948529d335534", + "version": "3.0.0" + } + }, + { + "package": "Runtime", + "repositoryURL": "https://github.com/wickwirew/Runtime.git", + "state": { + "branch": null, + "revision": "dad03135d7701a4e7b3a4051e75d6b37bd8e178e", + "version": "2.2.4" + } + }, + { + "package": "Spectre", + "repositoryURL": "https://github.com/kylef/Spectre.git", + "state": { + "branch": null, + "revision": "26cc5e9ae0947092c7139ef7ba612e34646086c7", + "version": "0.10.1" + } + }, + { + "package": "swift-argument-parser", + "repositoryURL": "https://github.com/apple/swift-argument-parser", + "state": { + "branch": null, + "revision": "83b23d940471b313427da226196661856f6ba3e0", + "version": "0.4.4" + } + }, + { + "package": "swift-collections", + "repositoryURL": "https://github.com/apple/swift-collections.git", + "state": { + "branch": null, + "revision": "48254824bb4248676bf7ce56014ff57b142b77eb", + "version": "1.0.2" + } + }, + { + "package": "swift-log", + "repositoryURL": "https://github.com/apple/swift-log.git", + "state": { + "branch": null, + "revision": "5d66f7ba25daf4f94100e7022febf3c75e37a6c7", + "version": "1.4.2" + } + }, + { + "package": "XCTAssertCrash", + "repositoryURL": "https://github.com/norio-nomura/XCTAssertCrash.git", + "state": { + "branch": null, + "revision": "880c5241254da53f32caf77248ee3d25cb2a9630", + "version": "0.2.0" + } + }, + { + "package": "Yams", + "repositoryURL": "https://github.com/jpsim/Yams.git", + "state": { + "branch": null, + "revision": "9ff1cc9327586db4e0c8f46f064b6a82ec1566fa", + "version": "4.0.6" + } + } + ] + }, + "version": 1 +} diff --git a/Package.swift b/Package.swift index b5803ccc..70e50fe6 100644 --- a/Package.swift +++ b/Package.swift @@ -18,81 +18,113 @@ let package = Package( .iOS(.v13) ], products: [ - // Products define the executables and libraries a package produces, and make them visible to other packages. - .library(name: "ApodiniMigratorCore", targets: ["ApodiniMigratorCore"]), - .library(name: "ApodiniMigrator", targets: ["ApodiniMigrator"]), .library(name: "ApodiniMigratorShared", targets: ["ApodiniMigratorShared"]), + .library(name: "ApodiniMigratorCore", targets: ["ApodiniMigratorCore"]), .library(name: "ApodiniMigratorClientSupport", targets: ["ApodiniMigratorClientSupport"]), .library(name: "ApodiniMigratorCompare", targets: ["ApodiniMigratorCompare"]), + .library(name: "ApodiniMigrator", targets: ["ApodiniMigrator"]), + .library(name: "RESTMigrator", targets: ["RESTMigrator"]), .executable(name: "migrator", targets: ["ApodiniMigratorCLI"]) ], dependencies: [ - // Dependencies declare other packages that this package depends on. - .package(url: "https://github.com/Apodini/ApodiniTypeInformation.git", .upToNextMinor(from: "0.2.0")), + .package(url: "https://github.com/Apodini/ApodiniTypeInformation.git", .upToNextMinor(from: "0.3.0")), .package(url: "https://github.com/kylef/PathKit.git", from: "1.0.1"), .package(url: "https://github.com/apple/swift-argument-parser", .upToNextMinor(from: "0.4.0")), .package(url: "https://github.com/apple/swift-log.git", from: "1.0.0"), .package(url: "https://github.com/omochi/FineJSON.git", from: "1.14.0"), - .package(url: "https://github.com/jpsim/Yams.git", from: "4.0.0") + .package(url: "https://github.com/jpsim/Yams.git", from: "4.0.0"), + .package(url: "https://github.com/apple/swift-collections.git", .upToNextMajor(from: "1.0.0")), + + // testing runtime crashes + .package(url: "https://github.com/norio-nomura/XCTAssertCrash.git", from: "0.2.0") ], targets: [ - // Targets are the basic building blocks of a package. A target can define a module or a test suite. - // Targets can depend on other targets in this package, and on products in packages this package depends on. + // The lowest level ApodiniMigrator package providing common API used across several targets, including + // common file extensions, encoding and decoding strategies and output formatting .target( - name: "ApodiniMigratorCore", + name: "ApodiniMigratorShared", dependencies: [ - .target(name: "ApodiniMigratorShared"), - .product(name: "ApodiniTypeInformation", package: "ApodiniTypeInformation"), + .product(name: "PathKit", package: "PathKit"), + .product(name: "FineJSON", package: "FineJSON"), .product(name: "Yams", package: "Yams") ] ), - .executableTarget( - name: "ApodiniMigratorCLI", + + // The core ApodiniMigrator package. It provides access to the TypeInformation framework and introduces + // the generalized API document. + .target( + name: "ApodiniMigratorCore", dependencies: [ - .target(name: "ApodiniMigrator"), - .product(name: "ArgumentParser", package: "swift-argument-parser"), - .product(name: "Logging", package: "swift-log") + .target(name: "ApodiniMigratorShared"), + .product(name: "ApodiniTypeInformation", package: "ApodiniTypeInformation"), + .product(name: "Yams", package: "Yams"), + .product(name: "OrderedCollections", package: "swift-collections") ] ), + + // This target provides any necessary interfaces for REST client libraries! .target( name: "ApodiniMigratorClientSupport", dependencies: [ .target(name: "ApodiniMigratorCore") ] ), + + // The Compare target builds upon the Core package containing the generalized MigrationGuide + // and all the necessary utilities for the comparison algorithms. + .target( + name: "ApodiniMigratorCompare", + dependencies: [ + .target(name: "ApodiniMigratorClientSupport") + ] + ), + + // The Migrator package provides the Migrator Interface. So everything which is required + // to build your own Migrator. LibraryStructure generation, Source Code generation, ... .target( name: "ApodiniMigrator", dependencies: [ .target(name: "ApodiniMigratorCompare"), - .target(name: "ApodiniMigratorClientSupport"), .product(name: "Logging", package: "swift-log") - ], - resources: [ - .process("Templates") ] ), + + // This target packages the REST client library generator and migrator. + // Further it contain the template files for the REST client library. .target( - name: "ApodiniMigratorShared", + name: "RESTMigrator", dependencies: [ - .product(name: "PathKit", package: "PathKit"), - .product(name: "FineJSON", package: "FineJSON"), - .product(name: "Yams", package: "Yams") + .target(name: "ApodiniMigrator"), + .target(name: "ApodiniMigratorCompare"), + .target(name: "ApodiniMigratorClientSupport"), + .product(name: "Logging", package: "swift-log") + ], + resources: [ + .process("Resources") ] ), - - .target( - name: "ApodiniMigratorCompare", + + // This target implements the command line interface of the ApodiniMigrator utility. + // It offers command to generate and migrate client libraries and a sub command + // to compare API documents. + .executableTarget( + name: "ApodiniMigratorCLI", dependencies: [ - .target(name: "ApodiniMigratorClientSupport") + .target(name: "RESTMigrator"), + .product(name: "ArgumentParser", package: "swift-argument-parser"), + .product(name: "Logging", package: "swift-log") ] ), + + // The unified test target. .testTarget( name: "ApodiniMigratorTests", dependencies: [ "ApodiniMigratorCore", - "ApodiniMigrator", + "RESTMigrator", "ApodiniMigratorCompare", - "ApodiniMigratorClientSupport" + "ApodiniMigratorClientSupport", + .product(name: "XCTAssertCrash", package: "XCTAssertCrash", condition: .when(platforms: [.macOS])) ], resources: [ .process("Resources") diff --git a/README.md b/README.md index 45b18dbf..f36c2c8d 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ SPDX-License-Identifier: MIT [![Build and Test](https://github.com/Apodini/ApodiniMigrator/actions/workflows/build-and-test.yml/badge.svg)](https://github.com/Apodini/ApodiniMigrator/actions/workflows/build-and-test.yml) [![codecov](https://codecov.io/gh/Apodini/ApodiniMigrator/branch/develop/graph/badge.svg?token=5MMKMPO5NR)](https://codecov.io/gh/Apodini/ApodiniMigrator) -`ApodiniMigrator` is a Swift package that performs several automated tasks, to migrate client applications after a Web Service publishes a new version that contains breaking changes. The tasks include automated generation of an intermediary client library that contains all required components to establish a client-server communication. Furthermore, `ApodiniMigrator` is able to automatically generate a machine-readable migration guide in either `json` or `yaml` format, that describes the changes between two subsequent Web API versions, and includes auxiliary migrating actions. By means of the migration guide, `ApodiniMigrator` can automatically migrate the intermadiary client library, ensuring therefore the compatibility with the new Web API version. It is part of [**Apodini**](https://github.com/Apodini/Apodini), a composable framework to build Web Services in using Swift. +`ApodiniMigrator` is a Swift package that performs several automated tasks, to migrate client applications after a Web Service publishes a new version that contains breaking changes. The tasks include automated generation of an intermediary client library that contains all required components to establish a client-server communication. Furthermore, `ApodiniMigrator` is able to automatically generate a machine-readable migration guide in either `json` or `yaml` format, that describes the changes between two subsequent Web API versions, and includes auxiliary migrating actions. By means of the migration guide, `ApodiniMigrator` can automatically migrate the intermediary client library, ensuring therefore the compatibility with the new Web API version. It is part of [**Apodini**](https://github.com/Apodini/Apodini), a composable framework to build Web Services in using Swift. ## Requirements @@ -168,7 +168,7 @@ info org.apodini.migrator : Starting generation of the migration guide... info org.apodini.migrator : Migration guide was generated successfully at /path/to/ApodiniMigrator/Resources/ExampleDocuments/migration_guide.json. ``` -Once the migration guide has been generate, use `migrate` argument to migrate the initial library: +Once the migration guide has been generated, use `migrate` argument to migrate the initial library: ```console $ ./migrator migrate @@ -185,7 +185,7 @@ info org.apodini.migrator : Package QONECTIQ was migrated successfully. You can ``` ## Contributing -Contributions to this projects are welcome. Please make sure to read the [contribution guidelines](https://github.com/Apodini/.github/blob/release/CONTRIBUTING.md) first. +Contributions to the projects are welcome. Please make sure to read the [contribution guidelines](https://github.com/Apodini/.github/blob/release/CONTRIBUTING.md) first. ## License This project is licensed under the MIT License. See [License](https://github.com/Apodini/ApodiniMigrator/blob/develop/LICENSES) for more information. diff --git a/Sources/ApodiniMigrator/Directories/DirectoryName.swift b/Sources/ApodiniMigrator/Directories/DirectoryName.swift deleted file mode 100644 index 78985247..00000000 --- a/Sources/ApodiniMigrator/Directories/DirectoryName.swift +++ /dev/null @@ -1,40 +0,0 @@ -// -// This source file is part of the Apodini open source project -// -// SPDX-FileCopyrightText: 2019-2021 Paul Schmiedmayer and the Apodini project authors (see CONTRIBUTORS.md) -// -// SPDX-License-Identifier: MIT -// - -import Foundation - -/// An enum that defines the names of the directories of a package generated by ApodiniMigrator -public enum DirectoryName: String { - /// Sources - case sources = "Sources" - /// HTTP - case http = "HTTP" - /// Models - case models = "Models" - /// Resources - case resources = "Resources" - /// Endpoints - case endpoints = "Endpoints" - /// Networking - case networking = "Networking" - /// Utils - case utils = "Utils" - /// Tests - case tests = "Tests" -} - -/// Path + DirectoryName -extension Path { - init(_ directoryName: DirectoryName) { - self.init(directoryName.rawValue) - } - - static func + (lhs: Path, rhs: DirectoryName) -> Self { - lhs + Path(rhs) - } -} diff --git a/Sources/ApodiniMigrator/Directories/ProjectDirectories.swift b/Sources/ApodiniMigrator/Directories/ProjectDirectories.swift deleted file mode 100644 index b02613e6..00000000 --- a/Sources/ApodiniMigrator/Directories/ProjectDirectories.swift +++ /dev/null @@ -1,111 +0,0 @@ -// -// This source file is part of the Apodini open source project -// -// SPDX-FileCopyrightText: 2019-2021 Paul Schmiedmayer and the Apodini project authors (see CONTRIBUTORS.md) -// -// SPDX-License-Identifier: MIT -// - -import Foundation - -/// An object that defines project directories out of a `packageName` and a `packagePath` -public struct ProjectDirectories { - /// Name of the package - public let packageName: String - /// Path where the package should be created - /// - Note: without package name - public let packagePath: Path - - /// Root directory of the package - public var root: Path { - packagePath + packageName - } - - /// Sources directory of the package - public var sources: Path { - root + .sources - } - - /// Target directory of the package - public var target: Path { - sources + packageName - } - - /// `HTTP` directory of the package - public var http: Path { - target + .http - } - - /// `Resources` directory path - public var resources: Path { - target + .resources - } - - /// `Models` directory of the package - public var models: Path { - target + .models - } - - /// `Endpoints` directory of the package - public var endpoints: Path { - target + .endpoints - } - - /// `Networking` directory of the package - public var networking: Path { - target + .networking - } - - /// `Utils` directory of the package - public var utils: Path { - target + DirectoryName.utils - } - - /// `Tests` directory of the package - public var tests: Path { - root + .tests - } - - /// Test target directory of the package - public var testsTarget: Path { - tests + Path(packageName + "Tests") - } - - /// All directories of the package that contain files - var allDirectories: [Path] { - [http, resources, models, endpoints, networking, utils, testsTarget] - } - - /// Initializes `self` out of a `packageName` and a `packagePath` - public init(packageName: String, packagePath: Path) { - self.packageName = packageName - self.packagePath = packagePath - } - - /// Initializes `self` out of a `packageName` and a string `packagePath` - public init(packageName: String, packagePath: String) { - self.packageName = packageName - self.packagePath = packagePath.asPath - } - - /// Creates empty directories of the package - public func build() throws { - try? root.delete() - - try allDirectories.forEach { try $0.mkpath() } - } - - /// A util function that returns the path from a `DirectoryName` - func path(of directory: DirectoryName) -> Path { - switch directory { - case .sources: return sources - case .http: return http - case .resources: return resources - case .models: return models - case .endpoints: return endpoints - case .networking: return networking - case .utils: return utils - case .tests: return tests - } - } -} diff --git a/Sources/ApodiniMigrator/Exports.swift b/Sources/ApodiniMigrator/Exports.swift index 6fe0f1c8..3110fde9 100644 --- a/Sources/ApodiniMigrator/Exports.swift +++ b/Sources/ApodiniMigrator/Exports.swift @@ -6,8 +6,5 @@ // SPDX-License-Identifier: MIT // -import Foundation - -@_exported import ApodiniMigratorCore +@_exported import PathKit @_exported import ApodiniMigratorCompare -@_exported import ApodiniMigratorShared diff --git a/Sources/ApodiniMigrator/FileFormatter/Indentation.swift b/Sources/ApodiniMigrator/FileFormatter/Indentation.swift deleted file mode 100644 index c635155f..00000000 --- a/Sources/ApodiniMigrator/FileFormatter/Indentation.swift +++ /dev/null @@ -1,43 +0,0 @@ -// -// This source file is part of the Apodini open source project -// -// SPDX-FileCopyrightText: 2019-2021 Paul Schmiedmayer and the Apodini project authors (see CONTRIBUTORS.md) -// -// SPDX-License-Identifier: MIT -// - -import Foundation - -/// An object representing a spacer / indentation in a swift file -struct Indentation: CustomStringConvertible { - /// A space string with length of 4 - static let tab = String(repeating: " ", count: 4) - /// A placeholder to add indentation to lines that start with a `.`, since the current logic of `IndentationFormatter` can't handle those cases - static let placeholder = "____INDENTATION____" - /// A placeholder to indicate to `IndentationFormatter` to ignore one level of indentation for the lines that start with `Indentation.skip` - static let skip = "____SKIP____" - - /// The level of the indentation - private var level: UInt - - /// Complete space of this indentation, repeating `Indentation.tab` `level`-times - var description: String { - String(repeating: Self.tab, count: Int(level)) - } - - // MARK: - Initializer - init(_ level: UInt) { - self.level = level - } - - - /// Decreases level by one - mutating func dropLevel() { - level = level > 0 ? level - 1 : 0 - } - - /// Adds indentation to `rhs` - static func + (lhs: Self, rhs: String) -> String { - lhs.description + rhs - } -} diff --git a/Sources/ApodiniMigrator/FileFormatter/IndentationFormatter.swift b/Sources/ApodiniMigrator/FileFormatter/IndentationFormatter.swift deleted file mode 100644 index c79058c3..00000000 --- a/Sources/ApodiniMigrator/FileFormatter/IndentationFormatter.swift +++ /dev/null @@ -1,95 +0,0 @@ -// -// This source file is part of the Apodini open source project -// -// SPDX-FileCopyrightText: 2019-2021 Paul Schmiedmayer and the Apodini project authors (see CONTRIBUTORS.md) -// -// SPDX-License-Identifier: MIT -// - -import Foundation - -/// An enumeration representing opening and closing brackets (curly or rounding) of `Swift` code blocks -private enum Bracket: Int { - case opening = 1 - case closing = -1 - - /// Initializer of a bracket type out of a character - init?(_ character: Character) { - if ["{", "(", "["].contains(character) { - self = .opening - } else if ["}", ")", "]"].contains(character) { - self = .closing - } else { - return nil - } - } - - /// The weight the bracket contributes to the `storage` of `IndentationFormatter` - var weight: Int { - rawValue - } -} - -/// An indentation swift file formatter. -/// The result of `format(_:)` is the one obtained by `(Command+A, Control+I)` `Xcode` command combinations. -/// Additionally the formatter replaces multiple empty lines with a single one. -public struct IndentationFormatter: SwiftFileFormatter { - /// Difference between counts of visited opening and closing brackets. - /// For compilable swift files, storage is always greater than zero while formatting, and zero at the end - private var storage = 0 - - /// The indentation to be applied in a line based on the state of the storage - private var currentIndentation: Indentation { - guard storage >= 0 else { - fatalError("The swift file is malformed") - } - return .init(UInt(storage)) - } - - public init() {} - - /// Updates the storage with the difference between counts of opening and closing brackets in `line` - /// - Parameter line: the line to be processed - /// - Returns: If `line` starts with one closing bracket, returns a `.closing`, otherwise `nil` - private mutating func updateStorage(with line: String) -> Bracket? { - // ignoring comments (not considering /***/ comments though) - if !line.hasPrefix("//") { - let lineBrackets = line.compactMap { Bracket($0) } - storage += lineBrackets.reduce(0) { $0 + $1.weight } - // if encountered a line with a starting closing bracket, return it. - // needed to decrease the indentation level for `line` - if lineBrackets.first == .closing { - return .closing - } - } - return nil - } - - /// Formats content with `(Command+A, Control+I)` `Xcode` command combinations - /// - Parameters content: string content of the swift file - /// - Returns the formatted content - public mutating func format(_ content: String) -> String { - let formatted = content.sanitizedLines().reduce(into: "") { result, line in - var indentation = currentIndentation - var currentLine = line - if updateStorage(with: currentLine) == .closing || line.starts(with: Indentation.skip) { - indentation.dropLevel() - currentLine = currentLine.without(Indentation.skip) - } - result += indentation + currentLine + .lineBreak - } - assert(storage == 0, "Encountered a malformed swift file. Non-balanced number of opening a closing brackets: \(abs(storage))") - return formatted.with(Indentation.tab, insteadOf: Indentation.placeholder) - } - - /// Formats content at the specified path with `(Command+A, Control+I)` `Xcode` command combinations, and persists the changes - /// - Parameters path: Path where the swift file is located - /// - Throws if the read operation failed - /// - Note results in fatalError if path does not exists, or if not a swift file - public mutating func format(_ path: Path) throws { - guard path.exists, path.is(.swift) else { - fatalError("Invalid swift file path: \(path.string)") - } - try path.write(format(try path.read())) - } -} diff --git a/Sources/ApodiniMigrator/FileFormatter/SwiftFileFormatter.swift b/Sources/ApodiniMigrator/FileFormatter/SwiftFileFormatter.swift deleted file mode 100644 index 8b033fa7..00000000 --- a/Sources/ApodiniMigrator/FileFormatter/SwiftFileFormatter.swift +++ /dev/null @@ -1,38 +0,0 @@ -// -// This source file is part of the Apodini open source project -// -// SPDX-FileCopyrightText: 2019-2021 Paul Schmiedmayer and the Apodini project authors (see CONTRIBUTORS.md) -// -// SPDX-License-Identifier: MIT -// - -import Foundation - -/// A protocol to format swift files -public protocol SwiftFileFormatter { - /// Initializer - init() - - /// Formats content - /// - Parameters content: string content of the swift file - /// - Returns the formatted content - mutating func format(_ content: String) -> String - - /// Formats content at the specified path - /// - Parameters path: Path where the swift file is located - /// - Throws if invalid path, or if the read operation failed - mutating func format(_ path: Path) throws -} - -public extension String { - /// Returns a formatted version of `self` by a formatterType - func formatted(with formatterType: S.Type) -> String { - var formatter = formatterType.init() - return formatter.format(self) - } - - /// Returns an indentation formatted version of `self` - func indentationFormatted() -> String { - formatted(with: IndentationFormatter.self) - } -} diff --git a/Sources/ApodiniMigrator/LibraryStructure/Components/Directory.swift b/Sources/ApodiniMigrator/LibraryStructure/Components/Directory.swift new file mode 100644 index 00000000..c1349110 --- /dev/null +++ b/Sources/ApodiniMigrator/LibraryStructure/Components/Directory.swift @@ -0,0 +1,44 @@ +// +// This source file is part of the Apodini open source project +// +// SPDX-FileCopyrightText: 2019-2021 Paul Schmiedmayer and the Apodini project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + +import Foundation +import PathKit + +/// A ``LibraryComposite`` used to build a `Directory`. +/// The `Directory` might container other ``LibraryComponent``s. +public class Directory: LibraryComposite { + /// The directory name + public let path: Name + + /// The directory content. + public let content: [LibraryComponent] + + /// Create a new `Directory`. + /// - Parameters: + /// - name: The directory name. + /// - content: The directory content using a ``DefaultLibraryComponentBuilder`` closure. + public init(_ name: Name, @DefaultLibraryComponentBuilder content: () -> [LibraryComponent] = { [] }) { + precondition(!name.isEmpty) + self.path = name + self.content = content() + } + + // swiftlint:disable:next identifier_name + internal init(_ name: Name, _content: [LibraryComponent]) { + precondition(!name.isEmpty) + self.path = name + self.content = _content + } + + public func handle(at path: Path, with context: MigrationContext) throws { + let directoryPath = path + self.path.description(with: context) + + context.logger.debug("Creating directory \(self.path.description(with: context)) at: \(path.absolute())") + try directoryPath.mkpath() + } +} diff --git a/Sources/ApodiniMigrator/LibraryStructure/Components/Empty.swift b/Sources/ApodiniMigrator/LibraryStructure/Components/Empty.swift new file mode 100644 index 00000000..2d85938f --- /dev/null +++ b/Sources/ApodiniMigrator/LibraryStructure/Components/Empty.swift @@ -0,0 +1,16 @@ +// +// This source file is part of the Apodini open source project +// +// SPDX-FileCopyrightText: 2019-2021 Paul Schmiedmayer and the Apodini project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + +import Foundation + +/// An `Empty` ``LibraryNode``. Does nothing. +public struct Empty: LibraryNode { + public init() {} + + public func handle(at path: Path, with context: MigrationContext) {} +} diff --git a/Sources/ApodiniMigrator/LibraryStructure/Components/GeneratedFile.swift b/Sources/ApodiniMigrator/LibraryStructure/Components/GeneratedFile.swift new file mode 100644 index 00000000..d38a4c7b --- /dev/null +++ b/Sources/ApodiniMigrator/LibraryStructure/Components/GeneratedFile.swift @@ -0,0 +1,39 @@ +// +// This source file is part of the Apodini open source project +// +// SPDX-FileCopyrightText: 2019-2021 Paul Schmiedmayer and the Apodini project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + +import Foundation +import PathKit + +/// A special ``LibraryNode`` which describes a single generated file, simply by supplying +/// a file name an the source code file using ``SourceCodeRenderable``. +public protocol GeneratedFile: LibraryNode, SourceCodeRenderable { + var fileName: Name { get } +} + +extension GeneratedFile { + // this method is important for testing + func formattedFile(with context: MigrationContext) -> String { + var fileContent = self.renderableContent + + for (placeholder, content) in context.placeholderValues { + fileContent.replaceOccurrencesRespectingIndent(of: placeholder.description, with: content) + } + + return fileContent.appending("\n") + } +} + +public extension GeneratedFile { + /// Default implementation to write the generated file and handle ``Placeholder`` replacements. + func handle(at path: Path, with context: MigrationContext) throws { + precondition(!fileName.isEmpty) + context.logger.debug("Rendering file \(fileName.description(with: context)) at: \(path.absolute())") + let filePath = path + fileName.description(with: context) + try filePath.write(formattedFile(with: context), encoding: .utf8) + } +} diff --git a/Sources/ApodiniMigrator/LibraryStructure/Components/ResourceFile.swift b/Sources/ApodiniMigrator/LibraryStructure/Components/ResourceFile.swift new file mode 100644 index 00000000..015daef0 --- /dev/null +++ b/Sources/ApodiniMigrator/LibraryStructure/Components/ResourceFile.swift @@ -0,0 +1,94 @@ +// +// This source file is part of the Apodini open source project +// +// SPDX-FileCopyrightText: 2019-2021 Paul Schmiedmayer and the Apodini project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + +import Foundation +import PathKit + +/// A `ResourceFile` is a file which is copied from the resources (e.g. `Bundle.module`) of a target. +/// The file is searched in the bundle supplied in the ``Migrator``. +public class ResourceFile: LibraryNode { + let srcFileName: Name + let dstFilename: Name + + /// String which is to be prepended to the resulting file. Empty if not supplied. + let filePrefix: String + /// String which is to be appended to the resulting file. Empty if not supplied. + let fileSuffix: String + + var contentReplacer: [Placeholder: String] = [:] + + /// Initializes a new `ResourceFile`. + /// - Parameters: + /// - srcFileName: The file ``Name`` to search for in the ``Migrator/bundle``. + /// - dstFileName: If supplied the filename is used when writing the file to disk. + /// Otherwise the `srcFileName` is used. + /// - filePrefix: Optional ``SourceCodeBuilder`` closure to supply a prefix which is prepended to the file content. + /// - fileSuffix: Optional ``SourceCodeBuilder`` closure to supply a suffix which is appended to the file content. + public init( + copy srcFileName: Name, + to dstFileName: Name? = nil, + @SourceCodeBuilder filePrefix: () -> String = { "" }, + @SourceCodeBuilder fileSuffix: () -> String = { "" } + ) { + precondition(!srcFileName.isEmpty) + self.srcFileName = srcFileName + if let dstFileName = dstFileName { + self.dstFilename = dstFileName + } else { + self.dstFilename = srcFileName + } + self.filePrefix = filePrefix() + self.fileSuffix = fileSuffix() + } + + /// Adds a new content replacement for the given file. This can be used to dynamically supply ``Placeholder`` values. + /// - Parameters: + /// - placeholder: The ``Placeholder`` which should be replaced in the file content. + /// - content: The value for the ``Placeholder``. + /// - Returns: Returns `self` for chanining. + public func replacing(_ placeholder: Placeholder, with content: String) -> Self { + precondition(contentReplacer[placeholder] == nil) + contentReplacer[placeholder] = content + return self + } + + public func handle(at path: Path, with context: MigrationContext) throws { + let rawSrcFileName = srcFileName.description(with: context) + let rawDstFileName = dstFilename.description(with: context) + + guard let fileUrl = context.bundle.url(forResource: rawSrcFileName, withExtension: nil) else { + fatalError("Could not locate resource (\(rawSrcFileName)) in bundle (\(context.bundle)) for \(self)") + } + + guard var fileContent = try? String(contentsOf: fileUrl, encoding: .utf8) else { + fatalError("Failed to read file contents (\(rawSrcFileName)) in bundle (\(context.bundle) for \(self)") + } + + for (placeholder, content) in context.placeholderValues.merging(contentReplacer, uniquingKeysWith: { $1 }) { + fileContent.replaceOccurrencesRespectingIndent(of: placeholder.description, with: content) + } + + if !filePrefix.isEmpty { + fileContent = filePrefix + "\n" + fileContent + } + if !fileSuffix.isEmpty { + fileContent += "\n" + fileSuffix + } + + context.logger.debug("Copying resource file \(rawSrcFileName)\(rawSrcFileName != rawDstFileName ? "to \(rawDstFileName)": "") at: \(path.absolute())") + + let destinationPath = path + rawDstFileName + try destinationPath.write(fileContent, encoding: .utf8) + } +} + +extension ResourceFile: CustomStringConvertible { + public var description: String { + "ResourceFile(fileName: \(srcFileName))" + } +} diff --git a/Sources/ApodiniMigrator/LibraryStructure/Components/Root/ReadMeFile.swift b/Sources/ApodiniMigrator/LibraryStructure/Components/Root/ReadMeFile.swift new file mode 100644 index 00000000..e203e8df --- /dev/null +++ b/Sources/ApodiniMigrator/LibraryStructure/Components/Root/ReadMeFile.swift @@ -0,0 +1,20 @@ +// +// This source file is part of the Apodini open source project +// +// SPDX-FileCopyrightText: 2019-2021 Paul Schmiedmayer and the Apodini project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + +import Foundation + +/// The `Readme` file of a swift package. +/// +/// Note: This implementation expects the readme file to be a ``ResourceFile``. +public class ReadMeFile: ResourceFile { + /// Initialize a `ReadMeFile`. + /// - Parameter name: The name of the readme file (including file extension). + public init(_ name: String = "README.md") { + super.init(copy: Name(stringLiteral: name)) + } +} diff --git a/Sources/ApodiniMigrator/LibraryStructure/Components/Root/RootDirectory.swift b/Sources/ApodiniMigrator/LibraryStructure/Components/Root/RootDirectory.swift new file mode 100644 index 00000000..0651dd5b --- /dev/null +++ b/Sources/ApodiniMigrator/LibraryStructure/Components/Root/RootDirectory.swift @@ -0,0 +1,66 @@ +// +// This source file is part of the Apodini open source project +// +// SPDX-FileCopyrightText: 2019-2021 Paul Schmiedmayer and the Apodini project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + +import Foundation +import PathKit + +public class RootDirectory: LibraryComposite { + public var path: Name = .packageName + + public var content: [LibraryComponent] + + var sources: Sources + var tests: Tests? + + var packageSwift: SwiftPackageFile + + init(content: [LibraryComponent]) { + var foundSources: Sources? + var foundTests: Tests? + var foundPackage: SwiftPackageFile? + + for content in content { + if let sources = content as? Sources { + precondition(foundSources == nil, "Encountered sources twice!") + foundSources = sources + } else if let tests = content as? Tests { + precondition(foundTests == nil, "Encountered tests twice!") + foundTests = tests + } else if let package = content as? SwiftPackageFile { + precondition(foundPackage == nil, "Encountered Package.swift twice!") + foundPackage = package + } + } + + + self.content = content + + guard + let sources = foundSources, + let packageSwift = foundPackage else { + preconditionFailure("Every library needs sources and a Package.swift!") + } + + self.sources = sources + self.tests = foundTests + self.packageSwift = packageSwift + } + + public func handle(at path: Path, with context: MigrationContext) throws { + guard let packageName = context.placeholderValues[.packageName] else { + fatalError("PackageName not present") + } + + let rootPath = path + packageName + try? rootPath.delete() + try rootPath.mkpath() + + packageSwift.targets.append(contentsOf: sources.targets.map { $0.targetDescription(with: context) }) + packageSwift.targets.append(contentsOf: tests?.targets.map { $0.targetDescription(with: context) } ?? []) + } +} diff --git a/Sources/ApodiniMigrator/LibraryStructure/Components/Root/Sources.swift b/Sources/ApodiniMigrator/LibraryStructure/Components/Root/Sources.swift new file mode 100644 index 00000000..8e567ebc --- /dev/null +++ b/Sources/ApodiniMigrator/LibraryStructure/Components/Root/Sources.swift @@ -0,0 +1,20 @@ +// +// This source file is part of the Apodini open source project +// +// SPDX-FileCopyrightText: 2019-2021 Paul Schmiedmayer and the Apodini project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + +import Foundation + +/// The `Sources` folder in a swift package. +public class Sources: Directory, TargetContainingDirectory { + public var targets: [TargetDirectory] { + content.compactMap { $0 as? TargetDirectory } + } + + public init(@TargetLibraryComponentBuilder content: () -> [LibraryComponent] = { [] }) { + super.init("Sources", content: content) + } +} diff --git a/Sources/ApodiniMigrator/LibraryStructure/Components/Root/StubLinuxMainFile.swift b/Sources/ApodiniMigrator/LibraryStructure/Components/Root/StubLinuxMainFile.swift new file mode 100644 index 00000000..f1042b43 --- /dev/null +++ b/Sources/ApodiniMigrator/LibraryStructure/Components/Root/StubLinuxMainFile.swift @@ -0,0 +1,33 @@ +// +// This source file is part of the Apodini open source project +// +// SPDX-FileCopyrightText: 2019-2021 Paul Schmiedmayer and the Apodini project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + +import Foundation + +public struct StubLinuxMainFile: GeneratedFile { + public var fileName: Name = "LinuxMain.swift" + + let filePrefix: String + + public init(@SourceCodeBuilder prefix filePrefix: () -> String = { "" }) { + self.filePrefix = filePrefix() + } + + public var renderableContent: String { + if !filePrefix.isEmpty { + filePrefix + } + + """ + #error(\""" + ----------------------------------------------------- + Please test with `swift test --enable-test-discovery` + ----------------------------------------------------- + \""") + """ + } +} diff --git a/Sources/ApodiniMigrator/LibraryStructure/Components/Root/SwiftPackageFile/PackageDependency.swift b/Sources/ApodiniMigrator/LibraryStructure/Components/Root/SwiftPackageFile/PackageDependency.swift new file mode 100644 index 00000000..e83eda8c --- /dev/null +++ b/Sources/ApodiniMigrator/LibraryStructure/Components/Root/SwiftPackageFile/PackageDependency.swift @@ -0,0 +1,21 @@ +// +// This source file is part of the Apodini open source project +// +// SPDX-FileCopyrightText: 2019-2021 Paul Schmiedmayer and the Apodini project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + +import Foundation + +struct PackageDependency: SourceCodeRenderable { + var name: String? + var url: String + var requirementString: String + + var renderableContent: String { + """ + .package(\(name != nil ? "name: \"\(name ?? "")\", " : "")url: \"\(url)\", \(requirementString)) + """ + } +} diff --git a/Sources/ApodiniMigrator/LibraryStructure/Components/Root/SwiftPackageFile/PackageProduct.swift b/Sources/ApodiniMigrator/LibraryStructure/Components/Root/SwiftPackageFile/PackageProduct.swift new file mode 100644 index 00000000..931997c7 --- /dev/null +++ b/Sources/ApodiniMigrator/LibraryStructure/Components/Root/SwiftPackageFile/PackageProduct.swift @@ -0,0 +1,30 @@ +// +// This source file is part of the Apodini open source project +// +// SPDX-FileCopyrightText: 2019-2021 Paul Schmiedmayer and the Apodini project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + +import Foundation + +enum PackageProductType { + case library + case executable + case plugin +} + +struct PackageProduct: SourceCodeRenderable { + let type: PackageProductType + let name: Name + // Note: Library type for type=`.library` is unsupported right now! + let targets: [Name] + + var renderableContent: String { + """ + .\(type)(name: "\(name.description)", targets: [\( + targets.map { "\"\($0.description)\"" }.joined(separator: ",") + )]) + """ + } +} diff --git a/Sources/ApodiniMigrator/LibraryStructure/Components/Root/SwiftPackageFile/PackageTarget.swift b/Sources/ApodiniMigrator/LibraryStructure/Components/Root/SwiftPackageFile/PackageTarget.swift new file mode 100644 index 00000000..d9ced19a --- /dev/null +++ b/Sources/ApodiniMigrator/LibraryStructure/Components/Root/SwiftPackageFile/PackageTarget.swift @@ -0,0 +1,99 @@ +// +// This source file is part of the Apodini open source project +// +// SPDX-FileCopyrightText: 2019-2021 Paul Schmiedmayer and the Apodini project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + +import Foundation + +struct PackageTarget: SourceCodeRenderable { + let type: TargetType + let name: String + let dependencies: [TargetDependency] + let resources: [TargetResource] + + var renderableContent: String { + ".\(type.rawValue)(" + Indent { + Joined(by: ",") { + "name: \"\(name)\"" + + if !dependencies.isEmpty { + "dependencies: [" + Indent { + dependencies + .map { $0.renderableContent } + .joined(separator: ",") + } + "]" + } + + if !resources.isEmpty { + "resources: [" + Indent { + resources + .map { $0.renderableContent } + .joined(separator: ",") + } + "]" + } + } + } + ")" + } +} + +extension TargetDirectory { + func targetDescription(with context: MigrationContext) -> PackageTarget { + PackageTarget(type: type, name: path.description(with: context), dependencies: dependencies, resources: resources) + } +} + + +public enum TargetType: String { + case test = "testTarget" + case regular = "target" + case executable = "executableTarget" +} + +public protocol TargetDependency: SourceCodeRenderable {} + +struct LocalDependency: TargetDependency { + let target: Name + + var renderableContent: String { + """ + .target(name: "\(target.description)") + """ + } +} + +struct ProductDependency: TargetDependency { + let product: Name + let package: Name + + var renderableContent: String { + """ + .product(name: "\(product.description)", package: "\(package.description)") + """ + } +} + + +public enum ResourceType: String { + case process + case copy +} + +public struct TargetResource: SourceCodeRenderable { + let type: ResourceType + let path: Name + + public var renderableContent: String { + """ + .\(type.rawValue)("\(path.description)") + """ + } +} diff --git a/Sources/ApodiniMigrator/LibraryStructure/Components/Root/SwiftPackageFile/SwiftPackageFile.swift b/Sources/ApodiniMigrator/LibraryStructure/Components/Root/SwiftPackageFile/SwiftPackageFile.swift new file mode 100644 index 00000000..e2e84d92 --- /dev/null +++ b/Sources/ApodiniMigrator/LibraryStructure/Components/Root/SwiftPackageFile/SwiftPackageFile.swift @@ -0,0 +1,133 @@ +// +// This source file is part of the Apodini open source project +// +// SPDX-FileCopyrightText: 2019-2021 Paul Schmiedmayer and the Apodini project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + +import Foundation + +/// The `Package.swift` file of a swift package. +public class SwiftPackageFile: GeneratedFile { + public var fileName: Name = "Package.swift" + + var swiftToolsVersion: String + var platforms: [String] = [] + var products: [PackageProduct] = [] + var dependencies: [PackageDependency] = [] + + var targets: [PackageTarget] = [] + + /// Initialize a new `SwiftPackageFile`. + /// - Parameter swiftTools: The `swift-tools-version` version string used for the package file. + public init(swiftTools: String) { + self.swiftToolsVersion = swiftTools + } + + /// Configure the platforms for the swift package. + /// - Parameter platforms: The platform strings (e.g. `.macOS(.v12)`) + /// - Returns: Returns self for chaining. + public func platform(_ platforms: String...) -> Self { + self.platforms.append(contentsOf: platforms) + return self + } + + /// Configure a dependency for the swift package. + /// - Parameters: + /// - name: Optionally the name of the swift dependency. + /// - url: The git url to the dependency + /// - requirementString: The requirement string (e.g. `.branch("someBranch")`, `.upToNextMinor(from: "0.1.0")`) + /// - Returns: Returns self for chaining. + public func dependency(name: String? = nil, url: String, _ requirementString: String) -> Self { + dependencies.append(PackageDependency(name: name, url: url, requirementString: requirementString)) + return self + } + + + /// Configure a library product of the swift package. + /// - Parameters: + /// - name: The name of the product. + /// - targets: The targets which are part of the product. + /// - Returns: Returns self for chaining. + public func product(library name: Name, targets: Name...) -> Self { + products.append(PackageProduct(type: .library, name: name, targets: targets)) + return self + } + + /// Configure a executable product of the swift package. + /// - Parameters: + /// - name: The name of the product. + /// - targets: The targets which are part of the product. + /// - Returns: Returns self for chaining. + public func product(executable name: Name, targets: Name...) -> Self { + products.append(PackageProduct(type: .executable, name: name, targets: targets)) + return self + } + + /// Configure a plugin product of the swift package. + /// - Parameters: + /// - name: The name of the product. + /// - targets: The targets which are part of the product. + /// - Returns: Returns self for chaining. + public func product(plugin name: Name, targets: Name...) -> Self { + products.append(PackageProduct(type: .plugin, name: name, targets: targets)) + return self + } + + public var renderableContent: String { + """ + // swift-tools-version:\(swiftToolsVersion) + // The swift-tools-version declares the minimum version of Swift required to build this package. + + import PackageDescription + + let package = Package( + """ + Indent { + Joined(by: ",") { + "name: \"\(Placeholder.packageName)\"" + + if !platforms.isEmpty { + "platforms: [" + Indent { + platforms.joined(separator: ", ") + } + "]" + } + + if !products.isEmpty { + "products: [" + Indent { + Joined(by: ",") { + products + } + } + "]" + } + + if !dependencies.isEmpty { + "dependencies: [" + Indent { + Joined(by: ",") { + dependencies + } + } + "]" + } + + if !targets.isEmpty { + "targets: [" + Indent { + Joined(by: ",") { + targets + } + } + "]" + } + } + } + + ")" + } +} diff --git a/Sources/ApodiniMigrator/LibraryStructure/Components/Root/TargetContainingDirectory.swift b/Sources/ApodiniMigrator/LibraryStructure/Components/Root/TargetContainingDirectory.swift new file mode 100644 index 00000000..ee54f6ff --- /dev/null +++ b/Sources/ApodiniMigrator/LibraryStructure/Components/Root/TargetContainingDirectory.swift @@ -0,0 +1,19 @@ +// +// This source file is part of the Apodini open source project +// +// SPDX-FileCopyrightText: 2019-2021 Paul Schmiedmayer and the Apodini project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + +import Foundation + +/// A protocol describing a ``Directory`` which contains ``TargetDorectory``s. +/// +/// The following two directories exist: +/// * ``Sources`` +/// * ``Tests`` +public protocol TargetContainingDirectory { + /// The ``TargetDirectory``s contained in this directory. + var targets: [TargetDirectory] { get } +} diff --git a/Sources/ApodiniMigrator/LibraryStructure/Components/Root/Tests.swift b/Sources/ApodiniMigrator/LibraryStructure/Components/Root/Tests.swift new file mode 100644 index 00000000..81668732 --- /dev/null +++ b/Sources/ApodiniMigrator/LibraryStructure/Components/Root/Tests.swift @@ -0,0 +1,20 @@ +// +// This source file is part of the Apodini open source project +// +// SPDX-FileCopyrightText: 2019-2021 Paul Schmiedmayer and the Apodini project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + +import Foundation + +/// The `Tests` directory in a swift package +public class Tests: Directory, TargetContainingDirectory { + public var targets: [TargetDirectory] { + content.compactMap { $0 as? TargetDirectory } + } + + public init(@TargetLibraryComponentBuilder content: () -> [LibraryComponent] = { [] }) { + super.init("Tests", content: content) + } +} diff --git a/Sources/ApodiniMigrator/LibraryStructure/Components/StringFile.swift b/Sources/ApodiniMigrator/LibraryStructure/Components/StringFile.swift new file mode 100644 index 00000000..040dbac6 --- /dev/null +++ b/Sources/ApodiniMigrator/LibraryStructure/Components/StringFile.swift @@ -0,0 +1,35 @@ +// +// This source file is part of the Apodini open source project +// +// SPDX-FileCopyrightText: 2019-2021 Paul Schmiedmayer and the Apodini project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + +import Foundation + +/// A simple string based file. +public struct StringFile: GeneratedFile { + /// The file ``Name``. + public let fileName: Name + /// The file content encoded as a string. + public let renderableContent: String + + /// Initialize a new `StringFile` by supplying file name and the content by string. + /// - Parameters: + /// - fileName: The file ``Name``. + /// - content: The full file content encoded as a string. + public init(name fileName: Name, content: String) { + self.fileName = fileName + self.renderableContent = content + } + + /// Initialize a new `StringFile` by supplying file name and the content using the ``SourceCodeBuilder``. + /// - Parameters: + /// - fileName: The file ``Name``. + /// - content: The content provided as a ``SourceCodeBuilder`` closure. + public init(name fileName: Name, @SourceCodeBuilder content: () -> String) { + self.fileName = fileName + self.renderableContent = content() + } +} diff --git a/Sources/ApodiniMigrator/LibraryStructure/Components/Targets/ExecutableTarget.swift b/Sources/ApodiniMigrator/LibraryStructure/Components/Targets/ExecutableTarget.swift new file mode 100644 index 00000000..c2de06d4 --- /dev/null +++ b/Sources/ApodiniMigrator/LibraryStructure/Components/Targets/ExecutableTarget.swift @@ -0,0 +1,20 @@ +// +// This source file is part of the Apodini open source project +// +// SPDX-FileCopyrightText: 2019-2021 Paul Schmiedmayer and the Apodini project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + +import Foundation + +/// A swift executable target placed in ``Sources``. +public class Executable: Target { + override public var type: TargetType { + .executable + } + + override public init(_ name: Name, @DefaultLibraryComponentBuilder content: () -> [LibraryComponent] = { [] }) { + super.init(name, _content: content()) + } +} diff --git a/Sources/ApodiniMigrator/LibraryStructure/Components/Targets/Target.swift b/Sources/ApodiniMigrator/LibraryStructure/Components/Targets/Target.swift new file mode 100644 index 00000000..6dfc1583 --- /dev/null +++ b/Sources/ApodiniMigrator/LibraryStructure/Components/Targets/Target.swift @@ -0,0 +1,28 @@ +// +// This source file is part of the Apodini open source project +// +// SPDX-FileCopyrightText: 2019-2021 Paul Schmiedmayer and the Apodini project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + +import Foundation +import PathKit + +/// A regular swift ``Sources`` target. +public class Target: Directory, TargetDirectory { + public var type: TargetType { + .regular + } + + public var dependencies: [TargetDependency] = [] + public var resources: [TargetResource] = [] + + override public init(_ name: Name, @DefaultLibraryComponentBuilder content: () -> [LibraryComponent] = { [] }) { + super.init(name, _content: content()) + } + + override init(_ name: Name, _content: [LibraryComponent]) { // swiftlint:disable:this identifier_name + super.init(name, _content: _content) + } +} diff --git a/Sources/ApodiniMigrator/LibraryStructure/Components/Targets/TargetDirectory.swift b/Sources/ApodiniMigrator/LibraryStructure/Components/Targets/TargetDirectory.swift new file mode 100644 index 00000000..4b318aa6 --- /dev/null +++ b/Sources/ApodiniMigrator/LibraryStructure/Components/Targets/TargetDirectory.swift @@ -0,0 +1,80 @@ +// +// This source file is part of the Apodini open source project +// +// SPDX-FileCopyrightText: 2019-2021 Paul Schmiedmayer and the Apodini project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + +import Foundation + +/// Represents any target in a swift package. +/// +/// **The following target types are supported by default:** +/// * ``Target``: a standard swift sources target +/// * ``ExecutableTarget``: a swift executable target +/// * ``TestTarget``: a swift test target +public protocol TargetDirectory: LibraryComponent { + /// The directory name. + var path: Name { get } + /// The ``TargetType``. + var type: TargetType { get } + /// Dependencies of the target. + var dependencies: [TargetDependency] { get set } + /// Optional resource declarations of the target. + var resources: [TargetResource] { get set } + + /// Add a `.target` dependency to the target. + /// - Parameter target: The name of the `.target` dependency. + /// - Returns: Returns self for chaining. + func dependency(target: Name) -> Self + + /// Add a `.product` dependency to the target. + /// - Parameters: + /// - product: The `.product` which should be added as dependency. + /// - package: The package name in which the dependency resides in. + /// - Returns: Returns self for chaining. + func dependency(product: Name, of package: Name) -> Self + + /// Add a resource declaration to the target. + /// - Parameters: + /// - type: The ``ResourceType``. + /// - path: The name of the resource folder. + /// - Returns: Returns self for chaining. + func resource(type: ResourceType, path: Name) -> Self +} + +public extension TargetDirectory { + /// Add a `.target` dependency to the target. + /// - Parameter target: The name of the `.target` dependency. + /// - Returns: Returns self for chaining. + func dependency(target: Name) -> Self { + var copy = self + copy.dependencies.append(LocalDependency(target: target)) + return copy + } + + /// Add a `.product` dependency to the target. + /// - Parameters: + /// - product: The `.product` which should be added as dependency. + /// - package: The package name in which the dependency resides in. + /// - Returns: Returns self for chaining. + func dependency(product: Name, of package: Name) -> Self { + var copy = self + copy.dependencies.append(ProductDependency(product: product, package: package)) + return copy + } +} + +public extension TargetDirectory { + /// Add a resource declaration to the target. + /// - Parameters: + /// - type: The ``ResourceType``. + /// - path: The name of the resource folder. + /// - Returns: Returns self for chaining. + func resource(type: ResourceType, path: Name) -> Self { + var copy = self + copy.resources.append(TargetResource(type: type, path: path)) + return copy + } +} diff --git a/Sources/ApodiniMigrator/LibraryStructure/Components/Targets/TestTarget.swift b/Sources/ApodiniMigrator/LibraryStructure/Components/Targets/TestTarget.swift new file mode 100644 index 00000000..43174ea2 --- /dev/null +++ b/Sources/ApodiniMigrator/LibraryStructure/Components/Targets/TestTarget.swift @@ -0,0 +1,23 @@ +// +// This source file is part of the Apodini open source project +// +// SPDX-FileCopyrightText: 2019-2021 Paul Schmiedmayer and the Apodini project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + +import Foundation + +/// A swift test target placed in ``Tests``. +public class TestTarget: Directory, TargetDirectory { + public var type: TargetType { + .test + } + + public var dependencies: [TargetDependency] = [] + public var resources: [TargetResource] = [] + + override public init(_ name: Name, @DefaultLibraryComponentBuilder content: () -> [LibraryComponent] = { [] }) { + super.init(name, _content: content()) + } +} diff --git a/Sources/ApodiniMigrator/LibraryStructure/LibraryComponent.swift b/Sources/ApodiniMigrator/LibraryStructure/LibraryComponent.swift new file mode 100644 index 00000000..26c1161a --- /dev/null +++ b/Sources/ApodiniMigrator/LibraryStructure/LibraryComponent.swift @@ -0,0 +1,25 @@ +// +// This source file is part of the Apodini open source project +// +// SPDX-FileCopyrightText: 2019-2021 Paul Schmiedmayer and the Apodini project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + +import Foundation +import PathKit + +/// A `LibraryComponent` describes any sort of component of a library structure. +/// This is the base protocol implementing a composite pattern. +/// +/// - SeeAlso: ``LibraryNode`` and ``LibraryComposite``. +public protocol LibraryComponent { + /// The base `handle` method to handle a ``LibraryComponent``. + /// This method is implemented by default by e.g. ``LibraryNode`` or ``LibraryComposite``. + /// + /// - Parameters: + /// - path: The path where this component is placed under. + /// - context: The ``MigrationContext`` in which this component is called. + /// - Throws: Throws any potential errors by the implementing parties. + func _handle(at path: Path, with context: MigrationContext) throws // swiftlint:disable:this identifier_name +} diff --git a/Sources/ApodiniMigrator/LibraryStructure/LibraryComposite.swift b/Sources/ApodiniMigrator/LibraryStructure/LibraryComposite.swift new file mode 100644 index 00000000..7e4f21ae --- /dev/null +++ b/Sources/ApodiniMigrator/LibraryStructure/LibraryComposite.swift @@ -0,0 +1,60 @@ +// +// This source file is part of the Apodini open source project +// +// SPDX-FileCopyrightText: 2019-2021 Paul Schmiedmayer and the Apodini project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + +import Foundation +import PathKit + +/// The composition of ``LibraryComponent``s. +/// This realizes the composite pattern. +public protocol LibraryComposite: LibraryComponent { + /// An optional protocol requirement. By default this is empty. + /// If supplied the `LibraryComposite` will be treated as a directory and all of the + /// ``content`` components are placed under the supplied directory ``Name``. + var path: Name { get } + + /// The content of the ``LibraryComposite`` buildt using ``DefaultLibraryComponentBuilder``. + @DefaultLibraryComponentBuilder + var content: [LibraryComponent] { get } + + /// Optionally to implement, called when this ``LibraryComponent`` is handled. + /// - Parameters: + /// - path: The path this component is placed under (doesn't include ``LibraryComposite/path``). + /// - context: The ``MigrationContext`` with which this component is called + /// - Throws: May throw any sort of error occuring when hanlding this component. + func handle(at path: Path, with context: MigrationContext) throws +} + +public extension LibraryComposite { + /// Default implementation with an empty name. + var path: Name { + Name(empty: ()) + } + + /// Default implementation for handle, which does nothing. + func handle(at path: Path, with context: MigrationContext) throws { + context.logger.debug("Handling library composite \(Self.self) at: \(path.absolute())") + } +} + +public extension LibraryComposite { + /// Default implementation for the internal `_handle` call. + /// This will call `handle` for self and all of the content ``LibraryComponent``s. + /// It automatically handles non empty ``LibraryComposite/path`s. + func _handle(at path: Path, with context: MigrationContext) throws { + // swiftlint:disable:previous identifier_name + try handle(at: path, with: context) + + let nextPath = self.path.isEmpty + ? path + : path + self.path.description(with: context) + + for component in content { + try component._handle(at: nextPath, with: context) + } + } +} diff --git a/Sources/ApodiniMigrator/LibraryStructure/LibraryNode.swift b/Sources/ApodiniMigrator/LibraryStructure/LibraryNode.swift new file mode 100644 index 00000000..b9ae1ea1 --- /dev/null +++ b/Sources/ApodiniMigrator/LibraryStructure/LibraryNode.swift @@ -0,0 +1,29 @@ +// +// This source file is part of the Apodini open source project +// +// SPDX-FileCopyrightText: 2019-2021 Paul Schmiedmayer and the Apodini project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + +import Foundation +import PathKit + +/// Describes a single leaf node in the tree of ``LibraryComponent``. +/// This is the leaf of the composite pattern. +public protocol LibraryNode: LibraryComponent { + /// The `handle` method is called to handle the ``LibraryNode``. + /// + /// - Parameters: + /// - path: The path where this component is placed under. + /// - context: The ``MigrationContext`` in which this component is called. + /// - Throws: Throws any potential errors by the implementing parties. + func handle(at path: Path, with context: MigrationContext) throws +} + +public extension LibraryNode { + /// Default implementation forwarding call. + func _handle(at path: Path, with context: MigrationContext) throws { // swiftlint:disable:this identifier_name + try handle(at: path, with: context) + } +} diff --git a/Sources/ApodiniMigrator/LibraryStructure/ResultBuilder/DefaultLibraryComponentBuilder.swift b/Sources/ApodiniMigrator/LibraryStructure/ResultBuilder/DefaultLibraryComponentBuilder.swift new file mode 100644 index 00000000..b098e578 --- /dev/null +++ b/Sources/ApodiniMigrator/LibraryStructure/ResultBuilder/DefaultLibraryComponentBuilder.swift @@ -0,0 +1,49 @@ +// +// This source file is part of the Apodini open source project +// +// SPDX-FileCopyrightText: 2019-2021 Paul Schmiedmayer and the Apodini project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + +import Foundation + +/// The general ``LibraryComponent`` builder. +@resultBuilder +public enum DefaultLibraryComponentBuilder { + /// Build a ``LibraryNode`` expression. + public static func buildExpression(_ expression: LibraryNode) -> [LibraryComponent] { + [expression] + } + + /// Build a ``LibraryComposite`` expression. + public static func buildExpression(_ expression: LibraryComposite) -> [LibraryComponent] { + [expression] + } + + /// Build component block + public static func buildBlock(_ components: [LibraryComponent]...) -> [LibraryComponent] { + components.flatten() + } + + /// Build either first. + public static func buildEither(first component: [LibraryComponent]) -> [LibraryComponent] { + component + } + + /// Build either second. + public static func buildEither(second component: [LibraryComponent]) -> [LibraryComponent] { + component + } + + /// Build an optional expression. + public static func buildOptional(_ component: [LibraryComponent]?) -> [LibraryComponent] { + // swiftlint:disable:previous discouraged_optional_collection + component ?? [Empty()] + } + + /// Build an array. + public static func buildArray(_ components: [[LibraryComponent]]) -> [LibraryComponent] { + components.flatten() + } +} diff --git a/Sources/ApodiniMigrator/LibraryStructure/ResultBuilder/RootLibraryComponentBuilder.swift b/Sources/ApodiniMigrator/LibraryStructure/ResultBuilder/RootLibraryComponentBuilder.swift new file mode 100644 index 00000000..2589c0ad --- /dev/null +++ b/Sources/ApodiniMigrator/LibraryStructure/ResultBuilder/RootLibraryComponentBuilder.swift @@ -0,0 +1,67 @@ +// +// This source file is part of the Apodini open source project +// +// SPDX-FileCopyrightText: 2019-2021 Paul Schmiedmayer and the Apodini project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + +import Foundation + +/// The ``LibraryComponent`` builder to build a ``RootDirectory`` of a swift package. +@resultBuilder +public enum RootLibraryComponentBuilder { + /// Build an expression from a ``Sources`` directory. + /// - Parameter expression: The ``Sources`` expression. + /// - Returns: Returns the component. + public static func buildExpression(_ expression: Sources) -> [LibraryComponent] { + [expression] + } + + /// Build an expression from a ``Tests`` directory. + /// - Parameter expression: The ``Tests`` expression. + /// - Returns: Returns the component. + public static func buildExpression(_ expression: Tests) -> [LibraryComponent] { + [expression] + } + + /// Build an expression from a ``LibraryNode``. + /// - Parameter expression: The ``LibraryNode`` expression. + /// - Returns: Returns the component. + public static func buildExpression(_ expression: LibraryNode) -> [LibraryComponent] { + [expression] + } + + /// Build a block from ``LibraryComponent``s. + /// - Parameter components: The components + /// - Returns: Returns the component. + public static func buildBlock(_ components: [LibraryComponent]...) -> [LibraryComponent] { + components.flatten() + } + + /// Build either first. + public static func buildEither(first component: [LibraryComponent]) -> [LibraryComponent] { + component + } + + /// Build either second. + public static func buildEither(second component: [LibraryComponent]) -> [LibraryComponent] { + component + } + + /// Build an optional expression. + public static func buildOptional(_ component: [LibraryComponent]?) -> [LibraryComponent] { + // swiftlint:disable:previous discouraged_optional_collection + component ?? [Empty()] + } + + /// Build an array. + public static func buildArray(_ components: [[LibraryComponent]]) -> [LibraryComponent] { + components.flatten() + } + + /// Build the final ``RootDirectory`` result. + public static func buildFinalResult(_ component: [LibraryComponent]) -> RootDirectory { + RootDirectory(content: component) + } +} diff --git a/Sources/ApodiniMigrator/LibraryStructure/ResultBuilder/TargetLibraryComponentBuilder.swift b/Sources/ApodiniMigrator/LibraryStructure/ResultBuilder/TargetLibraryComponentBuilder.swift new file mode 100644 index 00000000..80f42e3b --- /dev/null +++ b/Sources/ApodiniMigrator/LibraryStructure/ResultBuilder/TargetLibraryComponentBuilder.swift @@ -0,0 +1,55 @@ +// +// This source file is part of the Apodini open source project +// +// SPDX-FileCopyrightText: 2019-2021 Paul Schmiedmayer and the Apodini project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + +import Foundation + +/// The ``LibraryComponent`` builder to build a ``TargetDirectory`` of a swift package. +@resultBuilder +public enum TargetLibraryComponentBuilder { + /// Build an expression from a `Target` directory. + /// - Parameter expression: The `Target` directory expression. + /// - Returns: Returns the component + public static func buildExpression(_ expression: Target) -> [LibraryComponent] { + [expression] + } + + /// Build a block from ``LibraryComponent``s. + /// - Parameter components: The components + /// - Returns: Returns the component. + public static func buildBlock(_ components: [LibraryComponent]...) -> [LibraryComponent] { + components.flatten() + } + + /// Build either first. + public static func buildEither(first component: [LibraryComponent]) -> [LibraryComponent] { + component + } + + /// Build either second. + public static func buildEither(second component: [LibraryComponent]) -> [LibraryComponent] { + component + } + + /// Build an optional expression. + public static func buildOptional(_ component: [LibraryComponent]?) -> [LibraryComponent] { + // swiftlint:disable:previous discouraged_optional_collection + component ?? [] + } + + /// Build an array. + public static func buildArray(_ components: [[LibraryComponent]]) -> [LibraryComponent] { + components.flatten() + } +} + +public extension TargetLibraryComponentBuilder where Target == TestTarget { + /// Build a expression from a ``StubLinuxMainFile``. + static func buildExpression(_ expression: StubLinuxMainFile) -> [LibraryComponent] { + [expression] + } +} diff --git a/Sources/ApodiniMigrator/LibraryStructure/SharedNodeStorage.swift b/Sources/ApodiniMigrator/LibraryStructure/SharedNodeStorage.swift new file mode 100644 index 00000000..7d1055d2 --- /dev/null +++ b/Sources/ApodiniMigrator/LibraryStructure/SharedNodeStorage.swift @@ -0,0 +1,81 @@ +// +// This source file is part of the Apodini open source project +// +// SPDX-FileCopyrightText: 2019-2021 Paul Schmiedmayer and the Apodini project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + +import Foundation + +/// Box type to handle any data type as a reference. +class Box { + var element: Element + + init(_ element: Element) { + self.element = element + } +} + +/// A `SharedNodeStorage` can be used to store arbitrary data structures across multiple ``LibraryNode``s. +/// +/// For example file generator `A` might produce output which is used as input for the file generator `B`. +/// To do this you would declare a ``SharedNodeStorage`` in the ``Migrator`` and declare +/// ``SharedNodeReference``s in the file generators. Those references are set by passing the +/// ``SharedNodeStorage/projectedValue`` to both file generators. +@propertyWrapper +public struct SharedNodeStorage { + private var storageBox: Box = .init(nil) + + /// Initializes a new `SharedNodeStorage` which a optional initial value. + /// - Parameter value: The initial ``Element`` value which is optionally supplied. + public init(_ value: Element? = nil) { + self.storageBox.element = nil + } + + /// Initializes a new `SharedNodeStorage` with the `wrappedValue`. + /// - Parameter wrappedValue: The wrappedValue, supplied by the property syntax. + public init(_ wrappedValue: Element) { + self.storageBox.element = wrappedValue + } + + /// The `wrappedValue`. + /// - Note: Access to the wrappedValue with result in a `fatalError` if it wasn't previously set. + public var wrappedValue: Element { + guard let element = storageBox.element else { + fatalError("Value is not present!") + } + return element + } + + /// Create a ``SharedNodeReference`` from this storage object. + public var projectedValue: SharedNodeReference { + SharedNodeReference(storageBox: storageBox) + } +} + +/// A reference to a ``SharedNodeStorage``. +@propertyWrapper +public struct SharedNodeReference { + fileprivate var storageBox: Box + + /// Access to getters and setters of the referenced element. + public var wrappedValue: Element { + get { + guard let element = storageBox.element else { + fatalError("Value is not present!") + } + return element + } + set { + storageBox.element = newValue + } + } +} + +extension SharedNodeReference { + /// This initializer is mainly used for testing purpose, to directly initialize a reference with a value. + init(with value: Element) { + self.storageBox = Box(value) + } +} diff --git a/Sources/ApodiniMigrator/Migrator.swift b/Sources/ApodiniMigrator/Migrator.swift new file mode 100644 index 00000000..b618ded0 --- /dev/null +++ b/Sources/ApodiniMigrator/Migrator.swift @@ -0,0 +1,67 @@ +// +// This source file is part of the Apodini open source project +// +// SPDX-FileCopyrightText: 2019-2021 Paul Schmiedmayer and the Apodini project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + +import Foundation +import Logging +import PathKit + +/// The ``MigrationContext`` holds any state for a ongoing migration. +public struct MigrationContext { + /// This property holds the `Bundle.module` of the ``Migrator``. + public var bundle: Bundle + /// A logger instance for the given ``Migrator``. + public var logger: Logger + + /// A dictionary of global placeholder values (e.g. ``Placeholder/packageName``). + var placeholderValues: [Placeholder: String] = [:] +} + + +/// The ``Migrator`` protocol, any migrator must conform to. +public protocol Migrator { + /// The `Bundle.module` to give access to the target specific resources. + var bundle: Bundle { get } + + /// The swift `Logger` instance used within the whole migration process. + static var logger: Logger { get } + + /// This result builder based property builds the whole directory structure + /// of the client library and bootstraps all the migration processes. + /// The path structure of every client library consists of a ``RootDirectory`` + /// at the root. The ``RootDirectory`` contains ``Sources``, ``Tests`` + /// and the ``SwiftPackageFile`` which all can be declared using the Library-DSL + /// through the ``RootLibraryComponentBuilder``. + @RootLibraryComponentBuilder + var library: RootDirectory { get } + + /// This method is the entry point to the migration process. + /// There is an implementation by default for every Migrator which uses + /// the ``library`` to start up the migration processes. + /// + /// - Parameters: + /// - packageName: The Swift package name for the resulting library + /// (this string is placed under the ``Placeholder/packageName`` placeholder). + /// - packagePath: The path URL as a string to were the client library should be written. + /// - Throws: Rethrows errors thrown by any ``LibraryComponent``s. + func run(packageName: String, packagePath: String) throws +} + +public extension Migrator { + /// Default run implementation. + func run(packageName: String, packagePath: String) throws { + let name = packageName + .trimmingCharacters(in: .whitespaces) + .replacingOccurrences(of: "/", with: "_") + + let path = Path(packagePath) + var context = MigrationContext(bundle: bundle, logger: Self.logger) + context.placeholderValues[.packageName] = name + + try library._handle(at: path, with: context) + } +} diff --git a/Sources/ApodiniMigrator/Migrator/Endpoint/APIFile.swift b/Sources/ApodiniMigrator/Migrator/Endpoint/APIFile.swift deleted file mode 100644 index 3b4a25c0..00000000 --- a/Sources/ApodiniMigrator/Migrator/Endpoint/APIFile.swift +++ /dev/null @@ -1,56 +0,0 @@ -// -// This source file is part of the Apodini open source project -// -// SPDX-FileCopyrightText: 2019-2021 Paul Schmiedmayer and the Apodini project authors (see CONTRIBUTORS.md) -// -// SPDX-License-Identifier: MIT -// - -import Foundation - -/// Represents the `API.swift` file of the client library -struct APIFile: SwiftFile { - /// TypeInformation is a caseless enum named `API` - let typeInformation: TypeInformation = .enum(name: .init(name: "API"), rawValueType: nil, cases: []) - /// Kind of the file - let kind: Kind = .enum - /// All migrated endpoints of the library - let endpoints: [MigratedEndpoint] - - /// Initializes a new instance out all the migrated endpoints of the library - init(_ endpoints: [MigratedEndpoint]) { - self.endpoints = endpoints.sorted() - } - - /// Renders the wrapper method for the `migratedEndpoint` - private func method(for migratedEndpoint: MigratedEndpoint) -> String { - let endpoint = migratedEndpoint.endpoint - let nestedType = endpoint.response.nestedTypeString - var bodyInput = migratedEndpoint.parameters.map { "\($0.oldName): \($0.oldName)" } - bodyInput.append(contentsOf: DefaultEndpointInput.allCases.map { $0.keyValue }) - let body = - """ - \(migratedEndpoint.signature()) - \(nestedType).\(endpoint.deltaIdentifier)(\(String.lineBreak)\(bodyInput.joined(separator: ",\(String.lineBreak)"))\(String.lineBreak)) - } - """ - return body - } - - /// Renders the content of the file - func render() -> String { - """ - \(fileComment) - - \(Import(.foundation).render()) - - \(MARKComment(typeNameString)) - \(kind.signature) \(typeNameString) {} - - \(MARKComment(.endpoints)) - \(Kind.extension.signature) \(typeNameString) { - \(endpoints.map { method(for: $0) }.joined(separator: .doubleLineBreak)) - } - """ - } -} diff --git a/Sources/ApodiniMigrator/Migrator/Endpoint/EndpointFile.swift b/Sources/ApodiniMigrator/Migrator/Endpoint/EndpointFile.swift deleted file mode 100644 index 45e81631..00000000 --- a/Sources/ApodiniMigrator/Migrator/Endpoint/EndpointFile.swift +++ /dev/null @@ -1,67 +0,0 @@ -// -// This source file is part of the Apodini open source project -// -// SPDX-FileCopyrightText: 2019-2021 Paul Schmiedmayer and the Apodini project authors (see CONTRIBUTORS.md) -// -// SPDX-License-Identifier: MIT -// - -import Foundation - -/// An object that represents an `Type+Endpoint.swift` file in the client library -class EndpointFile: SwiftFile { - /// Nested response type of endpoints that are grouped in the file, e.g `User` and `[User]` -> `User` - let typeInformation: TypeInformation - /// Kind of the file, always extension - let kind: Kind = .extension - /// Endpoints that are rendered in the file (same nested response type) - private let endpoints: [Endpoint] - /// All changes of the migration guide that belong to the `endpoints` - private let changes: [Change] - /// Array of endpoints that have been migrated from `EndpointMethodMigrator`, property gets appended with new migrated endpoints inside of `methodBody(for:)` - private(set) var migratedEndpoints: [MigratedEndpoint] = [] - /// Imports of the file - private var imports = Import(.foundation) - - /// File comment that will be rendered for `self` - var endpointFileComment: FileHeaderComment { - .init(fileName: typeInformation.typeName.name + EndpointsMigrator.fileSuffix) - } - - /// Initializes a new instance out of the same nested response type of `endpoints` and the `changes` that belong to those endpoints - init(typeInformation: TypeInformation, endpoints: [Endpoint], changes: [Change]) { - self.typeInformation = typeInformation - self.endpoints = endpoints.sorted { lhs, rhs in - if lhs.response.typeString == rhs.response.typeString { - return lhs.deltaIdentifier < rhs.deltaIdentifier - } - return lhs.response.typeString < rhs.response.typeString - } - self.changes = changes - - if changes.contains(where: { $0.type == .deletion && $0.element.target == EndpointTarget.`self`.rawValue }) { - imports.insert(.combine) - } - } - - /// Renders the migrated method for `endpoint` - private func methodBody(for endpoint: Endpoint) -> String { - let endpointMigrator = EndpointMethodMigrator(endpoint, changes: changes.of(endpoint)) - migratedEndpoints.append(endpointMigrator.migratedEndpoint) - return endpointMigrator.render() - } - - /// Renders the content of the file with all migrated endpoints - func render() -> String { - """ - \(endpointFileComment.render()) - - \(imports.render()) - - \(MARKComment(.endpoints)) - \(kind.signature) \(typeInformation.typeName.name) { - \(endpoints.map { methodBody(for: $0) }.joined(separator: .doubleLineBreak)) - } - """ - } -} diff --git a/Sources/ApodiniMigrator/Migrator/Endpoint/EndpointMethodMigrator.swift b/Sources/ApodiniMigrator/Migrator/Endpoint/EndpointMethodMigrator.swift deleted file mode 100644 index 6ec54a1b..00000000 --- a/Sources/ApodiniMigrator/Migrator/Endpoint/EndpointMethodMigrator.swift +++ /dev/null @@ -1,215 +0,0 @@ -// -// This source file is part of the Apodini open source project -// -// SPDX-FileCopyrightText: 2019-2021 Paul Schmiedmayer and the Apodini project authors (see CONTRIBUTORS.md) -// -// SPDX-License-Identifier: MIT -// - -import Foundation - -class EndpointMethodMigrator: Renderable { - /// Endpoint of old version that will be migrated - private let endpoint: Endpoint - /// A flag that indicates whether the endpoint has been deleted in the new version - private let unavailable: Bool - /// Only changes that belong to `endpoint` - private let endpointChanges: [Change] - /// Migrated parameters of the endpoint by means of `endpointChanges`, property gets initialized by from `Self.migratedParameters(of:with:)` static method - private let parameters: [MigratedParameter] - /// Lazy migrated endpoint property. - lazy var migratedEndpoint: MigratedEndpoint = { - .init(endpoint: endpoint, unavailable: unavailable, parameters: parameters, path: path()) - }() - - /// An optional property that holds the id of the javascript convert function in case that response changed to some other type. Property set in `responseString()` - private var responseConvertID: Int? - - /// Initializes a new instance out of an endpoint of old version and the changes that belong to `endpoint` - init(_ endpoint: Endpoint, changes: [Change]) { - self.endpoint = endpoint - self.endpointChanges = changes - - unavailable = endpointChanges.contains(where: { $0.type == .deletion && $0.element.target == EndpointTarget.`self`.rawValue }) - parameters = Self.migratedParameters(of: endpoint, with: endpointChanges) - } - - /// Returns the string raw value of `target` - private func target(_ target: EndpointTarget) -> String { - target.rawValue - } - - /// Checks changes whether the response has changed to some other types. If that is the case returns the string of the new response type - /// and saves the corresponding convert id in `responseConvertID`, otherwise returns the string of the old response type - private func responseString() -> String { - if let responseChange = endpointChanges.firstMatch(on: \.type, with: .responseChange) as? UpdateChange, case let .element(anyCodable) = responseChange.to { - guard let convertID = responseChange.convertToFrom else { - fatalError("Response change did not provide an id for converting the response") - } - responseConvertID = convertID - let response = anyCodable.typed(TypeInformation.self) - return response.typeString - } - return endpoint.response.typeString - } - - /// Checks changes whether the operation has changed, if that is the case, returns the `operation` of the new version, otherwise the `operation` of `endpoint` - private func operation() -> ApodiniMigratorCore.Operation { - if - let operationChange = endpointChanges.first(where: { $0.element.target == target(.operation) }) as? UpdateChange, - case let .element(anyCodable) = operationChange.to { - return anyCodable.typed(ApodiniMigratorCore.Operation.self) - } - return endpoint.operation - } - - /// Checks whether the `path` has changed, if that is the case the new path is returned, otherwise the `path` of `endpoint` - private func path() -> EndpointPath { - if - let pathChange = endpointChanges.first(where: { $0.element.target == target(.resourcePath) }) as? UpdateChange, - case let .element(anyCodable) = pathChange.to { - return anyCodable.typed(EndpointPath.self) - } - return endpoint.path - } - - /// If response has changed, the migrator converts the return of the function to the old type by means of the `responseConvertID` saved from `responseString()`. - private func returnValueString() -> String { - var retValue = "return NetworkingService.trigger(handler)" - guard let convertID = responseConvertID else { - return retValue - } - let indentationPlaceholder = Indentation.placeholder - retValue += .lineBreak + indentationPlaceholder + ".tryMap { try \(endpoint.response.typeString).from($0, script: \(convertID)) }" + .lineBreak - retValue += indentationPlaceholder + ".eraseToAnyPublisher()" - return retValue - } - - /// Renders the body of the migrated endpoint - func render() -> String { - if unavailable { - return migratedEndpoint.unavailableBody() - } - - let responseString = self.responseString() - let queryParametersString = migratedEndpoint.queryParametersString() - let body = - """ - \(migratedEndpoint.signature()) - \(queryParametersString)var headers = httpHeaders - headers.setContentType(to: "application/json") - - var errors: [ApodiniError] = [] - \(endpoint.errors.map { "errors.addError(\($0.code), message: \($0.message.doubleQuoted))" }.lineBreaked) - - let handler = Handler<\(responseString)>( - path: \(migratedEndpoint.resourcePath().doubleQuoted), - httpMethod: .\(operation().asHTTPMethodString), - parameters: \(queryParametersString.isEmpty ? "[:]" : "parameters"), - headers: headers, - content: \(migratedEndpoint.contentParameterString()), - authorization: authorization, - errors: errors - ) - - \(returnValueString()) - } - """ - return body - } -} - -// MARK: - EndpointMethodMigrator -fileprivate extension EndpointMethodMigrator { - /// Returns the migrated endpoints of `endpoint` by means of `endpointChanges`, by taking into account all changes that target paramaters - // swiftlint:disable:next function_body_length - static func migratedParameters(of endpoint: Endpoint, with endpointChanges: [Change]) -> [MigratedParameter] { - var parameters: [MigratedParameter] = [] - let parameterTargets = [EndpointTarget.queryParameter, .pathParameter, .contentParameter].map { $0.rawValue } - - // First registering additions and deletions - for change in endpointChanges where parameterTargets.contains(change.element.target) && [.addition, .deletion].contains(change.type) { - if - let addChange = change as? AddChange, - case let .element(anyCodable) = addChange.added - { - parameters.append(.addedParameter(anyCodable.typed(Parameter.self), defaultValue: addChange.defaultValue)) - } else if - let deleteChange = change as? DeleteChange, - case let .elementID(id) = deleteChange.deleted, - let oldParameter = endpoint.parameters.firstMatch(on: \.deltaIdentifier, with: id) - { - parameters.append(.deletedParameter(oldParameter)) - } - } - - // Iterating through old parameters that have not been deleted and registering the corresponding changes - for oldParameter in endpoint.parameters where !parameters.contains(where: { $0.oldName == oldParameter.name }) { - let oldName = oldParameter.name - let oldType = oldParameter.typeInformation - var newType: TypeInformation? - var parameterType: ParameterType? - var newName: String? - var necessityValueJSONId: Int? - var convertFromToJSONId: Int? - // All changes related to parameters are of type `UpdateChange` - for change in endpointChanges where (change as? UpdateChange)?.targetID == oldParameter.deltaIdentifier { - if let updateChange = change as? UpdateChange { - if updateChange.type == .rename, case let .stringValue(rename) = updateChange.to { - newName = rename - continue - } - - if updateChange.parameterTarget == .kind, case let .element(anyCodable) = updateChange.to { - parameterType = anyCodable.typed(ParameterType.self) - continue - } - - if let necessityValue = updateChange.necessityValue, case let .json(id) = necessityValue { - necessityValueJSONId = id - assert(convertFromToJSONId == nil, "Provided necessity value for a parameter that already has a convert method") - continue - } - - if - updateChange.parameterTarget == .typeInformation, - let convertFromTo = updateChange.convertFromTo, - case let .element(anyCodable) = updateChange.to - { - convertFromToJSONId = convertFromTo - newType = anyCodable.typed(TypeInformation.self) - assert(necessityValueJSONId == nil, "Provided a convert method for a parameter that already has a necessity value") - } - } - } - - parameters.append( - .init( - oldName: oldName, - newName: newName ?? oldName, - kind: parameterType ?? oldParameter.parameterType, - necessity: oldParameter.necessity, - oldType: oldType, - newType: newType ?? oldType, - convertFromTo: convertFromToJSONId, - defaultValue: nil, - necessityValueJSONId: necessityValueJSONId, - deleted: false - ) - ) - } - return parameters - } -} - -// MARK: - Operation -fileprivate extension ApodiniMigratorCore.Operation { - var asHTTPMethodString: String { - switch self { - case .create: return "post" - case .read: return "get" - case .update: return "put" - case .delete: return "delete" - } - } -} diff --git a/Sources/ApodiniMigrator/Migrator/Endpoint/EndpointsMigrator.swift b/Sources/ApodiniMigrator/Migrator/Endpoint/EndpointsMigrator.swift deleted file mode 100644 index 0b667cda..00000000 --- a/Sources/ApodiniMigrator/Migrator/Endpoint/EndpointsMigrator.swift +++ /dev/null @@ -1,49 +0,0 @@ -// -// This source file is part of the Apodini open source project -// -// SPDX-FileCopyrightText: 2019-2021 Paul Schmiedmayer and the Apodini project authors (see CONTRIBUTORS.md) -// -// SPDX-License-Identifier: MIT -// - -import Foundation - -/// An object that handles / triggeres the migrated rendering of all endpoints of the client library -struct EndpointsMigrator { - /// Suffix of endpoint files, e.g. `User+Endpoint.swift` - static let fileSuffix = "+Endpoint" + .swift - /// Path to the `Endpoints` directory of the client library - let endpointsPath: Path - /// Path to target directory of the client library where the `API.swift` file gets rendered - let apiFilePath: Path - /// All old and added endpoints - let allEndpoints: [Endpoint] - /// All changes that belong to endpoints - let endpointChanges: [Change] - - /// Triggers the migrated rendering of all endpoints of the client library and rendering of the `API.swift` file - func migrate() throws { - // Grouping the endpoints based on their nested response type - let endpointGroups = allEndpoints.reduce(into: [String: Set]()) { result, current in - let nestedResponseType = current.response.nestedTypeString - result[nestedResponseType, default: []].insert(current) - } - - var migratedEndpoints: [MigratedEndpoint] = [] - - // Iterating through all endpoint groups, and rendering one migrated Endpoint file per group - for group in endpointGroups { - let endpoints = Array(group.value) - let endpointIds = endpoints.identifiers() - let groupChanges = endpointChanges.filter { endpointIds.contains($0.elementID) } - let fileName = group.key + Self.fileSuffix - let endpointFile = EndpointFile(typeInformation: .reference(group.key), endpoints: endpoints, changes: groupChanges) - // triggeres migration of endpoints, rendering of file, and stores the migratedEndpoints in `endpointFile.migratedEndpoints` - try endpointFile.write(at: endpointsPath, alternativeFileName: fileName) - migratedEndpoints.append(contentsOf: endpointFile.migratedEndpoints) - } - // Renders the api file from the migratedEndpoints collected in the for loop - let apiFile = APIFile(migratedEndpoints) - try apiFile.write(at: apiFilePath) - } -} diff --git a/Sources/ApodiniMigrator/Migrator/Migrator.swift b/Sources/ApodiniMigrator/Migrator/Migrator.swift deleted file mode 100644 index 532f3d95..00000000 --- a/Sources/ApodiniMigrator/Migrator/Migrator.swift +++ /dev/null @@ -1,247 +0,0 @@ -// -// This source file is part of the Apodini open source project -// -// SPDX-FileCopyrightText: 2019-2021 Paul Schmiedmayer and the Apodini project authors (see CONTRIBUTORS.md) -// -// SPDX-License-Identifier: MIT -// - -import Foundation -import Logging - -/// A generator for a swift package -public struct Migrator { - enum MigratorError: Error { - case incompatible(message: String) - } - - /// Migrator logger labeled `org.apodini.migrator` - public static let logger: Logger = { - .init(label: "org.apodini.migrator") - }() - - /// Name of the package to be migrated - private let packageName: String - /// Path of the package - private let packagePath: Path - /// Document of the current version of the package - private var document: Document - /// Directories of the package - public let directories: ProjectDirectories - - /// Logger of the migrator - private let logger: Logger - /// Endpoints migrator - private let endpointsMigrator: EndpointsMigrator - /// Models migrator - private let modelsMigrator: ModelsMigrator - /// Networking migrator - private let networkingMigrator: NetworkingMigrator - /// All models of the client library (including old, deleted and added ones) - private let allModels: [TypeInformation] - /// Dictionary of js script convert methods from the migration guide - private let scripts: [Int: JSScript] - /// Dictionary of json values from the migration guide - private let jsonValues: [Int: JSONValue] - /// Dictionary of updated json representations from the migration guide - private let objectJSONs: [String: JSONValue] - /// Encoder configuration of the new version as calculated by the `networkingMigrator` - private let encoderConfiguration: EncoderConfiguration - - /// Initializes a new Migrator instance - /// - Parameters: - /// - packageName: name of the package - /// - packagePath: path of the package - /// - documentPath: path where the document is located - /// - migrationGuide: migration guide - public init(packageName: String, packagePath: String, documentPath: String, migrationGuide: MigrationGuide = .empty) throws { - self.packageName = packageName.trimmingCharacters(in: .whitespaces).without("/").upperFirst - self.packagePath = packagePath.asPath - document = try Document.decode(from: documentPath.asPath) - if let id = migrationGuide.id, document.id != id { - throw MigratorError.incompatible( - message: - """ - Migration guide is not compatible with the provided document. Apparently another old document version, - has been used to generate the migration guide - """ - ) - } - - self.directories = ProjectDirectories(packageName: packageName, packagePath: packagePath) - self.scripts = migrationGuide.scripts - self.jsonValues = migrationGuide.jsonValues - self.objectJSONs = migrationGuide.objectJSONs - let changeFilter: ChangeFilter = .init(migrationGuide) - endpointsMigrator = .init( - endpointsPath: directories.endpoints, - apiFilePath: directories.target, - allEndpoints: document.endpoints + changeFilter.addedEndpoints(), - endpointChanges: changeFilter.endpointChanges - ) - let oldModels = document.allModels() - let addedModels = changeFilter.addedModels() - self.allModels = oldModels + addedModels - modelsMigrator = .init( - path: directories.models, - oldModels: oldModels, - addedModels: addedModels, - modelChanges: changeFilter.modelChanges - ) - - networkingMigrator = .init( - networkingPath: directories.networking, - oldMetaData: document.metaData, - networkingChanges: changeFilter.networkingChanges - ) - self.encoderConfiguration = networkingMigrator.encoderConfiguration() - - logger = Self.logger - } - - /// Triggeres the rendering of migrated content of the library and persists changes - public func run() throws { - logger.info("Preparing project directories...") - try directories.build() - - try writeRootFiles() - - try writeHTTP() - - try writeUtils() - - try writeResources() - - log(.endpoints) - try endpointsMigrator.migrate() - - log(.models) - try modelsMigrator.migrate() - - try writeNetworking() - - try writeTests() - } - - /// Writes files at the root of the project - private func writeRootFiles() throws { - let readMe = readTemplate(.readme) - - try (directories.root + .readme).write(readMe) - - let package = readTemplate(.package) - .with(packageName: packageName) - .indentationFormatted() - - try (directories.root + .package).write(package) - } - - /// Writes files of `HTTP` directory - private func writeHTTP() throws { - log(.http) - let https = Template.httpTemplates - - try https.forEach { template in - let path = directories.http + template - try path.write(templateContentWithFileComment(template)) - } - } - - /// Writes files of `Utils` directory - private func writeUtils() throws { - log(.utils) - let utils = templateContentWithFileComment(.utils) - - try (directories.utils + Template.utils).write(utils) - } - - /// Writes files at `Resources` - private func writeResources() throws { - log(.resources) - try (directories.resources + Resources.jsScripts.rawValue).write(scripts.json) - try (directories.resources + Resources.jsonValues.rawValue).write(jsonValues.json) - } - - /// Writes files at `Networking` directory - private func writeNetworking() throws { - log(.networking) - let serverPath = networkingMigrator.serverPath() - let encoderConfiguration = self.encoderConfiguration.networkingDescription - let decoderConfiguration = networkingMigrator.decoderConfiguration().networkingDescription - let handler = templateContentWithFileComment(.handler) - let networking = templateContentWithFileComment(.networkingService, indented: false) - .with(serverPath, insteadOf: Template.serverPath) - .with(encoderConfiguration, insteadOf: Template.encoderConfiguration) - .with(decoderConfiguration, insteadOf: Template.decoderConfiguration) - .indentationFormatted() - let networkingDirectory = directories.networking - - try (networkingDirectory + .handler).write(handler) - try (networkingDirectory + .networkingService).write(networking) - } - - /// Writes files at test target - private func writeTests() throws { - log(.tests) - let tests = directories.tests - let testsTarget = directories.testsTarget - let testFileName = packageName + "Tests" + .swift - let testFile = TestFileTemplate( - allModels, - objectJSONs: objectJSONs, - encoderConfiguration: encoderConfiguration, - fileName: testFileName, - packageName: packageName - ).render().indentationFormatted() - - try (testsTarget + testFileName).write(testFile) - - let manifests = templateContentWithFileComment(.xCTestManifests).with(packageName: packageName) - try (testsTarget + .xCTestManifests).write(manifests) - let linuxMain = readTemplate(.linuxMain) - - try (tests + .linuxMain).write(linuxMain.indentationFormatted()) - } - - /// A util function to log persisting of content at a directory - private func log(_ directory: DirectoryName) { - logger.info("Persisting content at \(directories.path(of: directory).string.without(packagePath.string + "/"))") - } - - /// A util function that returns the string content of a template - private func readTemplate(_ template: Template) -> String { - template.content() - } - - /// Returns the string content of template file by also added the file header comment - private func templateContentWithFileComment(_ template: Template, indented: Bool = true, alternativeFileName: String? = nil) -> String { - let fileHeader = FileHeaderComment(fileName: alternativeFileName ?? template.projectFileName).render() + .doubleLineBreak - let fileContent = fileHeader + readTemplate(template) - return indented ? fileContent.indentationFormatted() : fileContent - } -} - - -extension DecoderConfiguration { - var networkingDescription: String { - """ - dateDecodingStrategy: .\(dateDecodingStrategy.rawValue), - dataDecodingStrategy: .\(dataDecodingStrategy.rawValue) - """ - } -} - -extension EncoderConfiguration { - var networkingDescription: String { - """ - dateEncodingStrategy: .\(dateEncodingStrategy.rawValue), - dataEncodingStrategy: .\(dataEncodingStrategy.rawValue) - """ - } -} - -extension String { - func with(packageName: String) -> String { - with(packageName, insteadOf: Template.packageName) - } -} diff --git a/Sources/ApodiniMigrator/Migrator/Models/Enum/EnumEncodeValueMethod.swift b/Sources/ApodiniMigrator/Migrator/Models/Enum/EnumEncodeValueMethod.swift deleted file mode 100644 index 680a8c9a..00000000 --- a/Sources/ApodiniMigrator/Migrator/Models/Enum/EnumEncodeValueMethod.swift +++ /dev/null @@ -1,28 +0,0 @@ -// -// This source file is part of the Apodini open source project -// -// SPDX-FileCopyrightText: 2019-2021 Paul Schmiedmayer and the Apodini project authors (see CONTRIBUTORS.md) -// -// SPDX-License-Identifier: MIT -// - -import Foundation - -/// Represents the `encodableValue()` util method in an enum -struct EnumEncodeValueMethod: Renderable { - /// Renders the content of the initializer in a non-formatted way - func render() -> String { - """ - private func encodableValue() throws -> Self { - let deprecated = Self.\(EnumDeprecatedCases.variableName) - guard deprecated.contains(self) else { - return self - } - if let alternativeCase = Self.allCases.first(where: { !deprecated.contains($0) }) { - return alternativeCase - } - throw ApodiniError(code: 404, message: "The web service does not support the cases of this enum anymore") - } - """ - } -} diff --git a/Sources/ApodiniMigrator/Migrator/Models/Enum/EnumExtensions.swift b/Sources/ApodiniMigrator/Migrator/Models/Enum/EnumExtensions.swift deleted file mode 100644 index 13abf10f..00000000 --- a/Sources/ApodiniMigrator/Migrator/Models/Enum/EnumExtensions.swift +++ /dev/null @@ -1,61 +0,0 @@ -// -// This source file is part of the Apodini open source project -// -// SPDX-FileCopyrightText: 2019-2021 Paul Schmiedmayer and the Apodini project authors (see CONTRIBUTORS.md) -// -// SPDX-License-Identifier: MIT -// - -import Foundation -import ApodiniTypeInformation - -struct EnumExtensions: Renderable { - let `enum`: TypeInformation - let rawValueType: TypeInformation - var typeName: String { - `enum`.typeString - } - - init(_ enum: TypeInformation, rawValueType: TypeInformation) { - self.enum = `enum` - self.rawValueType = rawValueType - } - - private func initBody() -> String { - if rawValueType == .scalar(.string) { - return "self.init(rawValue: description)" - } - - let body = - """ - if let rawValue = RawValue(description) { - self.init(rawValue: rawValue) - } else { - return nil - } - """ - return body - } - - func render() -> String { - let body = - """ - \(MARKComment("CustomStringConvertible")) - \(Kind.extension.rawValue) \(typeName): CustomStringConvertible { - \(GenericComment(comment: "/// Textual representation")) - public var description: String { - rawValue.description - } - } - - \(MARKComment("LosslessStringConvertible")) - \(Kind.extension.rawValue) \(typeName): LosslessStringConvertible { - \(GenericComment(comment: "/// Instantiates an instance of the conforming type from a string representation.")) - public init?(_ description: String) { - \(initBody()) - } - } - """ - return body - } -} diff --git a/Sources/ApodiniMigrator/Migrator/Models/Enum/EnumMigrator.swift b/Sources/ApodiniMigrator/Migrator/Models/Enum/EnumMigrator.swift deleted file mode 100644 index 17fbda58..00000000 --- a/Sources/ApodiniMigrator/Migrator/Models/Enum/EnumMigrator.swift +++ /dev/null @@ -1,146 +0,0 @@ -// -// This source file is part of the Apodini open source project -// -// SPDX-FileCopyrightText: 2019-2021 Paul Schmiedmayer and the Apodini project authors (see CONTRIBUTORS.md) -// -// SPDX-License-Identifier: MIT -// - -import Foundation -import ApodiniMigratorCompare - -/// An object that handles the migration of an enum declaration and renders the output accordingly -struct EnumMigrator: SwiftFile { - /// Type information enum that will be rendered - let typeInformation: TypeInformation - /// Kind of the file, always `.enum` - let kind: Kind = .enum - /// RawValue type of the enum, either int or string - private let rawValueType: TypeInformation - /// An unsupported change related to the enum from the migration guide, - private let unsupportedChange: UnsupportedChange? - /// A flag that indicates whether enum has been deleted in the new version - private let notPresentInNewVersion: Bool - /// All changes related to the `enum` - private let changes: [Change] - /// A dictionary holding updates of the raw values of the enum - private var rawValueUpdates: [EnumCase: EnumCase] = [:] - - /// Initializes a new instance out of an `enum` type information and its correspoinding changes - init(`enum`: TypeInformation, changes: [Change]) { - guard `enum`.isEnum, let rawValueType = `enum`.sanitizedRawValueType else { - fatalError("Attempted to initialize EnumMigrator with a non enum TypeInformation \(`enum`.rootType)") - } - typeInformation = `enum` - self.rawValueType = rawValueType - self.changes = changes - unsupportedChange = changes.first { $0.type == .unsupported } as? UnsupportedChange - notPresentInNewVersion = changes.contains(where: { $0.type == .deletion && $0.element.target == EnumTarget.`self`.rawValue }) - setRawValueUpdates() - } - - /// Returns the corresponding raw value of the case, considering potential updates - private func rawValue(for case: EnumCase) -> String { - if let updated = rawValueUpdates[`case`] { - return " = \(updated.name.doubleQuoted)" - } - return "" - } - - /// Filters changes and returns the added cases of the enum if any - private func addedCases() -> [EnumCase] { - var retValue: [EnumCase] = [] - - for change in changes where change.element.target == EnumTarget.case.rawValue { - if let addChange = change as? AddChange, case let .element(anyCodable) = addChange.added { - retValue.append(anyCodable.typed(EnumCase.self)) - } - } - return retValue - } - - /// Filters changes and returns the deleted cases of the enum if any - private func deletedCases() -> [EnumCase] { - var retValue: [EnumCase] = [] - - for change in changes where change.element.target == EnumTarget.case.rawValue { - if - let deleteChange = change as? DeleteChange, - case let .elementID(id) = deleteChange.deleted, - let deletedCase = typeInformation.enumCases.firstMatch(on: \.deltaIdentifier, with: id) - { - retValue.append(deletedCase) - } - } - return retValue - } - - /// Filters changes and sets the corresponding raw value updates in `rawValueUpdates` dictionary - private mutating func setRawValueUpdates() { - for change in changes where change.element.target == EnumTarget.caseRawValue.rawValue { - if - let renameChange = change as? UpdateChange, - case let .element(oldCase) = renameChange.from, - case let .element(newCase) = renameChange.to { - rawValueUpdates[oldCase.typed(EnumCase.self)] = newCase.typed(EnumCase.self) - } - } - } - - /// Renders the migrated content of the enum - func render() -> String { - var annotation: Annotation? - - if let unsupportedChange = unsupportedChange { - annotation = GenericComment( - comment: "@available(*, deprecated, message: \(unsupportedChange.description.doubleQuoted))" - ) - } else if notPresentInNewVersion { - annotation = GenericComment( - comment: "@available(*, message: \("This enum is not used in the new version anymore!".doubleQuoted))" - ) - } - - if let annotation = annotation { - let enumFileTemplate = DefaultEnumFile(typeInformation, annotation: annotation) - return enumFileTemplate.render() - } - - let addedCases = self.addedCases() - let allCases = (typeInformation.enumCases + addedCases).sorted(by: \.name) - - var addedCasesAnnotation = "" - - if !addedCases.isEmpty { - addedCasesAnnotation = "@available(*, message: \("This enum has been migrated with new cases. The client developer should ensure to adjust potential switch blocks of this enum".doubleQuoted))" + .lineBreak - } - - - let fileContent = - """ - \(fileComment) - - \(Import(.foundation).render()) - - \(MARKComment(.model)) - \(addedCasesAnnotation)\(kind.signature) \(typeNameString): \(rawValueType.nestedTypeString), Codable, CaseIterable { - \(allCases.map { "case \($0.name)\(rawValue(for: $0))" }.lineBreaked) - - \(MARKComment(.deprecated)) - \(EnumDeprecatedCases(deprecated: deletedCases()).render()) - - \(MARKComment(.encodable)) - \(EnumEncodingMethod().render()) - - \(MARKComment(.decodable)) - \(EnumDecoderInitializer(allCases).render()) - - \(MARKComment(.utils)) - \(EnumEncodeValueMethod().render()) - } - - \(EnumExtensions(typeInformation, rawValueType: rawValueType).render()) - """ - return fileContent - } -} diff --git a/Sources/ApodiniMigrator/Migrator/Models/ModelsMigrator.swift b/Sources/ApodiniMigrator/Migrator/Models/ModelsMigrator.swift deleted file mode 100644 index 313fd37d..00000000 --- a/Sources/ApodiniMigrator/Migrator/Models/ModelsMigrator.swift +++ /dev/null @@ -1,57 +0,0 @@ -// -// This source file is part of the Apodini open source project -// -// SPDX-FileCopyrightText: 2019-2021 Paul Schmiedmayer and the Apodini project authors (see CONTRIBUTORS.md) -// -// SPDX-License-Identifier: MIT -// - -import Foundation - -/// An object that handles / triggeres the migrated rendering of enums and objects of the client library -struct ModelsMigrator { - /// Path of `Models` folder of the client library - private let modelsPath: Path - /// Changed models of the client library - private let changedModels: [TypeInformation] - /// Unchanged models of the client library - private let unchangedModels: [TypeInformation] - /// All changes of the migration guide with element either an enum or an object - private let modelChanges: [Change] - - /// Initializer for a new instance - init(path: Path, oldModels: [TypeInformation], addedModels: [TypeInformation], modelChanges: [Change]) { - self.modelsPath = path - self.modelChanges = modelChanges - - let changedIds = modelChanges.map { $0.elementID }.unique() - var unchangedModels = Set(addedModels) - var changedModels: Set = [] - for old in oldModels { - if changedIds.contains(old.deltaIdentifier) { - changedModels.insert(old) - } else { - unchangedModels.insert(old) - } - } - self.changedModels = changedModels.asArray - self.unchangedModels = unchangedModels.asArray - } - - /// Triggeres the rendering of migrated content of model files - func migrate() throws { - let multipleFileRenderer = try MultipleFileRenderer(unchangedModels) - try multipleFileRenderer.write(at: modelsPath) - - for changedModel in changedModels { - let changes = modelChanges.filter { $0.elementID == changedModel.deltaIdentifier } - if changedModel.isEnum { - let enumMigrator = EnumMigrator(enum: changedModel, changes: changes) - try enumMigrator.write(at: modelsPath) - } else if changedModel.isObject { - let objectMigrator = ObjectMigrator(changedModel, changes: changes) - try objectMigrator.write(at: modelsPath) - } - } - } -} diff --git a/Sources/ApodiniMigrator/Migrator/Models/MultipleFileRenderer.swift b/Sources/ApodiniMigrator/Migrator/Models/MultipleFileRenderer.swift deleted file mode 100644 index 5d5a225c..00000000 --- a/Sources/ApodiniMigrator/Migrator/Models/MultipleFileRenderer.swift +++ /dev/null @@ -1,36 +0,0 @@ -// -// This source file is part of the Apodini open source project -// -// SPDX-FileCopyrightText: 2019-2021 Paul Schmiedmayer and the Apodini project authors (see CONTRIBUTORS.md) -// -// SPDX-License-Identifier: MIT -// - -import Foundation - -/// An object to write unchanged models at a specific directory -struct MultipleFileRenderer { - /// Swift files - private let files: [SwiftFile] - - /// Initializes generator from an array of `TypeInformation` elements. - init(_ typeInformation: [TypeInformation]) throws { - files = typeInformation - .map { typeInformation in - if typeInformation.isEnum { - return DefaultEnumFile(typeInformation) - } else { - return DefaultObjectFile(typeInformation) - } - } - } - - /// Persists `files` at the specified directory - /// - Parameter directory: path of directory where the files should be persisted - /// - Throws: if the path is not a valid directory path, or if the write operation fails - func write(at directory: Path) throws { - try files.forEach { - try $0.write(at: directory) - } - } -} diff --git a/Sources/ApodiniMigrator/Migrator/Models/Object/DecoderInitializer.swift b/Sources/ApodiniMigrator/Migrator/Models/Object/DecoderInitializer.swift deleted file mode 100644 index 179efca6..00000000 --- a/Sources/ApodiniMigrator/Migrator/Models/Object/DecoderInitializer.swift +++ /dev/null @@ -1,83 +0,0 @@ -// -// This source file is part of the Apodini open source project -// -// SPDX-FileCopyrightText: 2019-2021 Paul Schmiedmayer and the Apodini project authors (see CONTRIBUTORS.md) -// -// SPDX-License-Identifier: MIT -// - -import Foundation - -/// Represents `init(from decoder: Decoder)` initializer of a Decodable object -struct DecoderInitializer: Renderable { - /// All properties of the object that this initializer belongs to - let properties: [TypeProperty] - /// Deleted properties of the object if any - let deleted: [DeletedProperty] - /// Necessity changes related to the properties of the object - let necessityChanges: [UpdateChange] - /// Convert changes related to the properties of the object - let convertChanges: [UpdateChange] - - /// Initializer - init( - _ properties: [TypeProperty], - deleted: [DeletedProperty] = [], - necessityChanges: [UpdateChange] = [], - convertChanges: [UpdateChange] = [] - ) { - self.properties = properties - self.deleted = deleted - self.necessityChanges = necessityChanges - self.convertChanges = convertChanges - } - - /// Returns the corresponding line of `property` inside of the initializer by considering potential changes of the property - private func decodingLine(for property: TypeProperty) -> String { - let id = property.deltaIdentifier - let name = property.name - if let deletedProperty = deleted.firstMatch(on: \.id, with: id) { - let valueString: String - if case let .json(id) = deletedProperty.fallbackValue { - valueString = "try \(property.type.typeString).instance(from: \(id))" - } else { - valueString = "nil" - } - return "\(name) = \(valueString)" - } else if let change = necessityChanges.firstMatch(on: \.targetID, with: id), - let necessityValue = change.necessityValue, - case let .element(anyCodable) = change.to, - anyCodable.typed(Necessity.self) == .optional, - case let .json(id) = necessityValue { - return "\(name) = try container.decodeIfPresent(\(property.type.typeString).self, forKey: .\(name)) ?? (try \(property.type.typeString).instance(from: \(id)))" - } else if let change = convertChanges.firstMatch(on: \.targetID, with: id), - case let .element(anyCodable) = change.to, - let scriptID = change.convertToFrom { - let newType = anyCodable.typed(TypeInformation.self) - let decodeMethod = "decode\(newType.isOptional ? "IfPresent" : "")" - return "\(name) = try \(property.type.typeString).from(try container.\(decodeMethod)(\(newType.typeString.dropQuestionMark).self, forKey: .\(name)), script: \(scriptID))" - } else { - return property.decoderInitLine - } - } - - /// Renders the content of the initializer in a non-formatted way - func render() -> String { - """ - public init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - - \(properties.map { "\(decodingLine(for: $0))" }.lineBreaked) - } - """ - } -} - -/// TypeProperty extension -fileprivate extension TypeProperty { - /// The corresponding line of the property to be rendered inside `init(from decoder: Decoder)` if no change affected the property - var decoderInitLine: String { - let decodeMethodString = "decode\(type.isOptional ? "IfPresent" : "")" - return "\(name) = try container.\(decodeMethodString)(\(type.typeString.dropQuestionMark).self, forKey: .\(name))" - } -} diff --git a/Sources/ApodiniMigrator/Migrator/Models/Object/EncodingMethod.swift b/Sources/ApodiniMigrator/Migrator/Models/Object/EncodingMethod.swift deleted file mode 100644 index 9e14454c..00000000 --- a/Sources/ApodiniMigrator/Migrator/Models/Object/EncodingMethod.swift +++ /dev/null @@ -1,70 +0,0 @@ -// -// This source file is part of the Apodini open source project -// -// SPDX-FileCopyrightText: 2019-2021 Paul Schmiedmayer and the Apodini project authors (see CONTRIBUTORS.md) -// -// SPDX-License-Identifier: MIT -// - -import Foundation - -/// Represents `encode(to:)` method of an Encodable object -struct EncodingMethod: Renderable { - /// The properties of the object that this method belongs to (not including deleted ones) - private let properties: [TypeProperty] - /// Necessity changes related with the properties of the object - private let necessityChanges: [UpdateChange] - /// Convert changes related with the properties of the object - private let convertChanges: [UpdateChange] - - /// Initializer for a new instance with non-deleted properties of the object, necessity changes and convert changes - init(_ properties: [TypeProperty], necessityChanges: [UpdateChange] = [], convertChanges: [UpdateChange] = []) { - self.properties = properties - self.necessityChanges = necessityChanges - self.convertChanges = convertChanges - } - - /// Returns the corresponding line of the property inside of the method by considering all changes related with the property - private func encodingLine(for property: TypeProperty) -> String { - let id = property.deltaIdentifier - let name = property.name - if - let change = necessityChanges.firstMatch(on: \.targetID, with: id), - let necessityValue = change.necessityValue, - case let .element(anyCodable) = change.to, - anyCodable.typed(Necessity.self) == .required, - case let .json(id) = necessityValue { - return "try container.encode(\(name) ?? (try \(property.type.unwrapped.typeString).instance(from: \(id))), forKey: .\(name))" - } else if - let change = convertChanges.firstMatch(on: \.targetID, with: id), - case let .element(anyCodable) = change.to, - let scriptID = change.convertFromTo - { - let newType = anyCodable.typed(TypeInformation.self) - let encodeMethod = "encode\(newType.isOptional ? "IfPresent" : "")" - return "try container.\(encodeMethod)(try \(newType.typeString).from(\(name), script: \(scriptID)), forKey: .\(name))" - } else { - return property.encodingMethodLine - } - } - - /// Renders the content of the method in a non-formatted way - func render() -> String { - """ - public func encode(to encoder: Encoder) throws { - var container = encoder.container(keyedBy: CodingKeys.self) - - \(properties.map { "\(encodingLine(for: $0))" }.lineBreaked) - } - """ - } -} - -/// TypeProperty extension -extension TypeProperty { - /// The corresponding line of the property to be rendered inside `encode(to:)` method if the property is not affected by any change - var encodingMethodLine: String { - let encodeMethodString = "encode\(type.isOptional ? "IfPresent" : "")" - return "try container.\(encodeMethodString)(\(name), forKey: .\(name))" - } -} diff --git a/Sources/ApodiniMigrator/Migrator/Models/Object/ObjectMigrator.swift b/Sources/ApodiniMigrator/Migrator/Models/Object/ObjectMigrator.swift deleted file mode 100644 index 57e1b015..00000000 --- a/Sources/ApodiniMigrator/Migrator/Models/Object/ObjectMigrator.swift +++ /dev/null @@ -1,139 +0,0 @@ -// -// This source file is part of the Apodini open source project -// -// SPDX-FileCopyrightText: 2019-2021 Paul Schmiedmayer and the Apodini project authors (see CONTRIBUTORS.md) -// -// SPDX-License-Identifier: MIT -// - -import Foundation - -/// A util struct that holds an added property and its corresponding default value as provided by the migration guide -struct AddedProperty { - /// Property - let typeProperty: TypeProperty - /// Default value - let defaultValue: ChangeValue -} - -/// A util struct that holds the id of a deleted property and its corresponding fallback value as provided by the migration guide -struct DeletedProperty { - /// Id of the property - let id: DeltaIdentifier - /// Fallback value - let fallbackValue: ChangeValue -} - -/// An object that handles the migration of an object in the client library -struct ObjectMigrator: ObjectSwiftFile { - /// Type information of the object that will be migrated - var typeInformation: TypeInformation - /// Kind of the file, either object or struct - var kind: Kind - /// An unsupported change related to this object if any contained in the migration guide - private let unsupportedChange: UnsupportedChange? - /// A flag that indicates whether the object is present in the new version or not - private let notPresentInNewVersion: Bool - /// All old properties of the object - private let oldProperties: [TypeProperty] - /// Changes related to the object - private let changes: [Change] - /// All properties that have been added in the new version - private var addedProperties: [AddedProperty] = [] - /// All properties that have been deleted in the new version - private var deletedProperties: [DeletedProperty] = [] - /// All renaming changes of properties - private var renamePropertyChanges: [UpdateChange] = [] - /// All necessity changes of properties - private var propertyNecessityChanges: [UpdateChange] = [] - /// All convert changes of the properties - private var propertyConvertChanges: [UpdateChange] = [] - - /// Initializes a new instance out of an object type information, kind of the file and the changes related to the object - init(_ typeInformation: TypeInformation, kind: Kind = .struct, changes: [Change]) { - precondition([.struct, .class].contains(kind) && typeInformation.isObject, "Can't initialize an ObjectMigrator with a non object type information or file other than struct or class") - self.typeInformation = typeInformation - self.oldProperties = typeInformation.objectProperties - self.changes = changes - self.kind = kind - unsupportedChange = changes.first { $0.type == .unsupported } as? UnsupportedChange - notPresentInNewVersion = changes.contains(where: { $0.type == .deletion && $0.element.target == ObjectTarget.`self`.rawValue }) - collectPropertyChanges() - } - - /// Collects and stores property changes in the corresponding variables of the file - private mutating func collectPropertyChanges() { - let propertyTargets = [ObjectTarget.property, .necessity].map { $0.rawValue } - for change in changes where propertyTargets.contains(change.element.target) { - if let deleteChange = change as? DeleteChange, case let .elementID(id) = deleteChange.deleted { - deletedProperties.append(.init(id: id, fallbackValue: deleteChange.fallbackValue)) - } else if let addChange = change as? AddChange, case let .element(anyCodable) = addChange.added { - addedProperties.append(.init(typeProperty: anyCodable.typed(TypeProperty.self), defaultValue: addChange.defaultValue)) - } else if let updateChange = change as? UpdateChange { - if updateChange.type == .rename { - renamePropertyChanges.append(updateChange) - } else if updateChange.element.target == ObjectTarget.necessity.rawValue { - propertyNecessityChanges.append(updateChange) - } else if updateChange.type == .propertyChange { - propertyConvertChanges.append(updateChange) - } - } - } - } - - /// Renders the migrated result of the object - func render() -> String { - var annotation: Annotation? - if let unsupportedChange = unsupportedChange { - annotation = GenericComment( - comment: "@available(*, deprecated, message: \(unsupportedChange.description.doubleQuoted))" - ) - } else if notPresentInNewVersion { - annotation = GenericComment( - comment: "@available(*, deprecated, message: \"This model is not used in the new version anymore!\")" - ) - } - - if (oldProperties.isEmpty && addedProperties.isEmpty) || annotation != nil { - let objectFileTemplate = DefaultObjectFile(typeInformation, annotation: annotation) - return objectFileTemplate.render() - } - - let allProperties = (oldProperties + addedProperties.map(\.typeProperty)).sorted(by: \.name) - - let objectInitializer = ObjectInitializer(oldProperties, addedProperties: addedProperties) - let encodingMethod = EncodingMethod( - allProperties.filter { !deletedProperties.map(\.id).contains($0.deltaIdentifier) }, - necessityChanges: propertyNecessityChanges, - convertChanges: propertyConvertChanges - ) - - let decoderInitializer = DecoderInitializer( - allProperties, - deleted: deletedProperties, - necessityChanges: propertyNecessityChanges, - convertChanges: propertyConvertChanges - ) - - let fileContent = - """ - \(fileHeader(annotation: annotation?.comment ?? "")) - \(MARKComment(.codingKeys)) - \(ObjectCodingKeys(allProperties, renameChanges: renamePropertyChanges).render()) - - \(MARKComment(.properties)) - \(allProperties.map { $0.propertyLine }.lineBreaked) - - \(MARKComment(.initializer)) - \(objectInitializer.render()) - - \(MARKComment(.encodable)) - \(encodingMethod.render()) - - \(MARKComment(.decodable)) - \(decoderInitializer.render()) - } - """ - return fileContent - } -} diff --git a/Sources/ApodiniMigrator/Migrator/Networking/NetworkingMigrator.swift b/Sources/ApodiniMigrator/Migrator/Networking/NetworkingMigrator.swift deleted file mode 100644 index b89ce7f9..00000000 --- a/Sources/ApodiniMigrator/Migrator/Networking/NetworkingMigrator.swift +++ /dev/null @@ -1,46 +0,0 @@ -// -// This source file is part of the Apodini open source project -// -// SPDX-FileCopyrightText: 2019-2021 Paul Schmiedmayer and the Apodini project authors (see CONTRIBUTORS.md) -// -// SPDX-License-Identifier: MIT -// - -import Foundation -import ApodiniMigratorCompare - -struct NetworkingMigrator { - let networkingPath: Path - let oldMetaData: MetaData - let networkingChanges: [Change] - - func serverPath() -> String { - var serverPath = oldMetaData.versionedServerPath - for change in networkingChanges where change.element.target == NetworkingTarget.serverPath.rawValue { - if let updateChange = change as? UpdateChange, case let .stringValue(newPath) = updateChange.to { - serverPath = newPath - } - } - return serverPath - } - - func encoderConfiguration() -> EncoderConfiguration { - var encoderConfiguration = oldMetaData.encoderConfiguration - for change in networkingChanges where change.element.target == NetworkingTarget.encoderConfiguration.rawValue { - if let updateChange = change as? UpdateChange, case let .element(anyCodable) = updateChange.to { - encoderConfiguration = anyCodable.typed(EncoderConfiguration.self) - } - } - return encoderConfiguration - } - - func decoderConfiguration() -> DecoderConfiguration { - var decoderConfiguration = oldMetaData.decoderConfiguration - for change in networkingChanges where change.element.target == NetworkingTarget.decoderConfiguration.rawValue { - if let updateChange = change as? UpdateChange, case let .element(anyCodable) = updateChange.to { - decoderConfiguration = anyCodable.typed(DecoderConfiguration.self) - } - } - return decoderConfiguration - } -} diff --git a/Sources/ApodiniMigrator/Migrator/TestTarget/TestFileTemplate.swift b/Sources/ApodiniMigrator/Migrator/TestTarget/TestFileTemplate.swift deleted file mode 100644 index 88145594..00000000 --- a/Sources/ApodiniMigrator/Migrator/TestTarget/TestFileTemplate.swift +++ /dev/null @@ -1,97 +0,0 @@ -// -// This source file is part of the Apodini open source project -// -// SPDX-FileCopyrightText: 2019-2021 Paul Schmiedmayer and the Apodini project authors (see CONTRIBUTORS.md) -// -// SPDX-License-Identifier: MIT -// - -import Foundation -import ApodiniMigratorClientSupport -import ApodiniMigratorShared - -public struct TestFileTemplate: Renderable { - let models: [TypeInformation] - let fileName: String - let packageName: String - let objectJSONs: [String: JSONValue] - let encoderConfiguration: EncoderConfiguration - - public init( - _ models: [TypeInformation], - objectJSONs: [String: JSONValue] = [:], - encoderConfiguration: EncoderConfiguration = .default, - fileName: String, - packageName: String - ) { - self.models = models.sorted(by: \.typeString) - self.fileName = fileName - self.packageName = packageName - self.objectJSONs = objectJSONs - self.encoderConfiguration = encoderConfiguration - } - - private func dereference(_ model: TypeInformation) -> TypeInformation { - switch model { - case .scalar, .enum: return model - case let .repeated(element): return .repeated(element: dereference(element)) - case let .dictionary(key, value): return .dictionary(key: key, value: dereference(value)) - case let .optional(wrappedValue): return .optional(wrappedValue: dereference(wrappedValue)) - case let .object(name, properties, _): - return .object(name: name, properties: properties.map { .init(name: $0.name, type: dereference($0.type), annotation: $0.annotation) }) - case let .reference(key): - if let type = models.first(where: { $0.typeName.name == key.rawValue }) { - return dereference(type) - } - fatalError("Something went fundamentally wrong. Did not find the corresponding model of the reference with key: \(key.rawValue)") - } - } - - private func method(for model: TypeInformation) -> String { - let typeName = model.typeName.name - let jsonString: String - if let jsonValue = objectJSONs[typeName] { - jsonString = jsonValue.rawValue - } else { - jsonString = JSONStringBuilder.jsonString(dereference(model), with: encoderConfiguration) - } - - let returnValue = - """ - func test\(typeName)() throws { - let json: JSONValue = - \""" - \(jsonString) - \""" - - let instance = XCTAssertNoThrowWithResult(try \(typeName).instance(from: json)) - XCTAssertNoThrow(try \(typeName).encoder.encode(instance)) - } - """ - - return returnValue - } - - public func render() -> String { - """ - \(FileHeaderComment(fileName: fileName).render()) - - \(Import(.xCTest).render()) - @testable import \(packageName) - @testable \(Import(.apodiniMigratorClientSupport).render()) - - final class \(packageName)Tests: XCTestCase { - \(models.map { method(for: $0) }.joined(separator: .doubleLineBreak)) - - func XCTAssertNoThrowWithResult(_ expression: @autoclosure () throws -> T) -> T { - XCTAssertNoThrow(try expression()) - do { - return try expression() - } catch { - preconditionFailure(\"Expression threw an error: \\(error.localizedDescription)\") - } - } - } - """ - } -} diff --git a/Sources/ApodiniMigrator/NameComponents/GlobalPlaceholders.swift b/Sources/ApodiniMigrator/NameComponents/GlobalPlaceholders.swift new file mode 100644 index 00000000..d690d4d5 --- /dev/null +++ b/Sources/ApodiniMigrator/NameComponents/GlobalPlaceholders.swift @@ -0,0 +1,25 @@ +// +// This source file is part of the Apodini open source project +// +// SPDX-FileCopyrightText: 2019-2021 Paul Schmiedmayer and the Apodini project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + +import Foundation + +public extension Placeholder { + /// The global `PACKAGE_NAME` placeholder, which can be used to insert the Swift package name + /// into a ``Name``. + static var packageName: Placeholder { + Placeholder("PACKAGE_NAME") + } +} + +public extension Name { + /// The global `PACKAGE_NAME` placeholder, which can be used to insert the Swift package name + /// into a ``Name``. + static var packageName: Name { + "\(.packageName)" + } +} diff --git a/Sources/ApodiniMigrator/NameComponents/Name.swift b/Sources/ApodiniMigrator/NameComponents/Name.swift new file mode 100644 index 00000000..b0c22358 --- /dev/null +++ b/Sources/ApodiniMigrator/NameComponents/Name.swift @@ -0,0 +1,63 @@ +// +// This source file is part of the Apodini open source project +// +// SPDX-FileCopyrightText: 2019-2021 Paul Schmiedmayer and the Apodini project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + +import Foundation + +/// A `Name` is any kind of string which is built using ``NameComponent``s +/// which are either `String`s or ``Placeholder``s. +/// +/// There are several ways to build construct a `Name`: +/// +/// # Using string literals +/// ```swift +/// let name: Name = "Hello World" +/// ``` +/// +/// # Using string interpolation to insert Placeholders +/// This example inserts the globally defined `PACKAGE_NAME` placeholder. +/// The most convenient way to integrate ``Placeholder``s into ``Name``s is via the custom interpolation. +/// +/// ```swift +/// let name: Name = "The package is called \(.packageName)" +/// ``` +public struct Name: NameComponent { + var components: [NameComponent] + + var isEmpty: Bool { + components.isEmpty + } + + public init(empty: Void) { + self.components = [] + } + + public func description(with context: MigrationContext) -> String { + components + .map { $0.description(with: context ) } + .joined() + } + + public var description: String { + components + .map { $0.description } + .joined() + } +} + +extension Name: ExpressibleByStringLiteral { + public init(stringLiteral value: String) { + precondition(!value.contains("___"), "Placeholder replacements cannot be constructed via string literals!") + self.components = [value] + } +} + +extension Name: ExpressibleByStringInterpolation { + public init(stringInterpolation: NameStringInterpolation) { + self.components = stringInterpolation.components + } +} diff --git a/Sources/ApodiniMigrator/NameComponents/NameComponent.swift b/Sources/ApodiniMigrator/NameComponents/NameComponent.swift new file mode 100644 index 00000000..bfbb0063 --- /dev/null +++ b/Sources/ApodiniMigrator/NameComponents/NameComponent.swift @@ -0,0 +1,18 @@ +// +// This source file is part of the Apodini open source project +// +// SPDX-FileCopyrightText: 2019-2021 Paul Schmiedmayer and the Apodini project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + +import Foundation + +/// A `NameComponent` is a single component of a ``Name``. +/// It is typically a `String` literal or a ``Placeholder`` value. +public protocol NameComponent: CustomStringConvertible { + /// Retrieve the string representation of the ``NameComponent`` given the ``MigrationContext``. + /// - Parameter context: The ``MigrationContext`` which is used to retrieve values for a ``Placeholder``. + /// - Returns: The `String` with any ``Placeholder``s replaced (if they are present in the context). + func description(with context: MigrationContext) -> String +} diff --git a/Sources/ApodiniMigrator/NameComponents/NameStringInterpolation.swift b/Sources/ApodiniMigrator/NameComponents/NameStringInterpolation.swift new file mode 100644 index 00000000..c689a075 --- /dev/null +++ b/Sources/ApodiniMigrator/NameComponents/NameStringInterpolation.swift @@ -0,0 +1,44 @@ +// +// This source file is part of the Apodini open source project +// +// SPDX-FileCopyrightText: 2019-2021 Paul Schmiedmayer and the Apodini project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + +import Foundation + +public struct NameStringInterpolation: StringInterpolationProtocol { + public typealias StringLiteralType = String + + var components: [NameComponent] + + public init(literalCapacity: Int, interpolationCount: Int) { + self.components = [] + } + + private mutating func appendString(_ string: String, name: String) { + precondition(!string.contains("___"), "Placeholder replacements cannot be constructed via \(name)!") + components.append(string) + } + + public mutating func appendLiteral(_ literal: String) { + appendString(literal, name: "string literals") + } + + public mutating func appendInterpolation(_ value: Placeholder) { + components.append(value) + } + + public mutating func appendInterpolation(_ value: T) { + appendString(value.description, name: "CustomStringConvertible") + } + + public mutating func appendInterpolation(_ value: T) { + appendString("\(value)", name: "\(T.self)") + } + + public mutating func appendInterpolation(_ value: Any.Type) { + appendString("\(value)", name: "\(type(of: value))") + } +} diff --git a/Sources/ApodiniMigrator/NameComponents/Placeholder+NameComponent.swift b/Sources/ApodiniMigrator/NameComponents/Placeholder+NameComponent.swift new file mode 100644 index 00000000..8fa18a89 --- /dev/null +++ b/Sources/ApodiniMigrator/NameComponents/Placeholder+NameComponent.swift @@ -0,0 +1,19 @@ +// +// This source file is part of the Apodini open source project +// +// SPDX-FileCopyrightText: 2019-2021 Paul Schmiedmayer and the Apodini project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + +import Foundation + +extension Placeholder: NameComponent { + public func description(with context: MigrationContext) -> String { + guard let value = context.placeholderValues[self] else { + fatalError("Could not find value for placeholder \(self)") + } + + return value + } +} diff --git a/Sources/ApodiniMigrator/NameComponents/Placeholder.swift b/Sources/ApodiniMigrator/NameComponents/Placeholder.swift new file mode 100644 index 00000000..3fa99c2b --- /dev/null +++ b/Sources/ApodiniMigrator/NameComponents/Placeholder.swift @@ -0,0 +1,31 @@ +// +// This source file is part of the Apodini open source project +// +// SPDX-FileCopyrightText: 2019-2021 Paul Schmiedmayer and the Apodini project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + +import Foundation + +/// A `Placeholder` is a way of deferring the insertion of string content to a later point in time, +/// as this information may not yet be present or accessible. +/// E.g. this is commonly used to use the Swift package name in e.g. directory names or inside source code files. +public struct Placeholder: CustomStringConvertible { + /// The placeholder formatted string. + public var description: String { + "___\(name)___" + } + + /// The placeholder name. + public var name: String + + /// Create a new ``Placeholder`` given a placeholder name. + public init(_ name: String) { + self.name = name + } +} + +extension Placeholder: Equatable {} + +extension Placeholder: Hashable {} diff --git a/Sources/ApodiniMigratorCompare/MigrationGuide/SpecificationType.swift b/Sources/ApodiniMigrator/NameComponents/String+NameComponent.swift similarity index 67% rename from Sources/ApodiniMigratorCompare/MigrationGuide/SpecificationType.swift rename to Sources/ApodiniMigrator/NameComponents/String+NameComponent.swift index 6f016405..81137ec1 100644 --- a/Sources/ApodiniMigratorCompare/MigrationGuide/SpecificationType.swift +++ b/Sources/ApodiniMigrator/NameComponents/String+NameComponent.swift @@ -8,7 +8,8 @@ import Foundation -public enum SpecificationType: String, Value { - case apodini = "Apodini DSL" - case openapi = "OpenAPI" +extension String: NameComponent { + public func description(with context: MigrationContext) -> String { + self + } } diff --git a/Sources/ApodiniMigrator/NameComponents/String+ReplacingPlaceholder.swift b/Sources/ApodiniMigrator/NameComponents/String+ReplacingPlaceholder.swift new file mode 100644 index 00000000..59e1f1a8 --- /dev/null +++ b/Sources/ApodiniMigrator/NameComponents/String+ReplacingPlaceholder.swift @@ -0,0 +1,40 @@ +// +// This source file is part of the Apodini open source project +// +// SPDX-FileCopyrightText: 2019-2021 Paul Schmiedmayer and the Apodini project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + +import Foundation + +extension String { + // this is basically a `replacingOccurrences(of:with:)` + // though it considers the indent of a `placeholder` and applies it to the lines of `content` + mutating func replaceOccurrencesRespectingIndent(of target: String, with replacement: String) { + while let range = self.range(of: target) { + let indentString = indent(at: range) + let indentedReplacement = replacement + .split(separator: "\n", omittingEmptySubsequences: false) + .joined(separator: "\n\(indentString)") + self.replaceSubrange(range, with: indentedReplacement) + } + } + + private func indent(at range: Range) -> String { + var index = self.index(before: range.lowerBound) + + var indent = "" + + while self[index] == " " { + index = self.index(before: index) + indent += " " + } + + if self[index] == "\n" { + return indent + } + + return "" + } +} diff --git a/Sources/ApodiniMigrator/SourceCodeBuilder/EmptyLine.swift b/Sources/ApodiniMigrator/SourceCodeBuilder/EmptyLine.swift new file mode 100644 index 00000000..d4e61089 --- /dev/null +++ b/Sources/ApodiniMigrator/SourceCodeBuilder/EmptyLine.swift @@ -0,0 +1,16 @@ +// +// This source file is part of the Apodini open source project +// +// SPDX-FileCopyrightText: 2019-2021 Paul Schmiedmayer and the Apodini project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + +import Foundation + +/// An empty ``SourceCodeComponent``. +public struct EmptyLine: SourceCodeRenderable { + public init() {} + + public let renderableContent: String = "" +} diff --git a/Sources/ApodiniMigrator/SourceCodeBuilder/Group.swift b/Sources/ApodiniMigrator/SourceCodeBuilder/Group.swift new file mode 100644 index 00000000..a49f3463 --- /dev/null +++ b/Sources/ApodiniMigrator/SourceCodeBuilder/Group.swift @@ -0,0 +1,31 @@ +// +// This source file is part of the Apodini open source project +// +// SPDX-FileCopyrightText: 2019-2021 Paul Schmiedmayer and the Apodini project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + +import Foundation + +/// A way of Grouping several ``SourceCodeComponent``s. +/// This can be handy to treat multiple ``SourceCodeComponent``s as a single component (e.g. in combination with the ``Joined`` component). +public struct Group: SourceCodeComponent { + private let content: [SourceCodeComponent] + + /// Initialize a new `Group` + /// - Parameter content: The content of the group. + public init(@SourceCodeComponentBuilder content: () -> [SourceCodeComponent]) { + self.content = content() + } + + internal init(content: [SourceCodeComponent]) { + self.content = content + } + + public func render() -> [String] { + content + .map { $0.render() } + .flatten() + } +} diff --git a/Sources/ApodiniMigrator/SourceCodeBuilder/Indent.swift b/Sources/ApodiniMigrator/SourceCodeBuilder/Indent.swift new file mode 100644 index 00000000..0924169f --- /dev/null +++ b/Sources/ApodiniMigrator/SourceCodeBuilder/Indent.swift @@ -0,0 +1,43 @@ +// +// This source file is part of the Apodini open source project +// +// SPDX-FileCopyrightText: 2019-2021 Paul Schmiedmayer and the Apodini project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + +import Foundation + +/// Creates an indented section in the ``SourceCodeBuilder``. +public struct Indent: SourceCodeComponent { + private let indentString: String + private let content: [SourceCodeComponent] + + /// Creates a new indented content. + /// - Parameters: + /// - indentString: The indent string. + /// - content: The content as string. + public init(with indentString: String = " ", _ content: String) { + self.indentString = indentString + self.content = [content] + } + + /// Creates a new indented ``SourceCodeComponent``s build via a ``SourceCodeComponentBuilder`` closure. + /// - Parameters: + /// - indentString: The indent string. + /// - content: The content which is indented provided by a result builder closure. + public init( + with indentString: String = " ", + @SourceCodeComponentBuilder content: () -> [SourceCodeComponent] + ) { + self.indentString = indentString + self.content = content() + } + + public func render() -> [String] { + content + .map { $0.render() } + .flatten() + .map { indentString + $0 } + } +} diff --git a/Sources/ApodiniMigrator/SourceCodeBuilder/Joined.swift b/Sources/ApodiniMigrator/SourceCodeBuilder/Joined.swift new file mode 100644 index 00000000..a2c57374 --- /dev/null +++ b/Sources/ApodiniMigrator/SourceCodeBuilder/Joined.swift @@ -0,0 +1,68 @@ +// +// This source file is part of the Apodini open source project +// +// SPDX-FileCopyrightText: 2019-2021 Paul Schmiedmayer and the Apodini project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + +import Foundation + +/// Join several ``SourceCodeComponent`` +public struct Joined: SourceCodeComponent { + /// Describes how the ``SourceCodeComponent``s are joined. + /// This basically controls if the separator is applied before or after the newline character. + public enum JoinType { + case appendPreviousLine + case prependNextLine + } + + private let separator: String + private let joinType: JoinType + private let content: [SourceCodeComponent] + + /// Initialize new joined ``SourceCodeComponent``s. + /// - Parameters: + /// - separator: The separator character. + /// - joinType: Optionally, the ``JoinType``. + /// - content: The content of ``SourceCodeComponents``. + /// Note: You might want to use ``Group`` to group several ``SourceCodeComponents`` such that they are treated as + /// a single component here. + public init( + by separator: String, + using joinType: JoinType = .appendPreviousLine, + @SourceCodeComponentBuilder content: () -> [SourceCodeComponent] + ) { + self.separator = separator + self.joinType = joinType + self.content = content() + } + + public func render() -> [String] { + var lines = content + .map { $0.render() } + .filter { !$0.isEmpty } + + switch joinType { + case .appendPreviousLine: + for index in lines.startIndex ..< lines.index(before: lines.endIndex) { + let groupedLines = lines[index] + precondition(!groupedLines.isEmpty) + let groupIndex = groupedLines.index(before: groupedLines.endIndex) + + lines[index][groupIndex] = groupedLines[groupIndex] + separator + } + case .prependNextLine: + for index in lines.index(after: lines.startIndex) ..< lines.endIndex { + let groupedLines = lines[index] + precondition(!groupedLines.isEmpty) + let groupIndex = groupedLines.startIndex + + lines[index][groupIndex] = separator + groupedLines[groupIndex] + } + } + + return lines + .flatten() + } +} diff --git a/Sources/ApodiniMigrator/SourceCodeBuilder/SourceCodeBuilder.swift b/Sources/ApodiniMigrator/SourceCodeBuilder/SourceCodeBuilder.swift new file mode 100644 index 00000000..3eb42967 --- /dev/null +++ b/Sources/ApodiniMigrator/SourceCodeBuilder/SourceCodeBuilder.swift @@ -0,0 +1,88 @@ +// +// This source file is part of the Apodini open source project +// +// SPDX-FileCopyrightText: 2019-2021 Paul Schmiedmayer and the Apodini project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + +import Foundation + +// the protocols are to workaround inheritance and at the same time use enum +// type to host static-only members for efficiency. + +@resultBuilder +public enum SourceCodeBuilder: SourceCodeBuilderProtocol {} + +@resultBuilder +public enum SourceCodeComponentBuilder: SourceCodeComponentBuilderProtocol {} + + +/// The ``SourecCodeBuilder`` protocol. +public protocol SourceCodeComponentBuilderProtocol {} + +extension SourceCodeComponentBuilderProtocol { + /// Build ``SourceCodeComponent``s from a `CustomStringConvertible` expression. + @_disfavoredOverload + public static func buildExpression(_ expression: Convertible) -> [SourceCodeComponent] { + [expression.description] + } + + /// Build a ``SourceCodeComponent`` expression. + public static func buildExpression(_ expression: Renderable) -> [SourceCodeComponent] { + [expression] + } + + /// Build an array of ``SourceCodeComponent``s. + public static func buildExpression(_ expression: [Renderable]) -> [SourceCodeComponent] { + expression + } + + /// Builds a `Void` expression. This can be used to e.g. allow for property declarations inside + /// of the resultBuilder closure. + public static func buildExpression(_ expression: Void) -> [SourceCodeComponent] { + [] + } + + /// Build the block of ``SourceCodeComponent``s. + public static func buildBlock(_ components: [SourceCodeComponent]...) -> [SourceCodeComponent] { + components.flatten() + } + + /// Build either first. + public static func buildEither(first component: [SourceCodeComponent]) -> [SourceCodeComponent] { + [Group(content: component)] + } + + /// Build either second. + public static func buildEither(second component: [SourceCodeComponent]) -> [SourceCodeComponent] { + [Group(content: component)] + } + + /// Build an optional expression. + public static func buildOptional(_ component: [SourceCodeComponent]?) -> [SourceCodeComponent] { + // swiftlint:disable:previous discouraged_optional_collection + guard let component = component else { + return [] + } + return [Group(content: component)] + } + + /// Build an array (for loops). + public static func buildArray(_ components: [[SourceCodeComponent]]) -> [SourceCodeComponent] { + [Group(content: components.flatten())] + } +} + +public protocol SourceCodeBuilderProtocol: SourceCodeComponentBuilderProtocol {} + +extension SourceCodeBuilderProtocol { + /// Build the final source code file content. + /// This will render all the ``SourceCodeComponent``s and join them by the line separator `\n`. + public static func buildFinalResult(_ component: [SourceCodeComponent]) -> String { + component + .map { $0.render() } + .flatten() + .joined(separator: "\n") + } +} diff --git a/Sources/ApodiniMigrator/SourceCodeBuilder/SourceCodeComponent.swift b/Sources/ApodiniMigrator/SourceCodeBuilder/SourceCodeComponent.swift new file mode 100644 index 00000000..4254c6c9 --- /dev/null +++ b/Sources/ApodiniMigrator/SourceCodeBuilder/SourceCodeComponent.swift @@ -0,0 +1,27 @@ +// +// This source file is part of the Apodini open source project +// +// SPDX-FileCopyrightText: 2019-2021 Paul Schmiedmayer and the Apodini project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + +import Foundation + +/// Represents any element which can be part of source code. +/// +/// This is the core building block of the ``SourceCodeComponentBuilder`` and ``SourceCodeBuilder``. +public protocol SourceCodeComponent { + /// Called to render the source code of the `SourceCodeComponent`. + /// - Returns: Returns an array of lines. + func render() -> [String] +} + +extension String: SourceCodeComponent { + /// Every String is interpreted as a single line in the resulting code file. + public func render() -> [String] { + self + .split(separator: "\n", omittingEmptySubsequences: false) + .map { String($0) } + } +} diff --git a/Sources/ApodiniMigrator/SourceCodeBuilder/SourceCodeRenderable.swift b/Sources/ApodiniMigrator/SourceCodeBuilder/SourceCodeRenderable.swift new file mode 100644 index 00000000..1ca758b4 --- /dev/null +++ b/Sources/ApodiniMigrator/SourceCodeBuilder/SourceCodeRenderable.swift @@ -0,0 +1,25 @@ +// +// This source file is part of the Apodini open source project +// +// SPDX-FileCopyrightText: 2019-2021 Paul Schmiedmayer and the Apodini project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + +import Foundation + +/// Any element which can be rendered to source code using ``SourceCodeBuilder``. +public protocol SourceCodeRenderable: SourceCodeComponent { + /// The rendered source code content. + @SourceCodeBuilder + var renderableContent: String { get } +} + +public extension SourceCodeRenderable { + /// Default implementation for the render method based on the `renderableContent` property. + func render() -> [String] { + renderableContent + .split(separator: "\n", omittingEmptySubsequences: false) + .map { String($0) } + } +} diff --git a/Sources/ApodiniMigrator/Support/MARKComment.swift b/Sources/ApodiniMigrator/SourceCodeBuilder/Swift/Annotation.swift similarity index 53% rename from Sources/ApodiniMigrator/Support/MARKComment.swift rename to Sources/ApodiniMigrator/SourceCodeBuilder/Swift/Annotation.swift index 47f81d07..4edbfd51 100644 --- a/Sources/ApodiniMigrator/Support/MARKComment.swift +++ b/Sources/ApodiniMigrator/SourceCodeBuilder/Swift/Annotation.swift @@ -9,56 +9,80 @@ import Foundation /// A protocol for annotations that might appear in a `Swift` file -protocol Annotation: CustomStringConvertible { +public protocol Annotation: CustomStringConvertible { /// String content / comment of this annotation var comment: String { get } } /// Default `CustomStringConvertible`implementation -extension Annotation { +public extension Annotation { /// String representation of an annotation constructed with the specified `indentation` and `comment` var description: String { comment } } -struct GenericComment: Annotation { - let comment: String + +public struct GenericComment: Annotation { + public let comment: String + + public init(comment: String) { + self.comment = comment + } } -struct EndpointComment: Annotation { - let comment: String - - init(_ handlerName: String, path: String) { + +public struct EndpointComment: Annotation { + public let comment: String + + public init(_ handlerName: String, path: String) { comment = "/// API call for \(handlerName) at: \(path)" } } + /// A `MARK` comment annotation -struct MARKComment: Annotation { +public struct MARKComment: Annotation { + /// Distinct cases of mark comments that might appear in a file + public enum CommentType: String { + case model + case deprecated + case codingKeys + case properties + case initializer + case encodable + case decodable + case utils + case endpoints + + public var comment: String { + rawValue.upperFirst + } + } + /// The `// MARK: - ` string of the comment static let base = "// MARK: - " /// String content / comment of the instance - let comment: String + public let comment: String /// Initializer for a `MARKComment` instance /// - Parameters: /// - comment: The string that follows `// MARK: - ` - init(_ comment: String) { - self.comment = Self.base + comment.without(Self.base) + public init(_ comment: String) { + self.comment = Self.base + comment.replacingOccurrences(of: Self.base, with: "") } /// Initializer for a `MARKComment` instance /// - Parameters: /// - markCommentType: the type of the comment - init(_ markCommentType: MARKCommentType) { + public init(_ markCommentType: MARKComment.CommentType) { self.init(markCommentType.comment) } } extension MARKComment: Comparable { - static func < (lhs: MARKComment, rhs: MARKComment) -> Bool { + public static func < (lhs: MARKComment, rhs: MARKComment) -> Bool { lhs.comment < rhs.comment } } diff --git a/Sources/ApodiniMigrator/SourceCodeBuilder/Swift/FileHeaderComment.swift b/Sources/ApodiniMigrator/SourceCodeBuilder/Swift/FileHeaderComment.swift new file mode 100644 index 00000000..92962348 --- /dev/null +++ b/Sources/ApodiniMigrator/SourceCodeBuilder/Swift/FileHeaderComment.swift @@ -0,0 +1,29 @@ +// +// This source file is part of the Apodini open source project +// +// SPDX-FileCopyrightText: 2019-2021 Paul Schmiedmayer and the Apodini project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + +import Foundation + +/// An object that renders the header comment of a `Swift` file generated by `ApodiniMigrator` +public struct FileHeaderComment: SourceCodeRenderable { + static var testsDate: Date? + + public init() {} + + /// Returns the content of the file header comment + public var renderableContent: String { + let date = Self.testsDate ?? .init() + + """ + // + // Created by ApodiniMigrator on \(date.string(.date)) + // Copyright \u{00A9} \(date.string(.year)) TUM LS1. All rights reserved. + // + + """ + } +} diff --git a/Sources/ApodiniMigrator/SourceCodeBuilder/Swift/Import.swift b/Sources/ApodiniMigrator/SourceCodeBuilder/Swift/Import.swift new file mode 100644 index 00000000..92567e47 --- /dev/null +++ b/Sources/ApodiniMigrator/SourceCodeBuilder/Swift/Import.swift @@ -0,0 +1,59 @@ +// +// This source file is part of the Apodini open source project +// +// SPDX-FileCopyrightText: 2019-2021 Paul Schmiedmayer and the Apodini project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + +import Foundation + +/// An object representing imports of a Swift file +public struct Import: SourceCodeRenderable { + /// Distinct framework cases that can be imported in `ApodiniMigrator` + public enum Frameworks: String { + case foundation + case combine + case apodiniMigrator + case apodiniMigratorClientSupport + case xCTest + + /// String representation of the import + var string: String { + "import \(rawValue.upperFirst)" + } + } + + /// Set of to be imported frameworks + private var frameworks: Set + private var testable: Bool + + /// Initializes `self` with `frameworks` + public init(_ frameworks: Frameworks..., testable: Bool = false) { + self.frameworks = Set(frameworks.map { $0.string }) + self.testable = testable + } + + public init(_ frameworks: Name..., testable: Bool = false) { + self.frameworks = Set(frameworks.map { "import " + $0.description }) + self.testable = testable + } + + /// Inserts `framework` + public mutating func insert(_ framework: Frameworks, testable: Bool = false) { + precondition(self.testable == testable) + frameworks.insert(framework.string) + } + + /// String representation of `frameworks` + /// One line per framework, no empty lines in between + public var renderableContent: String { + for framework in frameworks.sorted() { + if testable { + "@testable \(framework)" + } else { + framework + } + } + } +} diff --git a/Sources/ApodiniMigrator/SourceCodeBuilder/Swift/Kind.swift b/Sources/ApodiniMigrator/SourceCodeBuilder/Swift/Kind.swift new file mode 100644 index 00000000..eefcc915 --- /dev/null +++ b/Sources/ApodiniMigrator/SourceCodeBuilder/Swift/Kind.swift @@ -0,0 +1,22 @@ +// +// This source file is part of the Apodini open source project +// +// SPDX-FileCopyrightText: 2019-2021 Paul Schmiedmayer and the Apodini project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + +import Foundation + +/// Distinct file / object types +public enum Kind: String { + case `struct` + case `class` + case `enum` + case `extension` + + /// Signature of `self`, classes are marked with `final` keyword + public var signature: String { + "public \(self == .class ? "final " : "")\(rawValue)" + } +} diff --git a/Sources/ApodiniMigrator/SourceCodeBuilder/Swift/SwiftFunction.swift b/Sources/ApodiniMigrator/SourceCodeBuilder/Swift/SwiftFunction.swift new file mode 100644 index 00000000..63d6503b --- /dev/null +++ b/Sources/ApodiniMigrator/SourceCodeBuilder/Swift/SwiftFunction.swift @@ -0,0 +1,157 @@ +// +// This source file is part of the Apodini open source project +// +// SPDX-FileCopyrightText: 2019-2021 Paul Schmiedmayer and the Apodini project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + +import Foundation + +/// A ``SourceCodeRenderable`` to render swift functions. +/// +/// Those can be placed inside ``SourceCodeBuilder``s. +public struct SwiftFunction: SourceCodeRenderable { + private static let indent: String = " " + + private let name: String + private let arguments: [String] + private let returnType: String? + private let access: String? + private let sendable: Bool + private let async: Bool + private let `throws` : Bool + private let whereClause: String? + private let functionBody: String? + + /// Create a new SwiftFunction. + /// + /// - Note: This initializer doesn't require a `functionBody` and thus only generates the function signature. + /// + /// - Parameters: + /// - name: The function name + /// - arguments: Array of function arguments (e.g. `["_ someString: String", "number: Int"]`) + /// - returnType: The string representation of a return type. `nil` for `Void`. + /// - access: The optional access level as a string representation. + /// - sendable: Defines if the function is annotated with `@Sendable`. + /// - async: Defines if the function is declared as `async`. + /// - throws: Defines if the function is declared as throwing. + /// - whereClause: A string representation of a where clause. + public init( + name: String, + arguments: [String] = [], + returnType: String? = nil, + access: String? = nil, + sendable: Bool = false, + async: Bool = false, + throws: Bool = false, + whereClause: String? = nil + ) { + self.name = name + self.arguments = arguments + self.returnType = returnType + self.access = access + self.sendable = sendable + self.async = async + self.throws = `throws` + self.whereClause = whereClause + self.functionBody = nil + } + + /// Create a new SwiftFunction. + /// - Parameters: + /// - name: The function name + /// - arguments: Array of function arguments (e.g. `["_ someString: String", "number: Int"]`) + /// - returnType: The string representation of a return type. `nil` for `Void`. + /// - access: The optional access level as a string representation. + /// - sendable: Defines if the function is annotated with `@Sendable`. + /// - async: Defines if the function is declared as `async`. + /// - throws: Defines if the function is declared as throwing. + /// - whereClause: A string representation of a where clause. + /// - functionBody: A ``SourceCodeBuilder`` closure to render the function body. + public init( + name: String, + arguments: [String] = [], + returnType: String? = nil, + access: String? = nil, + sendable: Bool = false, + async: Bool = false, + throws: Bool = false, + whereClause: String? = nil, + @SourceCodeBuilder functionBody: () -> String + ) { + self.name = name + self.arguments = arguments + self.returnType = returnType + self.access = access + self.sendable = sendable + self.async = async + self.throws = `throws` + self.whereClause = whereClause + self.functionBody = functionBody() + } + + public var renderableContent: String { + functionHead() + + if let body = functionBody { + Indent { + body + } + "}" // we assume that "functionHead" generated + } + } + + private func functionHead() -> String { + var head = "" + + if let access = access { + head += access + " " + } + + if sendable { + head += "@Sendable " + } + + head += "func \(name)(" + + var firstArgument = true + for argument in arguments { + if !firstArgument { + head += "," + } else { + firstArgument = false + } + + head += "\n" + head += "\(Self.indent)\(argument)" + } + if !arguments.isEmpty { + head += "\n" + } + + head += ")" + + if async { + head += " async" + } + + if `throws` { + head += " throws" + } + + if let returnType = returnType { + head += " -> \(returnType)" + } + + if let whereClause = whereClause { + head += " \(whereClause)" + } + + if functionBody != nil { + head += " {" + } + + return head + } +} diff --git a/Sources/ApodiniMigrator/Support/ChangeFilter.swift b/Sources/ApodiniMigrator/Support/ChangeFilter.swift deleted file mode 100644 index f76e6889..00000000 --- a/Sources/ApodiniMigrator/Support/ChangeFilter.swift +++ /dev/null @@ -1,55 +0,0 @@ -// -// This source file is part of the Apodini open source project -// -// SPDX-FileCopyrightText: 2019-2021 Paul Schmiedmayer and the Apodini project authors (see CONTRIBUTORS.md) -// -// SPDX-License-Identifier: MIT -// - -import Foundation - -/// A util object that serves to distribute changes to the elements that those belong to -struct ChangeFilter { - /// Filtered changes where change element is an endpoint - let endpointChanges: [Change] - /// Filtered changes where change element is a model (either object or enum) - let modelChanges: [Change] - /// Filtered changes where change element is related with `NetworkingService` - let networkingChanges: [Change] - - /// Initializes a new instance out of the migration guide - init(_ migrationGuide: MigrationGuide) { - let changes = migrationGuide.changes - endpointChanges = changes.filter { $0.element.isEndpoint } - modelChanges = changes.filter { $0.element.isModel } - networkingChanges = changes.filter { $0.element.isNetworking } - } - - /// Filters added models out of the model changes - func addedModels() -> [TypeInformation] { - modelChanges.compactMap { change in - if - change.element.target == ObjectTarget.`self`.rawValue, - let change = change as? AddChange, - case let .element(anyCodable) = change.added - { - return anyCodable.typed(TypeInformation.self) - } - return nil - } - } - - /// Filteres added endpoints out of the endpoint changes - func addedEndpoints() -> [Endpoint] { - endpointChanges.compactMap { change in - if - change.element.target == EndpointTarget.`self`.rawValue, - let change = change as? AddChange, - case let .element(anyCodable) = change.added - { - return anyCodable.typed(Endpoint.self) - } - return nil - } - } -} diff --git a/Sources/ApodiniMigrator/Support/Renderable.swift b/Sources/ApodiniMigrator/Support/Renderable.swift deleted file mode 100644 index e2502e49..00000000 --- a/Sources/ApodiniMigrator/Support/Renderable.swift +++ /dev/null @@ -1,23 +0,0 @@ -// -// This source file is part of the Apodini open source project -// -// SPDX-FileCopyrightText: 2019-2021 Paul Schmiedmayer and the Apodini project authors (see CONTRIBUTORS.md) -// -// SPDX-License-Identifier: MIT -// - -import Foundation - -/// A protocol for types that render a string content -protocol Renderable { - /// A functions that returns the string content of a `Renderable` instance - func render() -> String -} - -// MARK: - -extension Renderable { - /// Returns the formatted content of `render` - func indentationFormatted() -> String { - render().indentationFormatted() - } -} diff --git a/Sources/ApodiniMigrator/Support/Resource.swift b/Sources/ApodiniMigrator/Support/Resource.swift deleted file mode 100644 index 5befa9ee..00000000 --- a/Sources/ApodiniMigrator/Support/Resource.swift +++ /dev/null @@ -1,74 +0,0 @@ -// -// This source file is part of the Apodini open source project -// -// SPDX-FileCopyrightText: 2019-2021 Paul Schmiedmayer and the Apodini project authors (see CONTRIBUTORS.md) -// -// SPDX-License-Identifier: MIT -// - -import Foundation - -/// A protocol to allow manipulation of bundle resources -/// Conforming types should specify the `Bundle` where the resources are stored and the name of the file -/// By default `fileExtension` is set to `markdown`. `content()` and `data()` functions also provide default implementations -public protocol Resource { - /// File extension of this resource - var fileExtension: FileExtension { get } - /// Name of the resource file (without extension) - var name: String { get } - /// Bundle where this resource is stored - var bundle: Bundle { get } - - /// The read operations of these functions are performed from the `bundle` - /// Returns string content of the resource - func content() -> String - /// Returns the raw data of this resource. - func data() throws -> Data -} - -/// Default internal implementations -extension Resource { - /// name of the file - var fileName: String { - "\(name).\(fileExtension.description)" - } - - /// url of the file - var fileURL: URL { - guard let fileURL = bundle.url(forResource: fileName, withExtension: nil) else { - fatalError("Resource \(fileName) not found") - } - - return fileURL - } - - /// path of the resource file - var path: Path { - fileURL.path.asPath - } -} - -/// Default public implementations -public extension Resource { - /// file extension - var fileExtension: FileExtension { .markdown } - - /// string content of the file without last empty line - func content() -> String { - guard let content = try? String(contentsOf: fileURL, encoding: .utf8) else { - fatalError("Failed to read the resource") - } - let lines = content.sanitizedLines() - return lines.last?.isEmpty == true ? (lines.dropLast().joined(separator: .lineBreak)) : content - } - - /// raw data content of the file - func data() throws -> Data { - try Data(contentsOf: fileURL) - } - - /// Returns the decoded instance of the resource file - func instance() throws -> D { - try D.decode(from: try data()) - } -} diff --git a/Sources/ApodiniMigrator/Support/SwiftFile.swift b/Sources/ApodiniMigrator/Support/SwiftFile.swift deleted file mode 100644 index 5d53e9eb..00000000 --- a/Sources/ApodiniMigrator/Support/SwiftFile.swift +++ /dev/null @@ -1,161 +0,0 @@ -// -// This source file is part of the Apodini open source project -// -// SPDX-FileCopyrightText: 2019-2021 Paul Schmiedmayer and the Apodini project authors (see CONTRIBUTORS.md) -// -// SPDX-License-Identifier: MIT -// - -import Foundation - -/// Distinct file / object types -enum Kind: String { - case `struct` - case `class` - case `enum` - case `extension` - - /// Signature of `self`, classes are marked with `final` keyword - var signature: String { - "public \(self == .class ? "final " : "")\(rawValue)" - } -} - -/// A protocol that template models can conform to -protocol SwiftFile: Renderable { - /// The `typeInformation` for which the template will be created - var typeInformation: TypeInformation { get } - - /// The type of the content of the file - var kind: Kind { get } -} - -/// Distinct cases of mark comments that might appear in a file -enum MARKCommentType: String { - case model - case deprecated - case codingKeys - case properties - case initializer - case encodable - case decodable - case utils - case endpoints - - var comment: String { - rawValue.upperFirst - } -} - -/// `SwiftFileTemplate` default implementations -extension SwiftFile { - /// The string of the type name of the `typeInformation`, without the name of the module - var typeNameString: String { - typeInformation.typeName.name - } - - /// File extension - /// - Note: always `.swift` - var fileExtension: FileExtension { .swift } - - /// File name constructed from the type name and the file extension - var fileName: String { "\(typeNameString).\(fileExtension)" } - - /// File comment in the header of the `Swift` file - var fileComment: String { - FileHeaderComment(fileName: fileName).render() - } - - /// Writes the content of `render()` method at the specified path, formatted with `IndentationFormatter` - /// - Parameter directory: The path of directory where the content should be written - /// - Throws: if the writing of the content fails - /// - Returns: absolute path where the file is located - @discardableResult - func write(at directory: Path, alternativeFileName: String? = nil) throws -> Path { - let absolutePath = directory + (alternativeFileName ?? fileName) - try absolutePath.write(indentationFormatted()) - return absolutePath - } -} - -/// A protocol for object swift files (object models of the client library -protocol ObjectSwiftFile: SwiftFile {} -/// ObjectSwiftFile extension -extension ObjectSwiftFile { - /// File header including file comment, foundation import and the signature of object declaration - func fileHeader(annotation: String = "") -> String { - """ - \(fileComment) - - \(Import(.foundation).render()) - - \(MARKComment(.model)) - \(annotation)\(kind.signature) \(typeNameString): Codable { - """ - } -} - -/// An object that renders the header comment of a `Swift` file generated by `ApodiniMigrator` -public struct FileHeaderComment: Renderable { - static var testsDate: Date? - - /// Name of the file - public let fileName: String - - public init(fileName: String) { - self.fileName = fileName - } - - /// Returns the content of the file header comment - public func render() -> String { - let date = Self.testsDate ?? .init() - let body = - """ - // - // \(fileName) - // - // Created by ApodiniMigrator on \(date.string(.date)) - // Copyright \u{00A9} \(date.string(.year)) TUM LS1. All rights reserved. - // - """ - return body - } -} - -/// An object representing imports of a Swift file -struct Import: Renderable { - /// Distinct framework cases that can be imported in `ApodiniMigrator` - enum Frameworks: String { - case foundation - case combine - case apodiniMigrator - case apodiniMigratorClientSupport - case xCTest - - /// String representation of the import - var string: String { - "import \(rawValue.upperFirst)" - } - } - - /// Set of to be imported frameworks - private var frameworks: Set - - /// Initializes `self` with `frameworks` - init(_ frameworks: Frameworks...) { - self.frameworks = Set(frameworks) - } - - /// Inserts `framework` - mutating func insert(_ framework: Frameworks) { - frameworks.insert(framework) - } - - /// String represeantion of `frameworks` - /// One line per framwork, no empty lines in between - func render() -> String { - """ - \(frameworks.sorted(by: \.string).map { $0.string }.lineBreaked) - """ - } -} diff --git a/Sources/ApodiniMigrator/Support/Template.swift b/Sources/ApodiniMigrator/Support/Template.swift deleted file mode 100644 index 1c28fbaf..00000000 --- a/Sources/ApodiniMigrator/Support/Template.swift +++ /dev/null @@ -1,65 +0,0 @@ -// -// This source file is part of the Apodini open source project -// -// SPDX-FileCopyrightText: 2019-2021 Paul Schmiedmayer and the Apodini project authors (see CONTRIBUTORS.md) -// -// SPDX-License-Identifier: MIT -// - -import Foundation -import ApodiniMigratorCore - -/// Resources added to the client library -enum Resources: String { - case jsScripts = "js-convert-scripts.json" - case jsonValues = "json-values.json" -} - -enum Template: String, Resource { - case package - case readme - case apodiniError - case hTTPAuthorization - case hTTPHeaders - case hTTPMethod - case parameters - case handler - case networkingService - case utils - case xCTestManifests - case linuxMain - - static var httpTemplates: [Template] { - [.apodiniError, .hTTPAuthorization, .hTTPHeaders, .hTTPMethod, .parameters] - } - - /// Resource - var name: String { rawValue.upperFirst } - var bundle: Bundle { .module } - - var projectFileExtension: FileExtension { - switch self { - case .readme: return .markdown - default: return .swift - } - } - - var projectFileName: String { - name + projectFileExtension - } -} - - -extension Path { - static func + (lhs: Path, rhs: Template) -> Self { - lhs + rhs.projectFileName - } -} - -// MARK: - Placeholders -extension Template { - static let packageName = "___PACKAGE_NAME___" - static let encoderConfiguration = "___encoder___configuration___" - static let decoderConfiguration = "___decoder___configuration___" - static let serverPath = "___serverpath___" -} diff --git a/Sources/ApodiniMigrator/Templates/Readme.md b/Sources/ApodiniMigrator/Templates/Readme.md deleted file mode 100644 index ef5dc6b4..00000000 --- a/Sources/ApodiniMigrator/Templates/Readme.md +++ /dev/null @@ -1,2 +0,0 @@ -# Read me - diff --git a/Sources/ApodiniMigratorCLI/Compare.swift b/Sources/ApodiniMigratorCLI/Compare.swift index 2c06f8e2..b453fb37 100644 --- a/Sources/ApodiniMigratorCLI/Compare.swift +++ b/Sources/ApodiniMigratorCLI/Compare.swift @@ -8,7 +8,7 @@ import Foundation import ArgumentParser -import ApodiniMigrator +import RESTMigrator struct Compare: ParsableCommand { static var configuration = CommandConfiguration( @@ -28,13 +28,12 @@ struct Compare: ParsableCommand { var format: OutputFormat = .json func run() throws { - let migrator = ApodiniMigrator.Migrator.self - let logger = migrator.logger + let logger = RESTMigrator.logger logger.info("Starting generation of the migration guide...") do { let migrationGuideFileName = "migration_guide" - let migrationGuide = try MigrationGuide.from(oldDocumentPath.asPath, newDocumentPath.asPath) + let migrationGuide = try MigrationGuide.from(Path(oldDocumentPath), Path(newDocumentPath)) let filePath = try migrationGuide.write(at: migrationGuidePath, outputFormat: format, fileName: migrationGuideFileName) logger.info("Migration guide was generated successfully at \(filePath).") } catch { diff --git a/Sources/ApodiniMigratorCLI/Generate.swift b/Sources/ApodiniMigratorCLI/Generate.swift index 20e2c332..03472174 100644 --- a/Sources/ApodiniMigratorCLI/Generate.swift +++ b/Sources/ApodiniMigratorCLI/Generate.swift @@ -8,7 +8,7 @@ import Foundation import ArgumentParser -import ApodiniMigrator +import RESTMigrator struct Generate: ParsableCommand { static var configuration = CommandConfiguration( @@ -25,18 +25,14 @@ struct Generate: ParsableCommand { var documentPath: String func run() throws { - let migrator = ApodiniMigrator.Migrator.self - let logger = migrator.logger + let logger = RESTMigrator.logger logger.info("Starting generation of package \(packageName)") do { - let generator = try migrator.init( - packageName: packageName, - packagePath: targetDirectory, - documentPath: documentPath - ) - try generator.run() + let migrator = try RESTMigrator(documentPath: documentPath) + + try migrator.run(packageName: packageName, packagePath: targetDirectory) logger.info("Package \(packageName) was generated successfully. You can open the package via \(packageName)/Package.swift") } catch { logger.error("Package generation failed with error: \(error)") diff --git a/Sources/ApodiniMigratorCLI/Migrate.swift b/Sources/ApodiniMigratorCLI/Migrate.swift index 2c90f2ba..636208e2 100644 --- a/Sources/ApodiniMigratorCLI/Migrate.swift +++ b/Sources/ApodiniMigratorCLI/Migrate.swift @@ -8,7 +8,7 @@ import Foundation import ArgumentParser -import ApodiniMigrator +import RESTMigrator struct Migrate: ParsableCommand { static var configuration = CommandConfiguration( @@ -28,20 +28,17 @@ struct Migrate: ParsableCommand { var migrationGuidePath: String func run() throws { - let migratorType = ApodiniMigrator.Migrator.self - let logger = migratorType.logger + let logger = RESTMigrator.logger logger.info("Starting migration of package \(packageName)") do { - let migrationGuide = try MigrationGuide.decode(from: migrationGuidePath.asPath) - let migrator = try migratorType.init( - packageName: packageName, - packagePath: targetDirectory, + let migrator = try RESTMigrator( documentPath: documentPath, - migrationGuide: migrationGuide + migrationGuidePath: migrationGuidePath ) - try migrator.run() + + try migrator.run(packageName: packageName, packagePath: targetDirectory) logger.info("Package \(packageName) was migrated successfully. You can open the package via \(packageName)/Package.swift") } catch { logger.error("Package migration failed with error: \(error)") diff --git a/Sources/ApodiniMigratorClientSupport/ApodiniMigratorCodable+Extensions.swift b/Sources/ApodiniMigratorClientSupport/ApodiniMigratorCodable+Extensions.swift index 660a8d01..5d57b1cd 100644 --- a/Sources/ApodiniMigratorClientSupport/ApodiniMigratorCodable+Extensions.swift +++ b/Sources/ApodiniMigratorClientSupport/ApodiniMigratorCodable+Extensions.swift @@ -182,7 +182,7 @@ extension ApodiniMigratorEncodable { fileprivate extension String { /// A function to be applied on js scripts to retrieve the name of the function func functionName() -> String { - let dropFunction = without("function ") + let dropFunction = replacingOccurrences(of: "function ", with: "") if let idx = dropFunction.firstIndex(of: "(") { return String(dropFunction[dropFunction.startIndex ..< idx]) diff --git a/Sources/ApodiniMigratorClientSupport/JSScript.swift b/Sources/ApodiniMigratorClientSupport/JSScript.swift index 9dbf085f..d15abf31 100644 --- a/Sources/ApodiniMigratorClientSupport/JSScript.swift +++ b/Sources/ApodiniMigratorClientSupport/JSScript.swift @@ -26,7 +26,7 @@ public struct JSScript: Value, RawRepresentable { /// Creates a new instance by decoding from the given decoder. public init(from decoder: Decoder) throws { - rawValue = try decoder.singleValueContainer().decode(String.self) + try rawValue = decoder.singleValueContainer().decode(String.self) } /// Encodes self into the given encoder. diff --git a/Sources/ApodiniMigratorCompare/Change/ChangeContextNode.swift b/Sources/ApodiniMigratorCompare/Change/ChangeContextNode.swift deleted file mode 100644 index 57195b08..00000000 --- a/Sources/ApodiniMigratorCompare/Change/ChangeContextNode.swift +++ /dev/null @@ -1,149 +0,0 @@ -// -// This source file is part of the Apodini open source project -// -// SPDX-FileCopyrightText: 2019-2021 Paul Schmiedmayer and the Apodini project authors (see CONTRIBUTORS.md) -// -// SPDX-License-Identifier: MIT -// - -import Foundation - -/// A reference object to register changes during comparison of documents of two versions. -/// Used internally in the migration guide generation to be passed in the DocumentComparator. Furthermore -/// handles the logic of encoding and decoding different change types -final class ChangeContextNode: Codable { - /// Changes of the container - private(set) var changes: [Change] - /// Compare config passed from migration guide, property is owned by the migration guide, and does not get encoded or decoded from `self` - var compareConfiguration: CompareConfiguration? - - /// All javascript convert methods created during comparison - var scripts: [Int: JSScript] - /// All json values of properties or parameter that require a default or fallback value - var jsonValues: [Int: JSONValue] - /// All json representations of objects that had some kind of breaking change in their properties - private(set) var objectJSONs: [String: JSONValue] - /// All models that have some kind of breaking change in the new version - private(set) var rhsModels: [TypeInformation] = [] - - /// Returns whether `changes` is empty - var isEmpty: Bool { - changes.isEmpty - } - - /// Initializes `self` with empty changes - init(compareConfiguration: CompareConfiguration? = nil) { - changes = [] - self.compareConfiguration = compareConfiguration - scripts = [:] - jsonValues = [:] - objectJSONs = [:] - } - - /// Encodes `self` into the given encoder via an `unkeyedContainer` - func encode(to encoder: Encoder) throws { - var container = encoder.unkeyedContainer() - - for change in changes { - if let change = change as? AddChange { - try container.encode(change) - } - - if let change = change as? DeleteChange { - try container.encode(change) - } - - if let change = change as? UpdateChange { - try container.encode(change) - } - } - } - - /// Creates a new instance by decoding from the given decoder - init(from decoder: Decoder) throws { - var container = try decoder.unkeyedContainer() - self.changes = [] - scripts = [:] - jsonValues = [:] - objectJSONs = [:] - - while !container.isAtEnd { - if let value = try? container.decode(AddChange.self) { - changes.append(value) - } - - if let value = try? container.decode(DeleteChange.self) { - changes.append(value) - } - - if let value = try? container.decode(UpdateChange.self) { - changes.append(value) - } - } - } - - /// Registers `change` to `self` - func add(_ change: Change) { - changes.append(change) - } - - /// Stores the script and returns its stored index - func store(script: JSScript) -> Int { - let count = scripts.count - scripts[count] = script - return count - } - - /// Stores the jsonValue and returns stored index - func store(jsonValue: JSONValue) -> Int { - let count = jsonValues.count - jsonValues[count] = jsonValue - return count - } - - /// For every compare between two models of different versions, this function is called to register potentially updated json representation of an object - func store(rhs: TypeInformation, encoderConfiguration: EncoderConfiguration) { - let propertyTargets = [ObjectTarget.property, .necessity].map { $0.rawValue } - if changes.contains(where: { - $0.breaking - && $0.element.isObject - && $0.elementID == rhs.deltaIdentifier - && propertyTargets.contains($0.element.target) - }) { - objectJSONs[rhs.typeName.name] = .init(JSONStringBuilder.jsonString(rhs, with: encoderConfiguration)) - } - } - - func set(rhsModels: [TypeInformation]) -> [TypeInformation] { - self.rhsModels = rhsModels - return rhsModels - } - - func currentVersion(of lhs: TypeInformation) -> TypeInformation { - switch lhs { - case .scalar: return lhs - case let .repeated(element): return .repeated(element: currentVersion(of: element)) - case let .dictionary(key, value): return .dictionary(key: key, value: currentVersion(of: value)) - case let .optional(wrappedValue): return .optional(wrappedValue: wrappedValue) - case .enum, .object: return rhsModels.firstMatch(on: \.deltaIdentifier, with: lhs.deltaIdentifier) ?? lhs - case .reference: fatalError("Encountered a reference in `\(Self.self)`") - } - } - - func typeRenames() -> [UpdateChange] { - guard compareConfiguration?.allowTypeRename == true else { - return [] - } - - return changes.filter { $0.type == .rename && $0.element.target == ObjectTarget.typeName.rawValue } as? [UpdateChange] ?? [] - } - - func typesAreRenamings(lhs: TypeInformation, rhs: TypeInformation) -> Bool { - typeRenames().contains(where: { rename in - if case let .stringValue(lhsName) = rename.from, case let .stringValue(rhsName) = rename.to { - return lhsName == lhs.deltaIdentifier.rawValue && rhsName == rhs.deltaIdentifier.rawValue - } - return false - }) - } -} diff --git a/Sources/ApodiniMigratorCompare/Change/ChangeElement.swift b/Sources/ApodiniMigratorCompare/Change/ChangeElement.swift deleted file mode 100644 index ff1782ce..00000000 --- a/Sources/ApodiniMigratorCompare/Change/ChangeElement.swift +++ /dev/null @@ -1,144 +0,0 @@ -// -// This source file is part of the Apodini open source project -// -// SPDX-FileCopyrightText: 2019-2021 Paul Schmiedmayer and the Apodini project authors (see CONTRIBUTORS.md) -// -// SPDX-License-Identifier: MIT -// - -import Foundation -import ApodiniMigratorCore - -public enum ElementType: String, Value { - case endpoint - case `enum` - case object - case networking -} - -/// Represents distinct top-level elements that are subject to change in the web service -public enum ChangeElement: DeltaIdentifiable, Value { - // MARK: Private Inner Types - private enum CodingKeys: String, CodingKey { - case endpoint, `enum`, object, networking, target - } - - /// Represents an endpoint change element identified by its id and the corresponding endpoint change target - case endpoint(DeltaIdentifier, target: EndpointTarget) - /// An internal convenience static method to return an `.endpoint` change element with its corresponding target - static func `for`(endpoint: Endpoint, target: EndpointTarget) -> ChangeElement { - .endpoint(endpoint.deltaIdentifier, target: target) - } - - /// Represents an enum change element identified by its id and the corresponding enum change target - case `enum`(DeltaIdentifier, target: EnumTarget) - /// An internal convenience static method to return an `.enum` change element with its corresponding target - static func `for`(enum: TypeInformation, target: EnumTarget) -> ChangeElement { - .enum(`enum`.deltaIdentifier, target: target) - } - - /// Represents an object change element identified by its id and the corresponding object change target - case object(DeltaIdentifier, target: ObjectTarget) - /// An internal convenience static method to return an `.object` change element with its corresponding target - static func `for`(object: TypeInformation, target: ObjectTarget) -> ChangeElement { - .object(object.deltaIdentifier, target: target) - } - - /// Represents an networking change element and the corresponding networking change target - /// - Note: Networking change element always have `DeltaIdentifier("NetworkingService")` as id - case networking(target: NetworkingTarget) - - /// Encodes `self` into the given encoder - public func encode(to encoder: Encoder) throws { - var container = encoder.container(keyedBy: CodingKeys.self) - let key: CodingKeys - switch self { - case .endpoint: key = .endpoint - case .enum: key = .enum - case .object: key = .object - case .networking: key = .networking - } - try container.encode(deltaIdentifier, forKey: key) - try container.encode(target, forKey: .target) - } - - /// Creates a new instance by decoding from the given decoder - public init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - let keys = container.allKeys - - if keys.contains(.endpoint) { - let target = try container.decode(EndpointTarget.self, forKey: .target) - self = .endpoint(try container.decode(DeltaIdentifier.self, forKey: .endpoint), target: target) - } else if keys.contains(.enum) { - let target = try container.decode(EnumTarget.self, forKey: .target) - self = .enum(try container.decode(DeltaIdentifier.self, forKey: .enum), target: target) - } else if keys.contains(.object) { - let target = try container.decode(ObjectTarget.self, forKey: .target) - self = .object(try container.decode(DeltaIdentifier.self, forKey: .object), target: target) - } else if keys.contains(.networking) { - let target = try container.decode(NetworkingTarget.self, forKey: .target) - self = .networking(target: target) - } else { - throw DecodingError.dataCorrupted(.init(codingPath: keys, debugDescription: "Failed to decode \(Self.self)")) - } - } -} - -// MARK: - ChangeElement -public extension ChangeElement { - /// Returns the delta identifier of the change element - var deltaIdentifier: DeltaIdentifier { - switch self { - case let .endpoint(deltaIdentifier, _): return deltaIdentifier - case let .enum(deltaIdentifier, _): return deltaIdentifier - case let .object(deltaIdentifier, _): return deltaIdentifier - case .networking: return "NetworkingService" - } - } - - /// Returns the corresponding string raw value of the target of `self` - var target: String { - switch self { - case let .endpoint(_, target): return target.rawValue - case let .enum(_, target): return target.rawValue - case let .object(_, target): return target.rawValue - case let .networking(target): return target.rawValue - } - } - - /// Type of the change element - var type: ElementType { - switch self { - case .endpoint: return .endpoint - case .enum: return .enum - case .object: return .object - case .networking: return .networking - } - } - - /// Indicates whether `self` is an `.endpoint` change element - var isEndpoint: Bool { - type == .endpoint - } - - /// Indicates whether `self` is an `.enum` change element - var isEnum: Bool { - type == .enum - } - - /// Indicates whether `self` is an `.object` change element - var isObject: Bool { - type == .object - } - - /// Indicates whether `self` is an `.enum` or `.object` change element - var isModel: Bool { - isEnum || isObject - } - - /// Indicates whether `self` is an `.networking` change element - var isNetworking: Bool { - type == .networking - } -} diff --git a/Sources/ApodiniMigratorCompare/Change/Changes/UpdateChange.swift b/Sources/ApodiniMigratorCompare/Change/Changes/UpdateChange.swift deleted file mode 100644 index c22e5c94..00000000 --- a/Sources/ApodiniMigratorCompare/Change/Changes/UpdateChange.swift +++ /dev/null @@ -1,203 +0,0 @@ -// -// This source file is part of the Apodini open source project -// -// SPDX-FileCopyrightText: 2019-2021 Paul Schmiedmayer and the Apodini project authors (see CONTRIBUTORS.md) -// -// SPDX-License-Identifier: MIT -// - -import Foundation - -public enum ParameterChangeTarget: String, Value { - case necessity - case kind - case typeInformation = "type" -} - -/// Represents an update change of an arbitrary element from some old value to some new value, -/// the most frequent change that can appear in the Migration guide. Depending on the change element -/// and the target, the type of an update change can either be a generic `.update` or a `.rename`, `.propertyChange`, `.parameterChange` or `.responseChange`, -/// which can be initialized through different initalizers -public struct UpdateChange: Change { - // MARK: Private Inner Types - enum CodingKeys: String, CodingKey { - case element - case type = "change-type" - case parameterTarget = "parameter-target" - case targetID = "target-id" - case from - case to - case similarity = "similarity-score" - case necessityValue = "necessity-value" - case convertFromTo = "convert-from-to-script-id" - case convertToFrom = "convert-to-from-script-id" - case convertionWarning = "convertion-warning" - case breaking - case solvable - case providerSupport = "provider-support" - } - - /// Top-level changed element related to the change - public let element: ChangeElement - /// Type of change, can either be a generic `.update` or a `.rename`, `.propertyChange`, `.parameterChange` or `.responseChange` - public let type: ChangeType - /// Old value of the target - public let from: ChangeValue - /// New value of the target - public let to: ChangeValue - /// Similarity score from 0 to 1 for renamings - public let similarity: Double? - /// Optional id of the target - public let targetID: DeltaIdentifier? - /// A json id in case that the necessity of a property or a parameter changed - public let necessityValue: ChangeValue? - /// JS convert function to convert old type to new type - public let convertFromTo: Int? - /// JS convert function to convert new type to old type, e.g. if the change element is an object and the target is property - public let convertToFrom: Int? - /// Warning regarding the provided convert scripts - public let convertionWarning: String? - /// The target of the parameter which is related to the change if type is a `parameterChange` - public let parameterTarget: ParameterChangeTarget? - /// Indicates whether the change is non-backward compatible - public let breaking: Bool - /// Indicates whether the change can be handled by `ApodiniMigrator` - public let solvable: Bool - /// Provider support field if change type is a rename and `compare-config` of the Migration Guide is set to `true` for `include-provider-support` - public let providerSupport: ProviderSupport? - - /// Initializer for an UpdateChange with type `.update` - init( - element: ChangeElement, - from: ChangeValue, - to: ChangeValue, - necessityValue: ChangeValue? = nil, - targetID: DeltaIdentifier? = nil, - breaking: Bool, - solvable: Bool - ) { - self.element = element - self.from = from - self.to = to - self.targetID = targetID - self.similarity = nil - self.necessityValue = necessityValue - convertFromTo = nil - convertToFrom = nil - convertionWarning = nil - parameterTarget = nil - self.breaking = breaking - self.solvable = solvable - self.providerSupport = nil - type = .update - } - - /// Initializer for an UpdateChange with type `.rename` - init( - element: ChangeElement, - from: String, - to: String, - similarity: Double?, - breaking: Bool, - solvable: Bool, - includeProviderSupport: Bool = false - ) { - self.element = element - self.from = .stringValue(from) - self.to = .stringValue(to) - self.similarity = similarity - targetID = .init(from) - self.necessityValue = nil - convertFromTo = nil - convertToFrom = nil - convertionWarning = nil - self.parameterTarget = nil - self.breaking = breaking - self.solvable = solvable - self.providerSupport = includeProviderSupport ? .renameValidationHint : nil - type = .rename - } - - /// Initializer for an UpdateChange with type `.responseChange` - init( - element: ChangeElement, - from: ChangeValue, - to: ChangeValue, - convertToFrom: Int, - convertionWarning: String?, - breaking: Bool, - solvable: Bool - ) { - self.element = element - self.from = from - self.to = to - self.similarity = nil - self.targetID = nil - self.necessityValue = nil - self.convertFromTo = nil - self.convertToFrom = convertToFrom - self.convertionWarning = convertionWarning - self.parameterTarget = nil - self.breaking = breaking - self.solvable = solvable - self.providerSupport = nil - type = .responseChange - } - - /// Initializer for an UpdateChange with type `.propertyChange` - init( - element: ChangeElement, - from: ChangeValue, - to: ChangeValue, - targetID: DeltaIdentifier, - convertFromTo: Int, - convertToFrom: Int, - convertionWarning: String?, - breaking: Bool, - solvable: Bool - ) { - self.element = element - self.from = from - self.to = to - self.similarity = nil - self.targetID = targetID - self.necessityValue = nil - self.convertFromTo = convertFromTo - self.convertToFrom = convertToFrom - self.convertionWarning = convertionWarning - self.parameterTarget = nil - self.breaking = breaking - self.solvable = solvable - self.providerSupport = nil - type = .propertyChange - } - - /// Initializer for an UpdateChange with type `.parameterChange` - init( - element: ChangeElement, - from: ChangeValue, - to: ChangeValue, - targetID: DeltaIdentifier, - necessityValue: ChangeValue? = nil, - convertFromTo: Int? = nil, - convertionWarning: String? = nil, - parameterTarget: ParameterChangeTarget, - breaking: Bool, - solvable: Bool - ) { - self.element = element - self.from = from - self.to = to - self.similarity = nil - self.targetID = targetID - self.necessityValue = necessityValue - self.convertFromTo = convertFromTo - self.convertToFrom = nil - self.convertionWarning = convertionWarning - self.parameterTarget = parameterTarget - self.breaking = breaking - self.solvable = solvable - self.providerSupport = nil - type = .parameterChange - } -} diff --git a/Sources/ApodiniMigratorCompare/Change/ProviderSupport.swift b/Sources/ApodiniMigratorCompare/Change/ProviderSupport.swift deleted file mode 100644 index 2aac10db..00000000 --- a/Sources/ApodiniMigratorCompare/Change/ProviderSupport.swift +++ /dev/null @@ -1,98 +0,0 @@ -// -// This source file is part of the Apodini open source project -// -// SPDX-FileCopyrightText: 2019-2021 Paul Schmiedmayer and the Apodini project authors (see CONTRIBUTORS.md) -// -// SPDX-License-Identifier: MIT -// - -import Foundation - -/// A util object to correct wrong classification of changes in the migration guide. -/// `ProviderSupport` is included as a field in changes of type addition, deletion or rename -public struct ProviderSupport: Value { - // MARK: Private Inner Types - private enum CodingKeys: String, CodingKey { - case hint, renamedFrom = "renamed-from", renamedTo = "renamed-to", renameIsValid = "rename-is-valid", warning - } - - /// Textual explanation how to adjust the change object - var hint: String - /// Element id if an addition change should instead be trated as a rename. - /// Property can be adjusted from the provider - var renamedFrom: ChangeValue? - /// Element id if a deletion change should instead be trated as a rename - /// Property can be adjusted from the provider - var renamedTo: ChangeValue? - /// Flag to indicate whether a rename change was identified correctly - /// Property can be adjusted from the provider - var renameIsValid: Bool? - /// Warning from `ApodiniCompare` - var warning: String - - /// Private initializer for a new `ProviderSupport` instance - private init( - hint: String = "", - renamedFrom: ChangeValue? = nil, - renamedTo: ChangeValue? = nil, - renameIsValid: Bool? = nil, - warning: String = "" - ) { - self.hint = hint - self.renamedFrom = renamedFrom - self.renamedTo = renamedTo - self.renameIsValid = renameIsValid - self.warning = warning - } - - /// Creates a new instance by decoding from the given decoder. - /// Since the property will be adjusted from the provider, and the adjustment might result in - /// a non-valid json, the initializer first initializes the fields with empty values, and then - /// try decode each of them. If some field has been malformed, the default empty values will be used - public init(from decoder: Decoder) throws { - self.init() - - do { - let container = try decoder.container(keyedBy: CodingKeys.self) - hint = try container.decode(String.self, forKey: .hint) - renamedFrom = try container.decodeIfPresent(ChangeValue.self, forKey: .renamedFrom) - renamedTo = try container.decodeIfPresent(ChangeValue.self, forKey: .renamedTo) - renameIsValid = try container.decodeIfPresent(Bool.self, forKey: .renameIsValid) - warning = try container.decode(String.self, forKey: .warning) - } catch { - return - } - } -} - -// MARK: - ProviderSupport -extension ProviderSupport { - // swiftlint:disable line_length - /// Rename hint for either a Delete or AddChange. - static func renameHint(_ type: C.Type) -> ProviderSupport { - assert(C.self == AddChange.self || C.self == DeleteChange.self, "Attempted to use rename hint for change types that are not addition or deletions") - let isAddition = C.self == AddChange.self - return .init( - hint: "If 'ApodiniCompare' categorized this change incorrectly, replace the value \(ChangeValue.idPlaceholder.value ?? "") of the field \(ChangeValue.CodingKeys.elementID.stringValue) with the corresponding name or id of the element in the '\(isAddition ? "old" : "new")' version, so that 'ApodiniMigrator' traits this change element as a \(ChangeType.rename.rawValue). Note that an adjustment to this change object, requires a corresponding adjustment to the \(isAddition ? "deletion" : "addition") change targeting the same element", - renamedFrom: isAddition ? .idPlaceholder : nil, - renamedTo: isAddition ? nil : .idPlaceholder, - warning: "The field \(ChangeValue.CodingKeys.elementID.stringValue) expects a value of JSON type 'string'. Wrong input might invalidate the provider support or even the entire Migration Guide!" - ) - } - - /// Hint to validate a rename change - static var renameValidationHint: ProviderSupport { - .init( - hint: "If 'ApodiniCompare' categorized this change incorrectly, replace the value \(true) of the field \(CodingKeys.renameIsValid.rawValue) with \(false), so that 'ApodiniMigrator' does not trait this change element as a \(ChangeType.rename.rawValue). If set to \(false), the element in '\(UpdateChange.CodingKeys.from.rawValue)' will be trated as a deletion, and the element in '\(UpdateChange.CodingKeys.to.rawValue)' will be trated as an addition", - renameIsValid: true, - warning: "The field \(CodingKeys.renameIsValid.rawValue) expects a value of JSON type 'boolean'. Wrong input might invalidate the provider support or even the entire Migration Guide!" - ) - } - // swiftlint:enable line_length -} - -extension ChangeValue { - static var idPlaceholder: ChangeValue { - .elementID("_") - } -} diff --git a/Sources/ApodiniMigratorCompare/ChangeModel/AnyChange.swift b/Sources/ApodiniMigratorCompare/ChangeModel/AnyChange.swift new file mode 100644 index 00000000..741ab55d --- /dev/null +++ b/Sources/ApodiniMigratorCompare/ChangeModel/AnyChange.swift @@ -0,0 +1,43 @@ +// +// This source file is part of the Apodini open source project +// +// SPDX-FileCopyrightText: 2019-2021 Paul Schmiedmayer and the Apodini project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + +import Foundation + +/// Represents any change. Protocol for ``Change``. +public protocol AnyChange { + /// The type of element this change is about. + associatedtype Element: ChangeableElement + + /// The ``DeltaIdentifier`` of the instance of the changed element. + var id: DeltaIdentifier { get } + /// The ``ChangeType``. This maps to cases in the ``Change`` type. + var type: ChangeType { get } + + /// Breaking classification of the change. + var breaking: Bool { get } + /// Solvable (by the Migrator) classification of the change. + /// This classification might be inaccurate, as the classification is heavily dependent on the api type. + var solvable: Bool { get } +} + +fileprivate extension AnyChange { + func typed() -> Change { + guard let change = self as? Change else { + fatalError("Encountered `AnyChange` which isn't of expected type `ChangeEnum`!") + } + return change + } +} + +public extension Array where Element: AnyChange { + /// Retrieves all changes from an array of ``Change``es for a given instance. + /// - Parameter element: The instance to filter for changes. + func of(base element: Element.Element) -> [Element] { + self.filter { $0.id == element.deltaIdentifier } + } +} diff --git a/Sources/ApodiniMigratorCompare/ChangeModel/Change+Models.swift b/Sources/ApodiniMigratorCompare/ChangeModel/Change+Models.swift new file mode 100644 index 00000000..0ecb5bda --- /dev/null +++ b/Sources/ApodiniMigratorCompare/ChangeModel/Change+Models.swift @@ -0,0 +1,138 @@ +// +// This source file is part of the Apodini open source project +// +// SPDX-FileCopyrightText: 2019-2021 Paul Schmiedmayer and the Apodini project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + +import Foundation + + +// MARK: IdChange +public extension Change { + /// A simple data structure/model to represent the `idChange` case of a ``Change``. + /// This can be used to more easily pass changes around without the constant need to unwrap the enum cases. + struct IdentifierChange { + /// The previous identifier value. + public let from: DeltaIdentifier + /// The updated identifier value. + public let to: DeltaIdentifier + /// The similarity score [0-1] of the identifiers. + public let similarity: Double? + /// Breaking classification. + public let breaking: Bool + /// Solvable classification. + public let solvable: Bool + } + + /// A ``IdentifierChange`` model instance of the self ``Change``. + /// Returns nil if the change is not a `.idChange` case. + var modeledIdentifierChange: IdentifierChange? { + guard case let .idChange(from, to, similarity, breaking, solvable) = self else { + return nil + } + return IdentifierChange(from: from, to: to, similarity: similarity, breaking: breaking, solvable: solvable) + } + + /// Initialize a ``Change`` enum case from a ``IdentifierChange``. + init(from model: IdentifierChange) { + self = .idChange(from: model.from, to: model.to, similarity: model.similarity, breaking: model.breaking, solvable: model.solvable) + } +} + +// MARK: AdditionChange +public extension Change { + /// A simple data structure/model to represent the `addition` case of a ``Change``. + /// This can be used to more easily pass changes around without the constant need to unwrap the enum cases. + struct AdditionChange { + /// The identifier of the newly added `Element` + public let id: DeltaIdentifier + /// The added `Element`. + public let added: Element + /// Optionally and only if applicable, an int identifier to the json representation of a default value. + public let defaultValue: Int? + /// Breaking classification. + public let breaking: Bool + /// Solvable classification. + public let solvable: Bool + } + + /// A ``AdditionChange`` model instance of the self ``Change``. + /// Returns nil if the change is not a `.addition` case. + var modeledAdditionChange: AdditionChange? { + guard case let .addition(id, added, defaultValue, breaking, solvable) = self else { + return nil + } + return AdditionChange(id: id, added: added, defaultValue: defaultValue, breaking: breaking, solvable: solvable) + } + + /// Initialize a ``Change`` enum case from a ``AdditionChange``. + init(from model: AdditionChange) { + self = .addition(id: model.id, added: model.added, defaultValue: model.defaultValue, breaking: model.breaking, solvable: model.solvable) + } +} + +// MARK: RemovalChange +public extension Change { + /// A simple data structure/model to represent the `removal` case of a ``Change``. + /// This can be used to more easily pass changes around without the constant need to unwrap the enum cases. + struct RemovalChange { + /// The identifier of the removed `Element`. + public let id: DeltaIdentifier + /// Optionally, the description of the removed `Element`. + /// Typically, this is not included as the element is part of the base `APIDocument`. + public let removed: Element? + /// Optionally and only if applicable, an int identifier to the json representation of a fallback value. + public let fallbackValue: Int? + /// Breaking classification. + public let breaking: Bool + /// Solvable classification. + public let solvable: Bool + } + + /// A ``RemovalChange`` model instance of the self ``Change``. + /// Returns nil if the change is not a `.removal` case. + var modeledRemovalChange: RemovalChange? { + guard case let .removal(id, removed, fallbackValue, breaking, solvable) = self else { + return nil + } + return RemovalChange(id: id, removed: removed, fallbackValue: fallbackValue, breaking: breaking, solvable: solvable) + } + + /// Initialize a ``Change`` enum case from a ``RemovalChange``. + init(from model: RemovalChange) { + self = .removal(id: model.id, removed: model.removed, fallbackValue: model.fallbackValue, breaking: model.breaking, solvable: model.solvable) + } +} + +// MARK: UpdateChange +public extension Change { + /// A simple data structure/model to represent the `update` case of a ``Change``. + /// This can be used to more easily pass changes around without the constant need to unwrap the enum cases. + struct UpdateChange { + /// The identifier of the updated `Element`. + public let id: DeltaIdentifier + /// A structure which describes the update. How the update is structured is entirely defined + /// by the `Element` itself. It may contain nested ``Changes``. + public let updated: Element.Update + /// Breaking classification. + public let breaking: Bool + /// Solvable classification. + public let solvable: Bool + } + + /// A ``UpdateChange`` model instance of the self ``Change``. + /// Returns nil if the change is not a `.update` case. + var modeledUpdateChange: UpdateChange? { + guard case let .update(id, updated, breaking, solvable) = self else { + return nil + } + return UpdateChange(id: id, updated: updated, breaking: breaking, solvable: solvable) + } + + /// Initialize a ``Change`` enum case from a ``UpdateChange``. + init(from model: UpdateChange) { + self = .update(id: model.id, updated: model.updated, breaking: model.breaking, solvable: model.solvable) + } +} diff --git a/Sources/ApodiniMigratorCompare/ChangeModel/Change.swift b/Sources/ApodiniMigratorCompare/ChangeModel/Change.swift new file mode 100644 index 00000000..1c58cb9e --- /dev/null +++ b/Sources/ApodiniMigratorCompare/ChangeModel/Change.swift @@ -0,0 +1,236 @@ +// +// This source file is part of the Apodini open source project +// +// SPDX-FileCopyrightText: 2019-2021 Paul Schmiedmayer and the Apodini project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + +import Foundation + +/// Typed version of an ``AnyChange``. +public enum Change: AnyChange, Equatable { + /// This case represents a change of the primary identifier (the ``DeltaIdentifier``) of the `Element`. + /// - Parameters: + /// - from: The previous identifier value. + /// - to: The updated identifier value. + /// - similarity: The similarity score [0-1] of the identifiers. + /// - breaking: Breaking classification. + /// - solvable: Solvable classification. + case idChange( + from: DeltaIdentifier, + to: DeltaIdentifier, + similarity: Double?, + breaking: Bool = false, + solvable: Bool = true + ) + + /// Describes a change where a new instance of an `Element` was added. + /// - Parameters: + /// - id: The identifier of the newly added `Element`. + /// - added: The added `Element`. + /// - defaultValue: Optionally and only if applicable, an int identifier to the json representation of a default value. + /// - breaking: Breaking classification. + /// - solvable: Solvable classification. + case addition( + id: DeltaIdentifier, + added: Element, + defaultValue: Int? = nil, + breaking: Bool = false, + solvable: Bool = true + ) + + /// Describes a change where a instance of an `Element` was completely removed. + /// - Parameters: + /// - id: The identifier of the removed `Element`. + /// - removed: Optionally, the description of the removed `Element`. + /// Typically, this is not included as the element is part of the base `APIDocument`. + /// - fallbackValue: Optionally and only if applicable, an int identifier to the json representation of a fallback value. + /// - breaking: Breaking classification. + /// - solvable: Solvable classification. + case removal( + id: DeltaIdentifier, + removed: Element? = nil, + fallbackValue: Int? = nil, + breaking: Bool = true, + solvable: Bool = false + ) + + /// Describes some sort of update to an existing instance of an `Element`. + /// - Parameters: + /// - id: The identifier of the updated `Element`. + /// - updated: A structure which describes the update. How the update is structured is entirely defined + /// by the `Element` itself. It may contain nested ``Changes``. + /// - breaking: Breaking classification. + /// - solvable: Solvable classification. + case update( + id: DeltaIdentifier, + updated: Element.Update, + breaking: Bool = true, + solvable: Bool = true + ) + + /// The identifier of the `Element` this change is about. + /// - Note: In the case of an `idChange` this property returns the "base" ``DeltaIdentifier``. + public var id: DeltaIdentifier { + switch self { + case let .idChange(from, _, _, _, _): + return from + case let .addition(id, _, _, _, _): + return id + case let .removal(id, _, _, _, _): + return id + case let .update(id, _, _, _): + return id + } + } + + /// The breaking classification of the change. + public var breaking: Bool { + switch self { + case let .idChange(_, _, _, breaking, _): + return breaking + case let .addition(_, _, _, breaking, _): + return breaking + case let .removal(_, _, _, breaking, _): + return breaking + case let .update(_, _, breaking, _): + return breaking + } + } + + /// The solvable classification of the change. + public var solvable: Bool { + switch self { + case let .idChange(_, _, _, _, solvable): + return solvable + case let .addition(_, _, _, _, solvable): + return solvable + case let .removal(_, _, _, _, solvable): + return solvable + case let .update(_, _, _, solvable): + return solvable + } + } +} + +// MARK: Codable +extension Change: Codable { + private enum CodingKeys: String, CodingKey { + case type + + case id + case breaking + case solvable + + case from + case to + case similarity + + case added + case defaultValue + + case removed + case fallbackValue + + case updated + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + let type = try container.decode(ChangeType.self, forKey: .type) + + switch type { + case .idChange: + self = .idChange( + from: try container.decode(DeltaIdentifier.self, forKey: .from), + to: try container.decode(DeltaIdentifier.self, forKey: .to), + similarity: try container.decodeIfPresent(Double.self, forKey: .similarity), + breaking: try container.decode(Bool.self, forKey: .breaking), + solvable: try container.decode(Bool.self, forKey: .solvable) + ) + case .addition: + self = .addition( + id: try container.decode(DeltaIdentifier.self, forKey: .id), + added: try container.decode(Element.self, forKey: .added), + defaultValue: try container.decodeIfPresent(Int.self, forKey: .defaultValue), + breaking: try container.decode(Bool.self, forKey: .breaking), + solvable: try container.decode(Bool.self, forKey: .solvable) + ) + case .removal: + self = .removal( + id: try container.decode(DeltaIdentifier.self, forKey: .id), + removed: try container.decodeIfPresent(Element.self, forKey: .removed), + fallbackValue: try container.decodeIfPresent(Int.self, forKey: .fallbackValue), + breaking: try container.decode(Bool.self, forKey: .breaking), + solvable: try container.decode(Bool.self, forKey: .solvable) + ) + case .update: + let id = try container.decode(DeltaIdentifier.self, forKey: .id) + let updated = try container.decode(Element.Update.self, forKey: .updated) + let breaking: Bool + let solvable: Bool + + if let nestedChange = updated as? UpdateChangeWithNestedChange, + let nestedBreaking = nestedChange.nestedBreakingClassification, + let nestedSolvable = nestedChange.nestedSolvableClassification { + breaking = nestedBreaking + solvable = nestedSolvable + } else { + breaking = try container.decode(Bool.self, forKey: .breaking) + solvable = try container.decode(Bool.self, forKey: .solvable) + } + + self = .update( + id: id, + updated: updated, + breaking: breaking, + solvable: solvable + ) + } + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + + switch self { + case let .idChange(from, to, similarity, breaking, solvable): + try container.encode(ChangeType.idChange, forKey: .type) + + try container.encode(from, forKey: .from) + try container.encode(to, forKey: .to) + try container.encodeIfPresent(similarity, forKey: .similarity) + try container.encode(breaking, forKey: .breaking) + try container.encode(solvable, forKey: .solvable) + case let .addition(id, added, defaultValue, breaking, solvable): + try container.encode(ChangeType.addition, forKey: .type) + + try container.encode(id, forKey: .id) + try container.encode(added, forKey: .added) + try container.encodeIfPresent(defaultValue, forKey: .defaultValue) + try container.encode(breaking, forKey: .breaking) + try container.encode(solvable, forKey: .solvable) + case let .removal(id, removed, fallbackValue, breaking, solvable): + try container.encode(ChangeType.removal, forKey: .type) + + try container.encode(id, forKey: .id) + try container.encodeIfPresent(removed, forKey: .removed) + try container.encodeIfPresent(fallbackValue, forKey: .fallbackValue) + try container.encode(breaking, forKey: .breaking) + try container.encode(solvable, forKey: .solvable) + case let .update(id, updated, breaking, solvable): + try container.encode(ChangeType.update, forKey: .type) + + try container.encode(id, forKey: .id) + try container.encode(updated, forKey: .updated) + if let nestedChange = updated as? UpdateChangeWithNestedChange, + nestedChange.isNestedChange { + // do nothing + } else { + try container.encode(breaking, forKey: .breaking) + try container.encode(solvable, forKey: .solvable) + } + } + } +} diff --git a/Sources/ApodiniMigratorCompare/ChangeModel/ChangeComparisonContext.swift b/Sources/ApodiniMigratorCompare/ChangeModel/ChangeComparisonContext.swift new file mode 100644 index 00000000..9ece02ca --- /dev/null +++ b/Sources/ApodiniMigratorCompare/ChangeModel/ChangeComparisonContext.swift @@ -0,0 +1,120 @@ +// +// This source file is part of the Apodini open source project +// +// SPDX-FileCopyrightText: 2019-2021 Paul Schmiedmayer and the Apodini project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + +import Foundation + +/// The `ChangeComparisonContext` tracks changes within comparison operations. +/// The entry point for `APIDocument` comparison is the ``DocumentComparator``. +public final class ChangeComparisonContext { + /// The configuration used when comparing two `APIDocument`s. + public let configuration: CompareConfiguration + /// This array contains all model definition of the update API document. + private let latestModels: [TypeInformation] + + /// All javascript convert methods created during comparison + public var scripts: [Int: JSScript] = [:] + /// All json values of properties or parameter that require a default or fallback value + public var jsonValues: [Int: JSONValue] = [:] + /// All json representations of objects that had some kind of breaking change in their properties + public private(set) var objectJSONs: [String: JSONValue] = [:] + + /// Stores all collected ``ServiceInformationChange``s. + public var serviceChanges: [ServiceInformationChange] = [] + /// Stores all collected ``ModelChange``s. + public var modelChanges: [ModelChange] = [] + /// Stores all collected ``EndpointChange``s. + public var endpointChanges: [EndpointChange] = [] + + init(configuration: CompareConfiguration? = nil, latestModels: [TypeInformation] = []) { + self.configuration = configuration ?? .default + self.latestModels = latestModels + } + + + /// Stores the script and returns its stored index + func store(script: JSScript) -> Int { + let count = scripts.count + scripts[count] = script + return count + } + + /// Stores the jsonValue and returns stored index + func store(jsonValue: JSONValue) -> Int { + let count = jsonValues.count + jsonValues[count] = jsonValue + return count + } +} + +// MARK: JSScript Support +extension ChangeComparisonContext { + func currentVersion(of lhs: TypeInformation) -> TypeInformation { + switch lhs { + case .scalar: + return lhs + case let .repeated(element): + return .repeated(element: currentVersion(of: element)) + case let .dictionary(key, value): + return .dictionary(key: key, value: currentVersion(of: value)) + case let .optional(wrappedValue): + return .optional(wrappedValue: wrappedValue) + case .enum, .object: + return latestModels.first(where: { $0.deltaIdentifier == lhs.deltaIdentifier }) + ?? lhs + case .reference: + fatalError("Encountered a reference in `\(Self.self)`") + } + } + + func isPairOfRenamedTypes(lhs: TypeInformation, rhs: TypeInformation) -> Bool { + if !configuration.allowTypeRename { + return false + } + + return modelChanges.contains(where: { change in + if case let .idChange(from, to, _, _, _) = change { + return from == lhs.deltaIdentifier && to == rhs.deltaIdentifier + } + return false + }) + } + + /// For every compare between two models of different versions, this function is called to register potentially updated json representation of an object + func store(rhs: TypeInformation, into modelChanges: inout [ModelChange]) { + // if in natural language: if the list of model changes contains + // an property change of an object where the identifier matches with "rhs" and the change is breaking + if modelChanges.contains(where: { change in + if case let .update(id, update, breaking, _) = change, + breaking, + id == rhs.deltaIdentifier, + case .property = update { + return true + } + return false + }) { + objectJSONs[rhs.typeName.rawValue] = .init(JSONStringBuilder.jsonString(rhs, with: configuration.encoderConfiguration)) + } + } +} + +extension ChangeComparisonContext: CustomDebugStringConvertible { + public var debugDescription: String { + """ + ChangeComparisonContext(\ + configuration: \(configuration), \ + latestModels: \(latestModels), \ + scripts: \(scripts), \ + jsonValues: \(jsonValues), \ + objectJSONs: \(objectJSONs), \ + serviceChanges: \(serviceChanges), \ + modelChanges: \(modelChanges), \ + endpointChanges: \(endpointChanges)\ + ) + """ + } +} diff --git a/Sources/ApodiniMigratorCompare/ChangeModel/ChangeType.swift b/Sources/ApodiniMigratorCompare/ChangeModel/ChangeType.swift new file mode 100644 index 00000000..b6f51bbf --- /dev/null +++ b/Sources/ApodiniMigratorCompare/ChangeModel/ChangeType.swift @@ -0,0 +1,34 @@ +// +// This source file is part of the Apodini open source project +// +// SPDX-FileCopyrightText: 2019-2021 Paul Schmiedmayer and the Apodini project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + +import Foundation + +/// One to one mapping of enum cases of the ``Change`` type. +public enum ChangeType: String, Codable { + case idChange + case addition + case removal + case update +} + +// MARK: ChangeType +extension Change { + /// Retrieve the type of change. + public var type: ChangeType { + switch self { + case .idChange: + return .idChange + case .addition: + return .addition + case .removal: + return .removal + case .update: + return .update + } + } +} diff --git a/Sources/ApodiniMigratorCompare/ChangeModel/ChangeableElement.swift b/Sources/ApodiniMigratorCompare/ChangeModel/ChangeableElement.swift new file mode 100644 index 00000000..a16b6a1f --- /dev/null +++ b/Sources/ApodiniMigratorCompare/ChangeModel/ChangeableElement.swift @@ -0,0 +1,15 @@ +// +// This source file is part of the Apodini open source project +// +// SPDX-FileCopyrightText: 2019-2021 Paul Schmiedmayer and the Apodini project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + +import Foundation + +/// Defines a element which changes can be described via the ApodiniMigrator ``Change`` model. +public protocol ChangeableElement: DeltaIdentifiable, Equatable, Codable { + /// Represents the type how an update change is represented. + associatedtype Update: Codable, Equatable +} diff --git a/Sources/ApodiniMigratorCompare/ChangeModel/Changes/EndpointChange.swift b/Sources/ApodiniMigratorCompare/ChangeModel/Changes/EndpointChange.swift new file mode 100644 index 00000000..71a4de32 --- /dev/null +++ b/Sources/ApodiniMigratorCompare/ChangeModel/Changes/EndpointChange.swift @@ -0,0 +1,159 @@ +// This source file is part of the Apodini open source project +// +// SPDX-FileCopyrightText: 2019-2021 Paul Schmiedmayer and the Apodini project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + +import Foundation + +/// ``Change`` type which is related to an `Endpoint`. +/// `.update` changes are encoded as ``EndpointUpdateChange``. +public typealias EndpointChange = Change + +extension Endpoint: ChangeableElement { + public typealias Update = EndpointUpdateChange +} + +public enum EndpointUpdateChange: Equatable { + /// Describes an update change related to `EndpointIdentifier`s (e.g. Operation, Path or HandlerName). + /// - Parameters: + /// - identifier: The ``EndpointIdentifierChange``. + case identifier(identifier: EndpointIdentifierChange) + + /// Describes an update to the `CommunicationalPattern` of the `Endpoint`. + case communicationalPattern( + from: CommunicationalPattern, + to: CommunicationalPattern + ) + + /// Describes an update to the response type of the `Endpoint`. + /// - Parameters: + /// - from: The original `TypeInformation`. + /// - to: The updated `TypeInformation`. + /// - backwardsMigration: An integer identifier to a json script which provides backwards migration between those types. + /// - migrationWarning: An optional textual warning for the migration. + /// - Note: The TypeInformation are either some sort of `.reference` type (e.g. also repeated types) or a `.scalar`. + case response( + from: TypeInformation, + to: TypeInformation, + backwardsMigration: Int, + migrationWarning: String? = nil + ) + + /// Describes an update to a parameter of the `Endpoint`. + case parameter( + parameter: ParameterChange + ) +} + +extension EndpointUpdateChange: UpdateChangeWithNestedChange { + public var isNestedChange: Bool { + if case .parameter = self { + return true + } + return false + } + + public var nestedBreakingClassification: Bool? { // swiftlint:disable:this discouraged_optional_boolean + switch self { + case let .parameter(parameter): + return parameter.breaking + default: + return nil + } + } + + public var nestedSolvableClassification: Bool? { // swiftlint:disable:this discouraged_optional_boolean + switch self { + case let .parameter(parameter): + return parameter.solvable + default: + return nil + } + } +} + +extension EndpointUpdateChange: Codable { + private enum UpdateType: String, Codable { + case identifier + case communicationalPattern + case response + case parameter + } + + private enum CodingKeys: String, CodingKey { + case type + + case identifier + + case from + case to + + case backwardsMigration + case migrationWarning + + case parameter + } + + private var type: UpdateType { + switch self { + case .identifier: + return .identifier + case .communicationalPattern: + return .communicationalPattern + case .response: + return .response + case .parameter: + return .parameter + } + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + let type = try container.decode(UpdateType.self, forKey: .type) + switch type { + case .identifier: + self = .identifier( + identifier: try container.decode(EndpointIdentifierChange.self, forKey: .identifier) + ) + case .communicationalPattern: + self = .communicationalPattern( + from: try container.decode(CommunicationalPattern.self, forKey: .from), + to: try container.decode(CommunicationalPattern.self, forKey: .to) + ) + case .response: + self = .response( + from: try container.decode(TypeInformation.self, forKey: .from), + to: try container.decode(TypeInformation.self, forKey: .to), + backwardsMigration: try container.decode(Int.self, forKey: .backwardsMigration), + migrationWarning: try container.decodeIfPresent(String.self, forKey: .migrationWarning) + ) + case .parameter: + self = .parameter( + parameter: try container.decode(ParameterChange.self, forKey: .parameter) + ) + } + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + + try container.encode(type, forKey: .type) + switch self { + case let .identifier(identifier): + try container.encode(identifier, forKey: .identifier) + case let .communicationalPattern(from, to): + try container.encode(from, forKey: .from) + try container.encode(to, forKey: .to) + case let .response(from, to, backwardsMigration, migrationWarning): + try container.encode(from, forKey: .from) + try container.encode(to, forKey: .to) + try container.encode(backwardsMigration, forKey: .backwardsMigration) + try container.encodeIfPresent(migrationWarning, forKey: .migrationWarning) + case let .parameter(parameter): + try container.encode(parameter, forKey: .parameter) + } + } +} diff --git a/Sources/ApodiniMigratorCompare/ChangeModel/Changes/EndpointIdentifierUpdate.swift b/Sources/ApodiniMigratorCompare/ChangeModel/Changes/EndpointIdentifierUpdate.swift new file mode 100644 index 00000000..5e971eb7 --- /dev/null +++ b/Sources/ApodiniMigratorCompare/ChangeModel/Changes/EndpointIdentifierUpdate.swift @@ -0,0 +1,22 @@ +// +// This source file is part of the Apodini open source project +// +// SPDX-FileCopyrightText: 2019-2021 Paul Schmiedmayer and the Apodini project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + +import Foundation + +/// ``Change`` type which is related to an `EndpointIdentifier`. +/// `.update` changes are encoded as `EndpointIdentifierUpdateChange`. +public typealias EndpointIdentifierChange = Change + +extension AnyEndpointIdentifier: ChangeableElement { + public typealias Update = EndpointIdentifierUpdateChange +} + +public struct EndpointIdentifierUpdateChange: Codable, Equatable { + public let from: AnyEndpointIdentifier + public let to: AnyEndpointIdentifier +} diff --git a/Sources/ApodiniMigratorCompare/ChangeModel/Changes/EnumCaseChange.swift b/Sources/ApodiniMigratorCompare/ChangeModel/Changes/EnumCaseChange.swift new file mode 100644 index 00000000..3890732c --- /dev/null +++ b/Sources/ApodiniMigratorCompare/ChangeModel/Changes/EnumCaseChange.swift @@ -0,0 +1,65 @@ +// +// This source file is part of the Apodini open source project +// +// SPDX-FileCopyrightText: 2019-2021 Paul Schmiedmayer and the Apodini project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + +import Foundation + +/// ``Change`` type which is related to an `EnumCase`. +/// `.update` changes are encoded as ``EnumCaseUpdateChange``. +public typealias EnumCaseChange = Change + +extension EnumCase: ChangeableElement { + public typealias Update = EnumCaseUpdateChange +} + +public enum EnumCaseUpdateChange: Equatable { + /// Describes an update of the raw **value** + case rawValue( + from: String, + to: String + ) +} + +extension EnumCaseUpdateChange: Codable { + private enum UpdateType: String, Codable { + case rawValue + } + + private enum CodingKeys: String, CodingKey { + case type + case from + case to + } + + private var type: UpdateType { + .rawValue + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + let type = try container.decode(UpdateType.self, forKey: .type) + switch type { + case .rawValue: + self = .rawValue( + from: try container.decode(String.self, forKey: .from), + to: try container.decode(String.self, forKey: .to) + ) + } + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + + try container.encode(type, forKey: .type) + switch self { + case let .rawValue(from, to): + try container.encode(from, forKey: .from) + try container.encode(to, forKey: .to) + } + } +} diff --git a/Sources/ApodiniMigratorCompare/ChangeModel/Changes/ExporterConfigurationChange.swift b/Sources/ApodiniMigratorCompare/ChangeModel/Changes/ExporterConfigurationChange.swift new file mode 100644 index 00000000..62219c17 --- /dev/null +++ b/Sources/ApodiniMigratorCompare/ChangeModel/Changes/ExporterConfigurationChange.swift @@ -0,0 +1,22 @@ +// +// This source file is part of the Apodini open source project +// +// SPDX-FileCopyrightText: 2019-2021 Paul Schmiedmayer and the Apodini project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + +import Foundation + +/// ``Change`` type which is related to an `ExporterConfiguration`. +/// `.update` changes are encoded as ``ExporterConfigurationUpdateChange``. +public typealias ExporterConfigurationChange = Change + +extension AnyExporterConfiguration: ChangeableElement { + public typealias Update = ExporterConfigurationUpdateChange +} + +public struct ExporterConfigurationUpdateChange: Codable, Equatable { + public let from: AnyExporterConfiguration + public let to: AnyExporterConfiguration +} diff --git a/Sources/ApodiniMigratorCompare/ChangeModel/Changes/ModelChange.swift b/Sources/ApodiniMigratorCompare/ChangeModel/Changes/ModelChange.swift new file mode 100644 index 00000000..56573b57 --- /dev/null +++ b/Sources/ApodiniMigratorCompare/ChangeModel/Changes/ModelChange.swift @@ -0,0 +1,156 @@ +// +// This source file is part of the Apodini open source project +// +// SPDX-FileCopyrightText: 2019-2021 Paul Schmiedmayer and the Apodini project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + +import Foundation + +/// ``Change`` type which is related to a Model/`TypeInformation`. +/// `.update` changes are encoded as `ModelUpdateChange`. +/// The type of `TypeInformation` (e.g. enum or object) might be derived from the `APIDocument`. +public typealias ModelChange = Change + +extension TypeInformation: ChangeableElement { + public typealias Update = ModelUpdateChange +} + +public enum ModelUpdateChange: Equatable { + // common + /// Describes a change to the `RootType` of a `TypeInformation` (e.g. object to enum). + case rootType( + from: TypeInformation.RootType, + to: TypeInformation.RootType, + newModel: TypeInformation + ) + + // .object + /// Describes a change to the properties of a `.object`. + case property(property: PropertyChange) + + // .enum + /// Describes a change to the cases of an `.enum`. + case `case`(case: EnumCaseChange) + + /// Describes a change to the raw value **type** of an `.enum`. + /// - Note: This is either a reference or a scalar. + case rawValueType( + from: TypeInformation, + to: TypeInformation + ) +} + +extension ModelUpdateChange: UpdateChangeWithNestedChange { + public var isNestedChange: Bool { + switch self { + case .property, .case: + return true + default: + return false + } + } + + public var nestedBreakingClassification: Bool? { // swiftlint:disable:this discouraged_optional_boolean + switch self { + case let .property(property): + return property.breaking + case let .case(`case`): + return `case`.breaking + default: + return nil + } + } + + public var nestedSolvableClassification: Bool? { // swiftlint:disable:this discouraged_optional_boolean + switch self { + case let .property(property): + return property.solvable + case let .case(`case`): + return `case`.solvable + default: + return nil + } + } +} + +extension ModelUpdateChange: Codable { + private enum UpdateType: String, Codable { + case rootType + case property + case `case` + case rawValueType + } + + private enum CodingKeys: String, CodingKey { + case type + + case from + case to + case newModel + + case property + + case `case` + } + + private var type: UpdateType { + switch self { + case .rootType: + return .rootType + case .property: + return .property + case .case: + return .case + case .rawValueType: + return .rawValueType + } + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + let type = try container.decode(UpdateType.self, forKey: .type) + switch type { + case .rootType: + self = .rootType( + from: try container.decode(TypeInformation.RootType.self, forKey: .from), + to: try container.decode(TypeInformation.RootType.self, forKey: .to), + newModel: try container.decode(TypeInformation.self, forKey: .newModel) + ) + case .property: + self = .property( + property: try container.decode(PropertyChange.self, forKey: .property) + ) + case .case: + self = .case( + case: try container.decode(EnumCaseChange.self, forKey: .case) + ) + case .rawValueType: + self = .rawValueType( + from: try container.decode(TypeInformation.self, forKey: .from), + to: try container.decode(TypeInformation.self, forKey: .to) + ) + } + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + + try container.encode(type, forKey: .type) + switch self { + case let .rootType(from, to, newModel): + try container.encode(from, forKey: .from) + try container.encode(to, forKey: .to) + try container.encode(newModel, forKey: .newModel) + case let .property(property): + try container.encode(property, forKey: .property) + case let .case(`case`): + try container.encode(`case`, forKey: .case) + case let .rawValueType(from, to): + try container.encode(from, forKey: .from) + try container.encode(to, forKey: .to) + } + } +} diff --git a/Sources/ApodiniMigratorCompare/ChangeModel/Changes/ParameterChange.swift b/Sources/ApodiniMigratorCompare/ChangeModel/Changes/ParameterChange.swift new file mode 100644 index 00000000..25340d16 --- /dev/null +++ b/Sources/ApodiniMigratorCompare/ChangeModel/Changes/ParameterChange.swift @@ -0,0 +1,118 @@ +// +// This source file is part of the Apodini open source project +// +// SPDX-FileCopyrightText: 2019-2021 Paul Schmiedmayer and the Apodini project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + +import Foundation + +/// ``Change`` type which is related to a `Parameter`. +/// `.update` changes are encoded as ``ParameterUpdateChange``. +public typealias ParameterChange = Change + +extension Parameter: ChangeableElement { + public typealias Update = ParameterUpdateChange +} + +public enum ParameterUpdateChange: Equatable { + /// Describes an update of the `ParameterType` + case parameterType( + from: ParameterType, + to: ParameterType + ) + + /// Describes an update of the parameter `Necessity`. + case necessity( + from: Necessity, + to: Necessity, + necessityMigration: Int? + ) + + /// Describes an update of the parameter type. + case type( + from: TypeInformation, + to: TypeInformation, + forwardMigration: Int, + conversionWarning: String? + ) +} + +extension ParameterUpdateChange: Codable { + private enum UpdateType: String, Codable { + case parameterType + case necessity + case type + } + + private enum CodingKeys: String, CodingKey { + case type + + case from + case to + + case necessityMigration + + case forwardMigration + case conversionWarning + } + + private var type: UpdateType { + switch self { + case .parameterType: + return .parameterType + case .necessity: + return .necessity + case .type: + return .type + } + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + let type = try container.decode(UpdateType.self, forKey: .type) + switch type { + case .parameterType: + self = .parameterType( + from: try container.decode(ParameterType.self, forKey: .from), + to: try container.decode(ParameterType.self, forKey: .to) + ) + case .necessity: + self = .necessity( + from: try container.decode(Necessity.self, forKey: .from), + to: try container.decode(Necessity.self, forKey: .to), + necessityMigration: try container.decodeIfPresent(Int.self, forKey: .necessityMigration) + ) + case .type: + self = .type( + from: try container.decode(TypeInformation.self, forKey: .from), + to: try container.decode(TypeInformation.self, forKey: .to), + forwardMigration: try container.decode(Int.self, forKey: .forwardMigration), + conversionWarning: try container.decodeIfPresent(String.self, forKey: .conversionWarning) + ) + } + } + + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + + try container.encode(type, forKey: .type) + switch self { + case let .parameterType(from, to): + try container.encode(from, forKey: .from) + try container.encode(to, forKey: .to) + case let .necessity(from, to, necessityMigration): + try container.encode(from, forKey: .from) + try container.encode(to, forKey: .to) + try container.encodeIfPresent(necessityMigration, forKey: .necessityMigration) + case let .type(from, to, forwardMigration, conversionWarning): + try container.encode(from, forKey: .from) + try container.encode(to, forKey: .to) + try container.encode(forwardMigration, forKey: .forwardMigration) + try container.encodeIfPresent(conversionWarning, forKey: .conversionWarning) + } + } +} diff --git a/Sources/ApodiniMigratorCompare/ChangeModel/Changes/PropertyChange.swift b/Sources/ApodiniMigratorCompare/ChangeModel/Changes/PropertyChange.swift new file mode 100644 index 00000000..6f15f3e8 --- /dev/null +++ b/Sources/ApodiniMigratorCompare/ChangeModel/Changes/PropertyChange.swift @@ -0,0 +1,103 @@ +// +// This source file is part of the Apodini open source project +// +// SPDX-FileCopyrightText: 2019-2021 Paul Schmiedmayer and the Apodini project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + +import Foundation + +/// ``Change`` type which is related to a `TypeProperty`. +/// `.update` changes are encoded as ``PropertyUpdateChange``. +public typealias PropertyChange = Change + +extension TypeProperty: ChangeableElement { + public typealias Update = PropertyUpdateChange +} + +public enum PropertyUpdateChange: Equatable { + /// Describes an update to the property necessity. + case necessity( + from: Necessity, + to: Necessity, + necessityMigration: Int + ) + + /// Describes an update to the property type. + case type( + from: TypeInformation, + to: TypeInformation, + forwardMigration: Int, + backwardMigration: Int, + conversionWarning: String? + ) +} + +extension PropertyUpdateChange: Codable { + private enum UpdateType: String, Codable { + case necessity + case type + } + + private enum CodingKeys: String, CodingKey { + case type + + case from + case to + case necessityMigration + + case forwardMigration + case backwardMigration + case conversionWarning + } + + private var type: UpdateType { + switch self { + case .necessity: + return .necessity + case .type: + return .type + } + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + let type = try container.decode(UpdateType.self, forKey: .type) + switch type { + case .necessity: + self = .necessity( + from: try container.decode(Necessity.self, forKey: .from), + to: try container.decode(Necessity.self, forKey: .to), + necessityMigration: try container.decode(Int.self, forKey: .necessityMigration) + ) + case .type: + self = .type( + from: try container.decode(TypeInformation.self, forKey: .from), + to: try container.decode(TypeInformation.self, forKey: .to), + forwardMigration: try container.decode(Int.self, forKey: .forwardMigration), + backwardMigration: try container.decode(Int.self, forKey: .backwardMigration), + conversionWarning: try container.decodeIfPresent(String.self, forKey: .conversionWarning) + ) + } + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + + try container.encode(type, forKey: .type) + switch self { + case let .necessity(from, to, necessityMigration): + try container.encode(from, forKey: .from) + try container.encode(to, forKey: .to) + try container.encode(necessityMigration, forKey: .necessityMigration) + case let .type(from, to, forwardMigration, backwardMigration, conversionWarning): + try container.encode(from, forKey: .from) + try container.encode(to, forKey: .to) + try container.encode(forwardMigration, forKey: .forwardMigration) + try container.encode(backwardMigration, forKey: .backwardMigration) + try container.encodeIfPresent(conversionWarning, forKey: .conversionWarning) + } + } +} diff --git a/Sources/ApodiniMigratorCompare/ChangeModel/Changes/ServiceInformationChange.swift b/Sources/ApodiniMigratorCompare/ChangeModel/Changes/ServiceInformationChange.swift new file mode 100644 index 00000000..363c1dd0 --- /dev/null +++ b/Sources/ApodiniMigratorCompare/ChangeModel/Changes/ServiceInformationChange.swift @@ -0,0 +1,101 @@ +// +// This source file is part of the Apodini open source project +// +// SPDX-FileCopyrightText: 2019-2021 Paul Schmiedmayer and the Apodini project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + +import Foundation + +/// ``Change`` type which is related to the `ServiceInformation` +/// `.update` changes are encoded as ``ServiceInformationUpdateChange``. +public typealias ServiceInformationChange = Change + +extension ServiceInformation: ChangeableElement { + public typealias Update = ServiceInformationUpdateChange +} + +public enum ServiceInformationUpdateChange: Equatable { + /// Defines an update to the service `Version`. + case version( + from: Version, + to: Version + ) + + /// Defines an update to the `HTTPInformation` of the service. + case http( + from: HTTPInformation, + to: HTTPInformation + ) + + /// Defines an update of the `ExporterConfiguration` of the service. + case exporter(exporter: ExporterConfigurationChange) +} + +// MARK: Codable +extension ServiceInformationUpdateChange: Codable { + private enum EnumType: String, Codable { + case version + case http + case exporter + } + + private var type: EnumType { + switch self { + case .version: + return .version + case .http: + return .http + case .exporter: + return .exporter + } + } + + private enum CodingKeys: String, CodingKey { + case type + + case from + case to + + case exporter + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + let type = try container.decode(EnumType.self, forKey: .type) + + switch type { + case .version: + self = .version( + from: try container.decode(Version.self, forKey: .from), + to: try container.decode(Version.self, forKey: .to) + ) + case .http: + self = .http( + from: try container.decode(HTTPInformation.self, forKey: .from), + to: try container.decode(HTTPInformation.self, forKey: .to) + ) + case .exporter: + self = .exporter(exporter: try container.decode(ExporterConfigurationChange.self, forKey: .exporter)) + } + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + + try container.encode(type, forKey: .type) + + switch self { + case let .version(from, to): + try container.encode(from, forKey: .from) + try container.encode(to, forKey: .to) + case let .http(from, to): + try container.encode(from, forKey: .from) + try container.encode(to, forKey: .to) + case let .exporter(exporter): + try container.encode(exporter, forKey: .exporter) + } + } +} diff --git a/Sources/ApodiniMigratorCompare/ChangeModel/UnsupportedChange.swift b/Sources/ApodiniMigratorCompare/ChangeModel/UnsupportedChange.swift new file mode 100644 index 00000000..33876aa7 --- /dev/null +++ b/Sources/ApodiniMigratorCompare/ChangeModel/UnsupportedChange.swift @@ -0,0 +1,25 @@ +// +// This source file is part of the Apodini open source project +// +// SPDX-FileCopyrightText: 2019-2021 Paul Schmiedmayer and the Apodini project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + +import Foundation + +/// This type can be used to mark a particular ``Change`` as unsupported by the `Migrator`. +public struct UnsupportedChange { + /// The change which is unsupported. + public let change: Change + /// A short textual description why this change is unsupported. + /// This description may be used to render the change to the user. + public let description: String +} + +public extension Change { + /// Classifies a given change instance as an ``UnsupportedChange``. + func classifyUnsupported(description: String) -> UnsupportedChange { + UnsupportedChange(change: self, description: description) + } +} diff --git a/Sources/ApodiniMigratorCompare/ChangeModel/UpdateChangeWithNestedChange.swift b/Sources/ApodiniMigratorCompare/ChangeModel/UpdateChangeWithNestedChange.swift new file mode 100644 index 00000000..f6228c17 --- /dev/null +++ b/Sources/ApodiniMigratorCompare/ChangeModel/UpdateChangeWithNestedChange.swift @@ -0,0 +1,22 @@ +// +// This source file is part of the Apodini open source project +// +// SPDX-FileCopyrightText: 2019-2021 Paul Schmiedmayer and the Apodini project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + +import Foundation + +/// An optional protocol a update change type of a ``ChangeableElement`` can implement +/// to signify that it may contain nested ``Change`` types. +/// This is useful to remove duplicated `breaking` and `solvable` classifications from encoding. +public protocol UpdateChangeWithNestedChange { + /// Defines if this instance is a nested ``Change``. + var isNestedChange: Bool { get } + + /// In the case of a nested ``Change``, it returns the `breaking` classification. + var nestedBreakingClassification: Bool? { get } // swiftlint:disable:this discouraged_optional_boolean + /// In the case of a nested ``Change``, it returns the `solvable` classification. + var nestedSolvableClassification: Bool? { get } // swiftlint:disable:this discouraged_optional_boolean +} diff --git a/Sources/ApodiniMigratorCompare/Comparators/Comparator.swift b/Sources/ApodiniMigratorCompare/Comparators/Comparator.swift index 571543c3..5a4aca30 100644 --- a/Sources/ApodiniMigratorCompare/Comparators/Comparator.swift +++ b/Sources/ApodiniMigratorCompare/Comparators/Comparator.swift @@ -10,40 +10,11 @@ import Foundation protocol Comparator { - associatedtype Element: Value + associatedtype ComparableElement: Value + associatedtype ChangeElement: ChangeableElement - var lhs: Element { get } - var rhs: Element { get } + var lhs: ComparableElement { get } + var rhs: ComparableElement { get } - var changes: ChangeContextNode { get } - - var configuration: EncoderConfiguration { get } - - func compare() -} - -extension Comparator { - var includeProviderSupport: Bool { - changes.compareConfiguration?.includeProviderSupport == true - } - - var allowEndpointIdentifierUpdate: Bool { - changes.compareConfiguration?.allowEndpointIdentifierUpdate == true - } - - var allowTypeRename: Bool { - changes.compareConfiguration?.allowTypeRename == true - } - - func sameNestedTypes(lhs: TypeInformation, rhs: TypeInformation) -> Bool { - if lhs.typeName.name == rhs.typeName.name { - return true - } - return allowTypeRename ? changes.typesAreRenamings(lhs: lhs, rhs: rhs) : false - } - - func typesNeedConvert(lhs: TypeInformation, rhs: TypeInformation) -> Bool { - let sameNestedType = sameNestedTypes(lhs: lhs, rhs: rhs) - return (sameNestedType && !lhs.sameType(with: rhs)) || !sameNestedType - } + func compare(_ context: ChangeComparisonContext, _ results: inout [Change]) } diff --git a/Sources/ApodiniMigratorCompare/Comparators/DocumentComparator.swift b/Sources/ApodiniMigratorCompare/Comparators/DocumentComparator.swift index bc162369..5ec24d7b 100644 --- a/Sources/ApodiniMigratorCompare/Comparators/DocumentComparator.swift +++ b/Sources/ApodiniMigratorCompare/Comparators/DocumentComparator.swift @@ -8,25 +8,45 @@ import Foundation -struct DocumentComparator: Comparator { - let lhs: Document - let rhs: Document - let changes: ChangeContextNode - let configuration: EncoderConfiguration - - func compare() { - let metaDataComparator = MetaDataComparator(lhs: lhs.metaData, rhs: rhs.metaData, changes: changes, configuration: configuration) - metaDataComparator.compare() - +/// The ``DocumentComparator`` allows to compare two `APIDocument` to uncover any changes between them. +public struct DocumentComparator { + /// The original/base document. + public let lhs: APIDocument + /// The updated document. + public let rhs: APIDocument + /// The associated ``ChangeComparisonContext``. It is used to track any changes. + /// Use this property to access the resulting change arrays. + public let context: ChangeComparisonContext + + /// Initialize a new DocumentComparator. + /// - Parameters: + /// - configuration: The ``CompareConfiguration`` used for the comparisons. + /// - lhs: The base `APIDocument`. + /// - rhs: The updated `APIDocument`. + public init(configuration: CompareConfiguration? = nil, lhs: APIDocument, rhs: APIDocument) { + self.lhs = lhs + self.rhs = rhs + self.context = ChangeComparisonContext( + configuration: configuration, + latestModels: rhs.models + ) + } + + /// This method kicks of the comparison operations. + /// After this method has completed, you can access the ``context`` property to acquire the results of the comparison. + public func compare() { + let metaDataComparator = ServiceInformationComparator(lhs: lhs.serviceInformation, rhs: rhs.serviceInformation) + metaDataComparator.compare(context, &context.serviceChanges) + + // It is important that the ModelsComparator runs before the EndpointsComparator, as this step + // collects possible migration js scripts which are later on referenced. let modelsComparator = ModelsComparator( - lhs: lhs.allModels(), - rhs: changes.set(rhsModels: rhs.allModels()), - changes: changes, - configuration: configuration + lhs: .init(lhs.models), + rhs: .init(rhs.models) ) - modelsComparator.compare() - - let endpointsComparator = EndpointsComparator(lhs: lhs.endpoints, rhs: rhs.endpoints, changes: changes, configuration: configuration) - endpointsComparator.compare() + modelsComparator.compare(context, &context.modelChanges) + + let endpointsComparator = EndpointsComparator(lhs: lhs.endpoints, rhs: rhs.endpoints) + endpointsComparator.compare(context, &context.endpointChanges) } } diff --git a/Sources/ApodiniMigratorCompare/Comparators/Endpoint/EndpointComparator.swift b/Sources/ApodiniMigratorCompare/Comparators/Endpoint/EndpointComparator.swift index 9769bf7d..e986aaeb 100644 --- a/Sources/ApodiniMigratorCompare/Comparators/Endpoint/EndpointComparator.swift +++ b/Sources/ApodiniMigratorCompare/Comparators/Endpoint/EndpointComparator.swift @@ -11,57 +11,59 @@ import Foundation struct EndpointComparator: Comparator { let lhs: Endpoint let rhs: Endpoint - let changes: ChangeContextNode - var configuration: EncoderConfiguration - - func compare() { - func element(_ target: EndpointTarget) -> ChangeElement { - .for(endpoint: lhs, target: target) - } - - if lhs.path != rhs.path { // Comparing resourcePaths - changes.add( - UpdateChange( - element: element(.resourcePath), - from: .element(lhs.path), - to: .element(rhs.path), - breaking: true, - solvable: true - ) + + func compare(_ context: ChangeComparisonContext, _ results: inout [EndpointChange]) { + var identifierChanges: [EndpointIdentifierChange] = [] + let identifiersComparator = IdentifiersComparator(lhs: .init(lhs.identifiers.values), rhs: .init(rhs.identifiers.values)) + identifiersComparator.compare(context, &identifierChanges) + results.append(contentsOf: identifierChanges.map { change in + .update( + id: lhs.deltaIdentifier, + updated: .identifier(identifier: change), + breaking: change.breaking, + solvable: change.solvable ) - } - - if lhs.operation != rhs.operation { - changes.add( - UpdateChange( - element: element(.operation), - from: .element(lhs.operation), - to: .element(rhs.operation), - breaking: true, - solvable: true + }) + + if lhs.communicationalPattern != rhs.communicationalPattern { + results.append(.update( + id: lhs.deltaIdentifier, + updated: .communicationalPattern( + from: lhs.communicationalPattern, + to: rhs.communicationalPattern ) - ) + )) } - - let parametersComparator = ParametersComparator(lhs: lhs, rhs: rhs, changes: changes, configuration: configuration) - parametersComparator.compare() - - let lhsResponse = lhs.response - let rhsResponse = rhs.response - - if typesNeedConvert(lhs: lhsResponse, rhs: rhsResponse) { - let jsScriptBuilder = JSScriptBuilder(from: lhsResponse, to: rhsResponse, changes: changes, encoderConfiguration: configuration) - changes.add( - UpdateChange( - element: element(.response), - from: .element(lhs.response.referenced()), - to: .element(rhs.response.referenced()), - convertToFrom: changes.store(script: jsScriptBuilder.convertToFrom), - convertionWarning: jsScriptBuilder.hint, - breaking: true, - solvable: true - ) + + + var parameterChanges: [ParameterChange] = [] + let parametersComparator = ParametersComparator(lhs: lhs.parameters, rhs: rhs.parameters) + parametersComparator.compare(context, ¶meterChanges) + results.append(contentsOf: parameterChanges.map { change in + .update( + id: lhs.deltaIdentifier, + updated: .parameter(parameter: change), + breaking: change.breaking, + solvable: change.solvable ) + }) + + + // by using `buildString` we exclude the target name from the comparison. We don't care about + // migrations happening on target level (e.g. moving models between targets). + if lhs.response.typeName.buildName() != rhs.response.typeName.buildName() { + let jsScriptBuilder = JSScriptBuilder(from: lhs.response, to: rhs.response, context: context) + let migrationId = context.store(script: jsScriptBuilder.convertToFrom) + + results.append(.update( + id: lhs.deltaIdentifier, + updated: .response( + from: lhs.response.asReference(), + to: rhs.response.asReference(), + backwardsMigration: migrationId, + migrationWarning: jsScriptBuilder.hint + ) + )) } } } diff --git a/Sources/ApodiniMigratorCompare/Comparators/Endpoint/EndpointsComparator.swift b/Sources/ApodiniMigratorCompare/Comparators/Endpoint/EndpointsComparator.swift index 9234167b..f74cfaf9 100644 --- a/Sources/ApodiniMigratorCompare/Comparators/Endpoint/EndpointsComparator.swift +++ b/Sources/ApodiniMigratorCompare/Comparators/Endpoint/EndpointsComparator.swift @@ -8,97 +8,66 @@ import Foundation -extension Array: Value where Element: Value {} - struct EndpointsComparator: Comparator { + struct MatchedPairs: Hashable { + let candidate: Endpoint + let relaxedMatching: Endpoint + + func contains(_ id: DeltaIdentifier) -> Bool { + candidate.deltaIdentifier == id || relaxedMatching.deltaIdentifier == id + } + } + let lhs: [Endpoint] let rhs: [Endpoint] - let changes: ChangeContextNode - var configuration: EncoderConfiguration - - func compare() { + + func compare(_ context: ChangeComparisonContext, _ results: inout [EndpointChange]) { let matchedIds = lhs.matchedIds(with: rhs) let removalCandidates = lhs.filter { !matchedIds.contains($0.deltaIdentifier) } let additionCandidates = rhs.filter { !matchedIds.contains($0.deltaIdentifier) } - handle(removalCandidates: removalCandidates, additionCandidates: additionCandidates) - - for matched in matchedIds { - if let lhs = lhs.firstMatch(on: \.deltaIdentifier, with: matched), - let rhs = rhs.firstMatch(on: \.deltaIdentifier, with: matched) { - let endpointComparator = EndpointComparator(lhs: lhs, rhs: rhs, changes: changes, configuration: configuration) - endpointComparator.compare() - } - } - } - - private func handle(removalCandidates: [Endpoint], additionCandidates: [Endpoint]) { - struct MatchedPairs: Hashable { - let candidate: Endpoint - let relaxedMatching: Endpoint - - func contains(_ id: DeltaIdentifier) -> Bool { - candidate.deltaIdentifier == id || relaxedMatching.deltaIdentifier == id - } - } - + var pairs: Set = [] - - if allowEndpointIdentifierUpdate { + + if context.configuration.allowEndpointIdentifierUpdate { for candidate in removalCandidates { let unmatched = additionCandidates.filter { added in pairs.allSatisfy { !$0.contains(added.deltaIdentifier) } } if let relaxedMatching = candidate.mostSimilarWithSelf(in: unmatched, useRawValueDistance: false) { - changes.add( - UpdateChange( - element: .for(endpoint: candidate, target: .deltaIdentifier), - from: candidate.deltaIdentifier.rawValue, - to: relaxedMatching.element.deltaIdentifier.rawValue, - similarity: relaxedMatching.similarity, - breaking: false, - solvable: true, - includeProviderSupport: includeProviderSupport - ) - ) - + results.append(.idChange( + from: candidate.deltaIdentifier, + to: relaxedMatching.element.deltaIdentifier, + similarity: relaxedMatching.similarity + // includeProviderSupport: context.configuration.includeProviderSupport + )) + pairs.insert(.init(candidate: candidate, relaxedMatching: relaxedMatching.element)) } } - + pairs.forEach { - let endpointComparator = EndpointComparator( - lhs: $0.candidate, - rhs: $0.relaxedMatching, - changes: changes, - configuration: configuration - ) - endpointComparator.compare() + let endpointComparator = EndpointComparator(lhs: $0.candidate, rhs: $0.relaxedMatching) + endpointComparator.compare(context, &results) } } - - let includeProviderSupport = allowEndpointIdentifierUpdate && self.includeProviderSupport + for removal in removalCandidates where !pairs.contains(where: { $0.contains(removal.deltaIdentifier) }) { - changes.add( - DeleteChange( - element: .for(endpoint: removal, target: .`self`), - deleted: .id(from: removal), - fallbackValue: .none, - breaking: true, - solvable: false, - includeProviderSupport: includeProviderSupport - ) - ) + results.append(.removal( + id: removal.deltaIdentifier + )) } - + for addition in additionCandidates where !pairs.contains(where: { $0.contains(addition.deltaIdentifier) }) { - changes.add( - AddChange( - element: .for(endpoint: addition, target: .`self`), - added: .element(addition.referencedTypes()), - defaultValue: .none, - breaking: false, - solvable: true, - includeProviderSupport: includeProviderSupport - ) - ) + results.append(.addition( + id: addition.deltaIdentifier, + added: addition.referencedTypes() + )) + } + + for matched in matchedIds { + if let lhs = lhs.first(where: { $0.deltaIdentifier == matched }), + let rhs = rhs.first(where: { $0.deltaIdentifier == matched }) { + let endpointComparator = EndpointComparator(lhs: lhs, rhs: rhs) + endpointComparator.compare(context, &results) + } } } } diff --git a/Sources/ApodiniMigratorCompare/Comparators/Endpoint/IdentifiersComparator.swift b/Sources/ApodiniMigratorCompare/Comparators/Endpoint/IdentifiersComparator.swift new file mode 100644 index 00000000..d20833c9 --- /dev/null +++ b/Sources/ApodiniMigratorCompare/Comparators/Endpoint/IdentifiersComparator.swift @@ -0,0 +1,49 @@ +// +// This source file is part of the Apodini open source project +// +// SPDX-FileCopyrightText: 2019-2021 Paul Schmiedmayer and the Apodini project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + +import Foundation + +struct IdentifiersComparator: Comparator { + let lhs: [AnyEndpointIdentifier] + let rhs: [AnyEndpointIdentifier] + + func compare(_ context: ChangeComparisonContext, _ results: inout [EndpointIdentifierChange]) { + let matchedIds = lhs.matchedIds(with: rhs) + let removalCandidates = lhs.filter { !matchedIds.contains($0.deltaIdentifier) } + let additionCandidates = rhs.filter { !matchedIds.contains($0.deltaIdentifier) } + + for addition in additionCandidates { + results.append(.addition( + id: addition.deltaIdentifier, + added: addition, + breaking: false, + solvable: true + )) + } + + for removal in removalCandidates { + results.append(.removal( + id: removal.deltaIdentifier, + removed: removal, + breaking: true, + solvable: false + )) + } + + for matched in matchedIds { + if let lhs = lhs.first(where: { $0.deltaIdentifier == matched }), + let rhs = rhs.first(where: { $0.deltaIdentifier == matched }), + lhs.value != rhs.value { + results.append(.update( + id: lhs.deltaIdentifier, + updated: .init(from: lhs, to: rhs) + )) + } + } + } +} diff --git a/Sources/ApodiniMigratorCompare/Comparators/Endpoint/ParameterComparator.swift b/Sources/ApodiniMigratorCompare/Comparators/Endpoint/ParameterComparator.swift index 81b55318..ebdc2487 100644 --- a/Sources/ApodiniMigratorCompare/Comparators/Endpoint/ParameterComparator.swift +++ b/Sources/ApodiniMigratorCompare/Comparators/Endpoint/ParameterComparator.swift @@ -11,74 +11,48 @@ import Foundation struct ParameterComparator: Comparator { let lhs: Parameter let rhs: Parameter - let changes: ChangeContextNode - let configuration: EncoderConfiguration - let lhsEndpoint: Endpoint - - private var element: ChangeElement { - .for(endpoint: lhsEndpoint, target: .target(for: lhs)) - } - - private var targetID: DeltaIdentifier { - lhs.deltaIdentifier - } - - init(lhs: Parameter, rhs: Parameter, changes: ChangeContextNode, configuration: EncoderConfiguration, lhsEndpoint: Endpoint) { - self.lhs = lhs - self.rhs = rhs - self.changes = changes - self.configuration = configuration - self.lhsEndpoint = lhsEndpoint - } - - func compare() { + + func compare(_ context: ChangeComparisonContext, _ results: inout [ParameterChange]) { if lhs.parameterType != rhs.parameterType { - changes.add( - UpdateChange( - element: element, - from: .element(lhs.parameterType), - to: .element(rhs.parameterType), - targetID: targetID, - parameterTarget: .kind, - breaking: true, - solvable: true + results.append(.update( + id: lhs.deltaIdentifier, + updated: .parameterType( + from: lhs.parameterType, + to: rhs.parameterType ) - ) + )) } - - if sameNestedTypes(lhs: lhs.typeInformation, rhs: rhs.typeInformation), lhs.necessity != rhs.necessity, rhs.necessity == .required { - return changes.add( - UpdateChange( - element: element, - from: .element(lhs.necessity), - to: .element(rhs.necessity), - targetID: targetID, - necessityValue: .value(from: rhs.typeInformation, with: configuration, changes: changes), - parameterTarget: .necessity, - breaking: rhs.necessity == .required, - solvable: true - ) - ) + + if lhs.necessity != rhs.necessity { + let jsonValue = JSONValue(JSONStringBuilder.jsonString(rhs.typeInformation, with: context.configuration.encoderConfiguration)) + let jsonId = context.store(jsonValue: jsonValue) + + results.append(.update( + id: lhs.deltaIdentifier, + updated: .necessity( + from: lhs.necessity, + to: rhs.necessity, + necessityMigration: jsonId + ), + breaking: rhs.necessity == .required + )) } - - let lhsType = lhs.typeInformation - let rhsType = rhs.typeInformation - - if typesNeedConvert(lhs: lhsType, rhs: rhsType) { - let jsScriptBuilder = JSScriptBuilder(from: lhsType, to: rhsType, changes: changes, encoderConfiguration: configuration) - changes.add( - UpdateChange( - element: element, - from: .element(lhsType.referenced()), - to: .element(rhsType.referenced()), - targetID: targetID, - convertFromTo: changes.store(script: jsScriptBuilder.convertFromTo), - convertionWarning: jsScriptBuilder.hint, - parameterTarget: .typeInformation, - breaking: true, - solvable: true + + // by using `buildString` we exclude the target name from the comparison. We don't care about + // migrations happening on target level (e.g. moving models between targets). + if lhs.typeInformation.typeName.buildName() != rhs.typeInformation.typeName.buildName() { + let jsScriptBuilder = JSScriptBuilder(from: lhs.typeInformation, to: rhs.typeInformation, context: context) + let migrationId = context.store(script: jsScriptBuilder.convertFromTo) + + results.append(.update( + id: lhs.deltaIdentifier, + updated: .type( + from: lhs.typeInformation.asReference(), + to: rhs.typeInformation.asReference(), + forwardMigration: migrationId, + conversionWarning: jsScriptBuilder.hint ) - ) + )) } } } diff --git a/Sources/ApodiniMigratorCompare/Comparators/Endpoint/ParametersComparator.swift b/Sources/ApodiniMigratorCompare/Comparators/Endpoint/ParametersComparator.swift index a86497dc..3933d79d 100644 --- a/Sources/ApodiniMigratorCompare/Comparators/Endpoint/ParametersComparator.swift +++ b/Sources/ApodiniMigratorCompare/Comparators/Endpoint/ParametersComparator.swift @@ -9,108 +9,65 @@ import Foundation struct ParametersComparator: Comparator { - let lhs: Endpoint - let rhs: Endpoint - let changes: ChangeContextNode - var configuration: EncoderConfiguration - let lhsParameters: [Parameter] - let rhsParameters: [Parameter] - - init(lhs: Endpoint, rhs: Endpoint, changes: ChangeContextNode, configuration: EncoderConfiguration) { - self.lhs = lhs - self.rhs = rhs - self.changes = changes - self.configuration = configuration - self.lhsParameters = lhs.parameters - self.rhsParameters = rhs.parameters - } - - func compare() { - let matchedIds = lhsParameters.matchedIds(with: rhsParameters) - let removalCandidates = lhsParameters.filter { !matchedIds.contains($0.deltaIdentifier) } - let additionCandidates = rhsParameters.filter { !matchedIds.contains($0.deltaIdentifier) } - handle(removalCandidates: removalCandidates, additionCandidates: additionCandidates) - - for matched in matchedIds { - if let lhs = lhsParameters.firstMatch(on: \.deltaIdentifier, with: matched), - let rhs = rhsParameters.firstMatch(on: \.deltaIdentifier, with: matched) { - let parameterComparator = ParameterComparator( - lhs: lhs, - rhs: rhs, - changes: changes, - configuration: configuration, - lhsEndpoint: self.lhs - ) - parameterComparator.compare() - } - } - } + let lhs: [Parameter] + let rhs: [Parameter] + + func compare(_ context: ChangeComparisonContext, _ results: inout [ParameterChange]) { + let matchedIds = lhs.matchedIds(with: rhs) + let removalCandidates = lhs.filter { !matchedIds.contains($0.deltaIdentifier) } + let additionCandidates = rhs.filter { !matchedIds.contains($0.deltaIdentifier) } - - private func handle(removalCandidates: [Parameter], additionCandidates: [Parameter]) { var relaxedMatchings: Set = [] - + for candidate in removalCandidates { if let relaxedMatching = candidate.mostSimilarWithSelf(in: additionCandidates.filter { !relaxedMatchings.contains($0.deltaIdentifier) }) { relaxedMatchings += relaxedMatching.element.deltaIdentifier relaxedMatchings += candidate.deltaIdentifier - - changes.add( - UpdateChange( - element: element(.target(for: candidate)), - from: candidate.name, - to: relaxedMatching.element.name, - similarity: relaxedMatching.similarity, - breaking: true, - solvable: true, - includeProviderSupport: includeProviderSupport - ) - ) - let parameterComparator = ParameterComparator( - lhs: candidate, - rhs: relaxedMatching.element, - changes: changes, - configuration: configuration, - lhsEndpoint: self.lhs - ) - parameterComparator.compare() + + results.append(.idChange( + from: candidate.deltaIdentifier, + to: relaxedMatching.element.deltaIdentifier, + similarity: relaxedMatching.similarity, + breaking: true + )) + + let parameterComparator = ParameterComparator(lhs: candidate, rhs: relaxedMatching.element) + parameterComparator.compare(context, &results) } } - + for removal in removalCandidates where !relaxedMatchings.contains(removal.deltaIdentifier) { - changes.add( - DeleteChange( - element: element(.target(for: removal)), - deleted: .id(from: removal), - fallbackValue: .none, - breaking: false, - solvable: true, - includeProviderSupport: includeProviderSupport - ) - ) + results.append(.removal( + id: removal.deltaIdentifier, + breaking: false, + solvable: true + )) } - + for addition in additionCandidates where !relaxedMatchings.contains(addition.deltaIdentifier) { - var defaultValue: ChangeValue? + var defaultValueId: Int? let isRequired = addition.necessity == .required if isRequired { - defaultValue = .value(from: addition.typeInformation, with: configuration, changes: changes) - } - - changes.add( - AddChange( - element: element(.target(for: addition)), - added: .element(addition.referencedType()), - defaultValue: defaultValue ?? .none, - breaking: isRequired, - solvable: true, - includeProviderSupport: includeProviderSupport + let defaultJsonValue = JSONValue( + JSONStringBuilder.jsonString(addition.typeInformation, with: context.configuration.encoderConfiguration) ) - ) + defaultValueId = context.store(jsonValue: defaultJsonValue) + } + + results.append(.addition( + id: addition.deltaIdentifier, + added: addition.referencedType(), + defaultValue: defaultValueId, + breaking: isRequired + )) + } + + for matched in matchedIds { + if let lhs = lhs.first(where: { $0.deltaIdentifier == matched }), + let rhs = rhs.first(where: { $0.deltaIdentifier == matched }) { + let parameterComparator = ParameterComparator(lhs: lhs, rhs: rhs) + parameterComparator.compare(context, &results) + } } - } - - private func element(_ target: EndpointTarget) -> ChangeElement { - .for(endpoint: lhs, target: target) } } diff --git a/Sources/ApodiniMigratorCompare/Comparators/MetaDataComparator.swift b/Sources/ApodiniMigratorCompare/Comparators/MetaDataComparator.swift deleted file mode 100644 index 97985b6c..00000000 --- a/Sources/ApodiniMigratorCompare/Comparators/MetaDataComparator.swift +++ /dev/null @@ -1,64 +0,0 @@ -// -// This source file is part of the Apodini open source project -// -// SPDX-FileCopyrightText: 2019-2021 Paul Schmiedmayer and the Apodini project authors (see CONTRIBUTORS.md) -// -// SPDX-License-Identifier: MIT -// - -import Foundation - -struct MetaDataComparator: Comparator { - let lhs: MetaData - let rhs: MetaData - let changes: ChangeContextNode - var configuration: EncoderConfiguration - - func compare() { - func element(_ target: NetworkingTarget) -> ChangeElement { - .networking(target: target) - } - - if lhs.versionedServerPath != rhs.versionedServerPath { - changes.add( - UpdateChange( - element: element(.serverPath), - from: .stringValue(lhs.versionedServerPath), - to: .stringValue(rhs.versionedServerPath), - breaking: true, - solvable: true - ) - ) - } - - let lhsEncoderConfig = lhs.encoderConfiguration - let rhsEncoderConfig = rhs.encoderConfiguration - - if lhsEncoderConfig != rhsEncoderConfig { - changes.add( - UpdateChange( - element: element(.encoderConfiguration), - from: .element(lhsEncoderConfig), - to: .element(rhsEncoderConfig), - breaking: true, - solvable: true - ) - ) - } - - let lhsDecoderConfig = lhs.decoderConfiguration - let rhsDecoderConfig = rhs.decoderConfiguration - - if lhsDecoderConfig != rhsDecoderConfig { - changes.add( - UpdateChange( - element: element(.decoderConfiguration), - from: .element(lhsDecoderConfig), - to: .element(rhsDecoderConfig), - breaking: true, - solvable: true - ) - ) - } - } -} diff --git a/Sources/ApodiniMigratorCompare/Comparators/Model/EnumCasesComparator.swift b/Sources/ApodiniMigratorCompare/Comparators/Model/EnumCasesComparator.swift new file mode 100644 index 00000000..ecc6c130 --- /dev/null +++ b/Sources/ApodiniMigratorCompare/Comparators/Model/EnumCasesComparator.swift @@ -0,0 +1,68 @@ +// +// This source file is part of the Apodini open source project +// +// SPDX-FileCopyrightText: 2019-2021 Paul Schmiedmayer and the Apodini project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + +import Foundation + +struct EnumCasesComparator: Comparator { + let lhs: [EnumCase] + let rhs: [EnumCase] + + func compare(_ context: ChangeComparisonContext, _ results: inout [EnumCaseChange]) { + let matchedIds = lhs.matchedIds(with: rhs) + let removalCandidates = lhs.filter { !matchedIds.contains($0.deltaIdentifier) } + let additionCandidates = rhs.filter { !matchedIds.contains($0.deltaIdentifier) } + + var relaxedMatchings: Set = [] + + for candidate in removalCandidates { + if let relaxedMatching = candidate.mostSimilarWithSelf(in: additionCandidates.filter { !relaxedMatchings.contains($0.deltaIdentifier) }) { + relaxedMatchings += relaxedMatching.element.deltaIdentifier + relaxedMatchings += candidate.deltaIdentifier + + results.append(.idChange( + from: candidate.deltaIdentifier, + to: relaxedMatching.element.deltaIdentifier, + similarity: relaxedMatching.similarity, + breaking: true + )) + + if candidate.rawValue != relaxedMatching.element.rawValue { + results.append(.update( + id: candidate.deltaIdentifier, + updated: .rawValue(from: candidate.rawValue, to: relaxedMatching.element.rawValue) + )) + } + } + } + + for removal in removalCandidates where !relaxedMatchings.contains(removal.deltaIdentifier) { + results.append(.removal( + id: removal.deltaIdentifier, + solvable: true + )) + } + + for addition in additionCandidates where !relaxedMatchings.contains(addition.deltaIdentifier) { + results.append(.addition( + id: addition.deltaIdentifier, + added: addition + )) + } + + for matched in matchedIds { + if let lhs = lhs.first(where: { $0.deltaIdentifier == matched }), + let rhs = rhs.first(where: { $0.deltaIdentifier == matched }), + lhs.rawValue != rhs.rawValue { + results.append(.update( + id: lhs.deltaIdentifier, + updated: .rawValue(from: lhs.rawValue, to: rhs.rawValue) + )) + } + } + } +} diff --git a/Sources/ApodiniMigratorCompare/Comparators/Model/EnumComparator.swift b/Sources/ApodiniMigratorCompare/Comparators/Model/EnumComparator.swift index 7c6866fd..e4855de5 100644 --- a/Sources/ApodiniMigratorCompare/Comparators/Model/EnumComparator.swift +++ b/Sources/ApodiniMigratorCompare/Comparators/Model/EnumComparator.swift @@ -12,131 +12,35 @@ import ApodiniTypeInformation struct EnumComparator: Comparator { let lhs: TypeInformation let rhs: TypeInformation - let changes: ChangeContextNode - let configuration: EncoderConfiguration - - func element(_ target: EnumTarget) -> ChangeElement { - .for(enum: lhs, target: target) - } - - func compare() { + + func compare(_ context: ChangeComparisonContext, _ results: inout [ModelChange]) { guard let lhsRawValue = lhs.rawValueType, let rhsRawValue = rhs.rawValueType else { - return - } - - if lhsRawValue != rhsRawValue { - return changes.add( - UnsupportedChange( - element: element(.`self`), - description: "The raw value type of this enum has changed to \(rhsRawValue.nestedTypeString). ApodiniMigrator is not able to migrate this change" - ) - ) + fatalError("Encountered non enum when comparing enum models") } - - let enumCasesComparator = EnumCasesComparator(lhs: lhs, rhs: rhs, changes: changes, configuration: configuration) - enumCasesComparator.compare() - } -} + if lhsRawValue != rhsRawValue { + results.append(.update( + id: lhs.deltaIdentifier, + updated: .rawValueType( + from: lhsRawValue.asReference(), + to: rhsRawValue.asReference() + ), + solvable: false + )) -private struct EnumCasesComparator: Comparator { - let lhs: TypeInformation - let rhs: TypeInformation - let changes: ChangeContextNode - let configuration: EncoderConfiguration - let lhsCases: [EnumCase] - let rhsCases: [EnumCase] - - init(lhs: TypeInformation, rhs: TypeInformation, changes: ChangeContextNode, configuration: EncoderConfiguration) { - self.lhs = lhs - self.rhs = rhs - self.changes = changes - self.configuration = configuration - self.lhsCases = lhs.enumCases - self.rhsCases = rhs.enumCases - } - - func compare() { - let matchedIds = lhsCases.matchedIds(with: rhsCases) - let removalCandidates = lhsCases.filter { !matchedIds.contains($0.deltaIdentifier) } - let additionCanditates = rhsCases.filter { !matchedIds.contains($0.deltaIdentifier) } - handle(removalCandidates: removalCandidates, additionCandidates: additionCanditates) - - for matched in matchedIds { - if let lhs = lhsCases.firstMatch(on: \.deltaIdentifier, with: matched), - let rhs = rhsCases.firstMatch(on: \.deltaIdentifier, with: matched) { - compare(lhs: lhs, rhs: rhs) - } - } - } - - private func compare(lhs: EnumCase, rhs: EnumCase) { - if lhs.rawValue != rhs.rawValue { - changes.add( - UpdateChange( - element: element(.caseRawValue), - from: .element(lhs), - to: .element(rhs), - breaking: true, - solvable: true - ) - ) - } - } - - private func element(_ target: EnumTarget) -> ChangeElement { - .for(enum: lhs, target: target) - } - - - private func handle(removalCandidates: [EnumCase], additionCandidates: [EnumCase]) { - var relaxedMatchings: Set = [] - - for candidate in removalCandidates { - if let relaxedMatching = candidate.mostSimilarWithSelf(in: additionCandidates.filter { !relaxedMatchings.contains($0.deltaIdentifier) }) { - relaxedMatchings += relaxedMatching.element.deltaIdentifier - relaxedMatchings += candidate.deltaIdentifier - - changes.add( - UpdateChange( - element: element(.case), - from: candidate.name, - to: relaxedMatching.element.name, - similarity: relaxedMatching.similarity, - breaking: true, - solvable: true, - includeProviderSupport: includeProviderSupport - ) - ) - - compare(lhs: candidate, rhs: relaxedMatching.element) - } - } - - for removal in removalCandidates where !relaxedMatchings.contains(removal.deltaIdentifier) { - changes.add( - DeleteChange( - element: element(.case), - deleted: .id(from: removal), - fallbackValue: .none, - breaking: true, - solvable: true, - includeProviderSupport: includeProviderSupport - ) - ) + return } - - for addition in additionCandidates where !relaxedMatchings.contains(addition.deltaIdentifier) { - changes.add( - AddChange( - element: element(.case), - added: .element(addition), - defaultValue: .none, - breaking: false, - solvable: true, - includeProviderSupport: includeProviderSupport - ) + + var enumCaseChanges: [EnumCaseChange] = [] + let enumCasesComparator = EnumCasesComparator(lhs: lhs.enumCases, rhs: rhs.enumCases) + enumCasesComparator.compare(context, &enumCaseChanges) + results.append(contentsOf: enumCaseChanges.map { change in + .update( + id: lhs.deltaIdentifier, + updated: .case(case: change), + breaking: change.breaking, + solvable: change.solvable ) - } + }) } } diff --git a/Sources/ApodiniMigratorCompare/Comparators/Model/ModelComparator.swift b/Sources/ApodiniMigratorCompare/Comparators/Model/ModelComparator.swift index e43e1943..b63efe5e 100644 --- a/Sources/ApodiniMigratorCompare/Comparators/Model/ModelComparator.swift +++ b/Sources/ApodiniMigratorCompare/Comparators/Model/ModelComparator.swift @@ -11,25 +11,26 @@ import Foundation struct ModelComparator: Comparator { let lhs: TypeInformation let rhs: TypeInformation - let changes: ChangeContextNode - let configuration: EncoderConfiguration - - func compare() { - guard lhs.rootType == rhs.rootType, [TypeInformation.RootType.enum, .object].contains(lhs.rootType) else { - return changes.add( - UnsupportedChange( - element: lhs.isObject ? .for(object: lhs, target: .`self`) : .for(enum: lhs, target: .`self`), - description: "ApodiniMigrator is not able to handle the migration of \(lhs.typeName.name). Change from enum to object or vice versa is currently not supported" - ) - ) + + func compare(_ context: ChangeComparisonContext, _ results: inout [ModelChange]) { + if lhs.rootType != rhs.rootType { + results.append(.update( + id: lhs.deltaIdentifier, + updated: .rootType(from: lhs.rootType, to: rhs.rootType, newModel: rhs), + breaking: true, + solvable: false + )) + + // we can't compare two types with different root type + return } if lhs.rootType == .object { - let objectComparator = ObjectComparator(lhs: lhs, rhs: rhs, changes: changes, configuration: configuration) - objectComparator.compare() + let objectComparator = ObjectComparator(lhs: lhs, rhs: rhs) + objectComparator.compare(context, &results) } else if lhs.rootType == .enum { - let enumComparator = EnumComparator(lhs: lhs, rhs: rhs, changes: changes, configuration: configuration) - enumComparator.compare() + let enumComparator = EnumComparator(lhs: lhs, rhs: rhs) + enumComparator.compare(context, &results) } } } diff --git a/Sources/ApodiniMigratorCompare/Comparators/Model/ModelsComparator.swift b/Sources/ApodiniMigratorCompare/Comparators/Model/ModelsComparator.swift index ab32974d..34e05d1e 100644 --- a/Sources/ApodiniMigratorCompare/Comparators/Model/ModelsComparator.swift +++ b/Sources/ApodiniMigratorCompare/Comparators/Model/ModelsComparator.swift @@ -9,89 +9,67 @@ import Foundation struct ModelsComparator: Comparator { + struct MatchedPairs: Hashable { + let candidate: TypeInformation + let relaxedMatching: TypeInformation + + func contains(_ id: DeltaIdentifier) -> Bool { + [candidate, relaxedMatching].identifiers().contains(id) + } + } + let lhs: [TypeInformation] let rhs: [TypeInformation] - let changes: ChangeContextNode - var configuration: EncoderConfiguration - - func compare() { + + func compare(_ context: ChangeComparisonContext, _ results: inout [ModelChange]) { let matchedIds = lhs.matchedIds(with: rhs) let removalCandidates = lhs.filter { !matchedIds.contains($0.deltaIdentifier) } let additionCandidates = rhs.filter { !matchedIds.contains($0.deltaIdentifier) } - handle(removalCandidates: removalCandidates, additionCandidates: additionCandidates) - - for matched in matchedIds { - if let lhs = lhs.firstMatch(on: \.deltaIdentifier, with: matched), - let rhs = rhs.firstMatch(on: \.deltaIdentifier, with: matched) { - let modelComparator = ModelComparator(lhs: lhs, rhs: rhs, changes: changes, configuration: configuration) - modelComparator.compare() - } - } - } - - private func handle(removalCandidates: [TypeInformation], additionCandidates: [TypeInformation]) { - struct MatchedPairs: Hashable { - let candidate: TypeInformation - let relaxedMatching: TypeInformation - - func contains(_ id: DeltaIdentifier) -> Bool { - [candidate, relaxedMatching].identifiers().contains(id) - } - } - + var pairs: Set = [] - - if allowTypeRename { + + if context.configuration.allowTypeRename { for candidate in removalCandidates { let unmatched = additionCandidates.filter { addition in pairs.allSatisfy { !$0.contains(addition.deltaIdentifier) } } if let relaxedMatching = candidate.mostSimilarWithSelf(in: unmatched) { - changes.add( - UpdateChange( - element: candidate.isObject ? .for(object: candidate, target: .typeName) : .for(enum: candidate, target: .typeName), - from: candidate.deltaIdentifier.rawValue, - to: relaxedMatching.element.deltaIdentifier.rawValue, - similarity: relaxedMatching.similarity, - breaking: false, - solvable: true, - includeProviderSupport: includeProviderSupport - ) - ) + results.append(.idChange( + from: candidate.deltaIdentifier, + to: relaxedMatching.element.deltaIdentifier, + similarity: relaxedMatching.similarity + )) + pairs.insert(.init(candidate: candidate, relaxedMatching: relaxedMatching.element)) } } - + // ensuring to have registered potential type renamings before comparing pairs.forEach { - let modelComparator = ModelComparator(lhs: $0.candidate, rhs: $0.relaxedMatching, changes: changes, configuration: configuration) - modelComparator.compare() + let modelComparator = ModelComparator(lhs: $0.candidate, rhs: $0.relaxedMatching) + modelComparator.compare(context, &results) } } - - let includeProviderSupport = allowTypeRename && self.includeProviderSupport + for removal in removalCandidates where !pairs.contains(where: { $0.contains(removal.deltaIdentifier) }) { - changes.add( - DeleteChange( - element: removal.isObject ? .for(object: removal, target: .`self`) : .for(enum: removal, target: .`self`), - deleted: .id(from: removal), - fallbackValue: .none, - breaking: false, - solvable: false, - includeProviderSupport: includeProviderSupport - ) - ) + results.append(.removal( + id: removal.deltaIdentifier, + breaking: false + )) } - + for addition in additionCandidates where !pairs.contains(where: { $0.contains(addition.deltaIdentifier) }) { - changes.add( - AddChange( - element: addition.isObject ? .for(object: addition, target: .`self`) : .for(enum: addition, target: .`self`), - added: .element(addition.referencedProperties()), - defaultValue: .none, - breaking: false, - solvable: true, - includeProviderSupport: includeProviderSupport - ) - ) + results.append(.addition( + id: addition.deltaIdentifier, + added: addition.referencedProperties(), + breaking: false + )) + } + + for matched in matchedIds { + if let lhs = lhs.first(where: { $0.deltaIdentifier == matched }), + let rhs = rhs.first(where: { $0.deltaIdentifier == matched }) { + let modelComparator = ModelComparator(lhs: lhs, rhs: rhs) + modelComparator.compare(context, &results) + } } } } diff --git a/Sources/ApodiniMigratorCompare/Comparators/Model/ObjectComparator.swift b/Sources/ApodiniMigratorCompare/Comparators/Model/ObjectComparator.swift index cc2ebbea..28c9d454 100644 --- a/Sources/ApodiniMigratorCompare/Comparators/Model/ObjectComparator.swift +++ b/Sources/ApodiniMigratorCompare/Comparators/Model/ObjectComparator.swift @@ -11,127 +11,20 @@ import Foundation struct ObjectComparator: Comparator { let lhs: TypeInformation let rhs: TypeInformation - let changes: ChangeContextNode - let configuration: EncoderConfiguration - let lhsProperties: [TypeProperty] - let rhsProperties: [TypeProperty] - - init(lhs: TypeInformation, rhs: TypeInformation, changes: ChangeContextNode, configuration: EncoderConfiguration) { - self.lhs = lhs - self.rhs = rhs - self.changes = changes - self.configuration = configuration - self.lhsProperties = lhs.objectProperties - self.rhsProperties = rhs.objectProperties - } - - func compare() { - let matchedIds = lhsProperties.matchedIds(with: rhsProperties) - let removalCandidates = lhsProperties.filter { !matchedIds.contains($0.deltaIdentifier) } - let additionCandidates = rhsProperties.filter { !matchedIds.contains($0.deltaIdentifier) } - handle(removalCandidates: removalCandidates, additionCandidates: additionCandidates) - - for matched in matchedIds { - if let lhs = lhsProperties.firstMatch(on: \.deltaIdentifier, with: matched), - let rhs = rhsProperties.firstMatch(on: \.deltaIdentifier, with: matched) { - compare(lhs: lhs, rhs: rhs) - } - } - - changes.store(rhs: rhs, encoderConfiguration: configuration) - } - - private func compare(lhs: TypeProperty, rhs: TypeProperty) { - let lhsType = lhs.type - let rhsType = rhs.type - - let targetID = lhs.deltaIdentifier - - if sameNestedTypes(lhs: lhsType, rhs: rhsType), lhs.necessity != rhs.necessity { - let currentLhsType = changes.currentVersion(of: lhsType) - changes.add( - UpdateChange( - element: element(.necessity), - from: .element(lhs.necessity), - to: .element(rhs.necessity), - necessityValue: .value(from: currentLhsType.unwrapped, with: configuration, changes: changes), - targetID: targetID, - breaking: true, - solvable: true - ) - ) - } else if typesNeedConvert(lhs: lhsType, rhs: rhsType) { - let jsScriptBuilder = JSScriptBuilder(from: lhsType, to: rhsType, changes: changes, encoderConfiguration: configuration) - changes.add( - UpdateChange( - element: element(.property), - from: .element(lhsType.referenced()), - to: .element(rhsType.referenced()), - targetID: targetID, - convertFromTo: changes.store(script: jsScriptBuilder.convertFromTo), - convertToFrom: changes.store(script: jsScriptBuilder.convertToFrom), - convertionWarning: jsScriptBuilder.hint, - breaking: true, - solvable: true - ) - ) - } - } - - private func handle(removalCandidates: [TypeProperty], additionCandidates: [TypeProperty]) { - var relaxedMatchings: Set = [] - - for candidate in removalCandidates { - if let relaxedMatching = candidate.mostSimilarWithSelf(in: additionCandidates.filter { !relaxedMatchings.contains($0.deltaIdentifier) }) { - relaxedMatchings += relaxedMatching.element.deltaIdentifier - relaxedMatchings += candidate.deltaIdentifier - - changes.add( - UpdateChange( - element: element(.property), - from: candidate.name, - to: relaxedMatching.element.name, - similarity: relaxedMatching.similarity, - breaking: true, - solvable: true, - includeProviderSupport: includeProviderSupport - ) - ) - - compare(lhs: candidate, rhs: relaxedMatching.element) - } - } - - for removal in removalCandidates where !relaxedMatchings.contains(removal.deltaIdentifier) { - let wasRequired = removal.necessity == .required - changes.add( - DeleteChange( - element: element(.property), - deleted: .id(from: removal), - fallbackValue: wasRequired ? .value(from: removal.type, with: configuration, changes: changes) : .none, - breaking: wasRequired, - solvable: true, - includeProviderSupport: includeProviderSupport - ) - ) - } - - for addition in additionCandidates where !relaxedMatchings.contains(addition.deltaIdentifier) { - let isRequired = addition.necessity == .required - changes.add( - AddChange( - element: element(.property), - added: .element(addition.referencedType()), - defaultValue: isRequired ? .value(from: addition.type, with: configuration, changes: changes) : .none, - breaking: isRequired, - solvable: true, - includeProviderSupport: includeProviderSupport - ) + + func compare(_ context: ChangeComparisonContext, _ results: inout [ModelChange]) { + var propertyChanges: [PropertyChange] = [] + let propertiesComparator = ObjectPropertiesComparator(lhs: lhs.objectProperties, rhs: rhs.objectProperties) + propertiesComparator.compare(context, &propertyChanges) + results.append(contentsOf: propertyChanges.map { change in + .update( + id: lhs.deltaIdentifier, + updated: .property(property: change), + breaking: change.breaking, + solvable: change.solvable ) - } - } - - private func element(_ target: ObjectTarget) -> ChangeElement { - .for(object: lhs, target: target) + }) + + context.store(rhs: rhs, into: &results) } } diff --git a/Sources/ApodiniMigratorCompare/Comparators/Model/ObjectPropertiesComparator.swift b/Sources/ApodiniMigratorCompare/Comparators/Model/ObjectPropertiesComparator.swift new file mode 100644 index 00000000..ac0bd1a4 --- /dev/null +++ b/Sources/ApodiniMigratorCompare/Comparators/Model/ObjectPropertiesComparator.swift @@ -0,0 +1,118 @@ +// +// This source file is part of the Apodini open source project +// +// SPDX-FileCopyrightText: 2019-2021 Paul Schmiedmayer and the Apodini project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + +import Foundation + +struct ObjectPropertiesComparator: Comparator { + let lhs: [TypeProperty] + let rhs: [TypeProperty] + + func compare(_ context: ChangeComparisonContext, _ results: inout [PropertyChange]) { + let matchedIds = lhs.matchedIds(with: rhs) + let removalCandidates = lhs.filter { !matchedIds.contains($0.deltaIdentifier) } + let additionCandidates = rhs.filter { !matchedIds.contains($0.deltaIdentifier) } + + var relaxedMatchings: Set = [] + + for candidate in removalCandidates { + if let relaxedMatching = candidate.mostSimilarWithSelf(in: additionCandidates.filter { !relaxedMatchings.contains($0.deltaIdentifier) }) { + relaxedMatchings += relaxedMatching.element.deltaIdentifier + relaxedMatchings += candidate.deltaIdentifier + + results.append(.idChange( + from: candidate.deltaIdentifier, + to: relaxedMatching.element.deltaIdentifier, + similarity: relaxedMatching.similarity, + breaking: true + )) + + compare(context, &results, lhs: candidate, rhs: relaxedMatching.element) + } + } + + for removal in removalCandidates where !relaxedMatchings.contains(removal.deltaIdentifier) { + let wasRequired = removal.necessity == .required + + var valueId: Int? + if wasRequired { + let jsonValue = JSONValue(JSONStringBuilder.jsonString(removal.type, with: context.configuration.encoderConfiguration)) + valueId = context.store(jsonValue: jsonValue) + } + + results.append(.removal( + id: removal.deltaIdentifier, + fallbackValue: valueId, + breaking: wasRequired, + solvable: true + )) + } + + for addition in additionCandidates where !relaxedMatchings.contains(addition.deltaIdentifier) { + let isRequired = addition.necessity == .required + + var valueId: Int? + if isRequired { + let jsonValue = JSONValue(JSONStringBuilder.jsonString(addition.type, with: context.configuration.encoderConfiguration)) + valueId = context.store(jsonValue: jsonValue) + } + + results.append(.addition( + id: addition.deltaIdentifier, + added: addition.referencedType(), + defaultValue: valueId, + breaking: isRequired, + solvable: true + )) + } + + for matched in matchedIds { + if let lhs = lhs.first(where: { $0.deltaIdentifier == matched }), + let rhs = rhs.first(where: { $0.deltaIdentifier == matched }) { + compare(context, &results, lhs: lhs, rhs: rhs) + } + } + } + + private func compare(_ context: ChangeComparisonContext, _ results: inout [PropertyChange], lhs: TypeProperty, rhs: TypeProperty) { + let lhsType = lhs.type + let rhsType = rhs.type + + if lhsType.typeName == rhsType.typeName && lhs.necessity != rhs.necessity { + let currentLhsType = context.currentVersion(of: lhsType) + let jsonValue = JSONValue(JSONStringBuilder.jsonString(currentLhsType.unwrapped, with: context.configuration.encoderConfiguration)) + let migrationId = context.store(jsonValue: jsonValue) + + results.append(.update( + id: lhs.deltaIdentifier, + updated: .necessity( + from: lhs.necessity, + to: rhs.necessity, + necessityMigration: migrationId + ) + )) + } + + if lhsType.typeName != rhsType.typeName { + let jsScriptBuilder = JSScriptBuilder(from: lhsType, to: rhsType, context: context) + + let forwardScript = context.store(script: jsScriptBuilder.convertFromTo) + let backwardScript = context.store(script: jsScriptBuilder.convertToFrom) + + results.append(.update( + id: lhs.deltaIdentifier, + updated: .type( + from: lhsType.asReference(), + to: rhsType.asReference(), + forwardMigration: forwardScript, + backwardMigration: backwardScript, + conversionWarning: jsScriptBuilder.hint + ) + )) + } + } +} diff --git a/Sources/ApodiniMigratorCompare/Comparators/ServiceInformationComparator.swift b/Sources/ApodiniMigratorCompare/Comparators/ServiceInformationComparator.swift new file mode 100644 index 00000000..3e4cf453 --- /dev/null +++ b/Sources/ApodiniMigratorCompare/Comparators/ServiceInformationComparator.swift @@ -0,0 +1,79 @@ +// +// This source file is part of the Apodini open source project +// +// SPDX-FileCopyrightText: 2019-2021 Paul Schmiedmayer and the Apodini project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + +import Foundation + +struct ServiceInformationComparator: Comparator { + let lhs: ServiceInformation + let rhs: ServiceInformation + + func compare(_ context: ChangeComparisonContext, _ results: inout [ServiceInformationChange]) { + if lhs.version.string != rhs.version.string { + results.append(.update( + id: lhs.deltaIdentifier, + updated: .version(from: lhs.version, to: rhs.version), + // while rest uses the version in the path, the path itself encodes that + // breaking change via the EndpointIdentifierChange + breaking: false + )) + } + + if lhs.http.description != rhs.http.description { + results.append(.update( + id: lhs.deltaIdentifier, + updated: .http(from: lhs.http, to: rhs.http) + )) + } + + + var types = Set(lhs.configuredExporters) + types.formUnion(rhs.configuredExporters) + for type in types { + let lhsExporter = lhs.exporters[type] + let rhsExporter = rhs.exporters[type] + + let change: ExporterConfigurationChange? + switch (lhsExporter, rhsExporter) { + case let (nil, .some(rhsExporter)): + change = .addition( + id: rhsExporter.deltaIdentifier, + added: rhsExporter, + defaultValue: nil, + breaking: true, + solvable: false + ) + case let (.some(lhsExporter), nil): + change = .removal( + id: lhsExporter.deltaIdentifier, + removed: nil, + fallbackValue: nil, + breaking: true, + solvable: false + ) + case let (.some(lhsExporter), .some(rhsExporter)): + change = .update( + id: lhsExporter.deltaIdentifier, + updated: .init(from: lhsExporter, to: rhsExporter), + breaking: true, + solvable: true // we assume solve-ability + ) + default: + change = nil + } + + if let change = change { + results.append(.update( + id: lhs.deltaIdentifier, + updated: .exporter(exporter: change), + breaking: change.breaking, + solvable: change.solvable + )) + } + } + } +} diff --git a/Sources/ApodiniMigratorCompare/CompareConfiguration.swift b/Sources/ApodiniMigratorCompare/CompareConfiguration.swift index 4a032fb2..142274de 100644 --- a/Sources/ApodiniMigratorCompare/CompareConfiguration.swift +++ b/Sources/ApodiniMigratorCompare/CompareConfiguration.swift @@ -9,22 +9,24 @@ import Foundation public struct CompareConfiguration: Value { - // MARK: Private Inner Types private enum CodingKeys: String, CodingKey { case includeProviderSupport = "include-provider-support" case allowEndpointIdentifierUpdate = "allowed-endpoint-id-update" case allowTypeRename = "allowed-type-rename" + case encoderConfiguration } let includeProviderSupport: Bool let allowEndpointIdentifierUpdate: Bool let allowTypeRename: Bool + let encoderConfiguration: EncoderConfiguration public static var `default`: CompareConfiguration { .init( includeProviderSupport: false, allowEndpointIdentifierUpdate: false, - allowTypeRename: false + allowTypeRename: false, + encoderConfiguration: .default ) } @@ -32,7 +34,8 @@ public struct CompareConfiguration: Value { .init( includeProviderSupport: true, allowEndpointIdentifierUpdate: true, - allowTypeRename: true + allowTypeRename: true, + encoderConfiguration: .default ) } } diff --git a/Sources/ApodiniMigratorCompare/JSConvert/JSObjectScript.swift b/Sources/ApodiniMigratorCompare/JSConvert/JSObjectScript.swift index f046d01c..df9abbe5 100644 --- a/Sources/ApodiniMigratorCompare/JSConvert/JSObjectScript.swift +++ b/Sources/ApodiniMigratorCompare/JSConvert/JSObjectScript.swift @@ -35,8 +35,7 @@ struct JSObjectScript { private let from: TypeInformation /// NewType private let to: TypeInformation - /// TypeRenames, additional attempt to increase the matching probability - private let changes: ChangeContextNode + private let context: ChangeComparisonContext /// Properties of old type private let fromProperties: [TypeProperty] /// Properties of new type @@ -48,21 +47,17 @@ struct JSObjectScript { /// JScript converting to to from, property holds the convert function right after initialization of the instance private(set) var convertToFrom: JSScript = "" - private let encoderConfiguration: EncoderConfiguration - /// Initializer of a new instance init( from: TypeInformation, to: TypeInformation, - changes: ChangeContextNode = .init(), - encoderConfiguration: EncoderConfiguration = .default + context: ChangeComparisonContext ) { self.from = from self.to = to - self.changes = changes - fromProperties = from.objectProperties - toProperties = to.objectProperties - self.encoderConfiguration = encoderConfiguration + self.context = context + self.fromProperties = from.objectProperties + self.toProperties = to.objectProperties construct() } @@ -89,7 +84,7 @@ struct JSObjectScript { } private mutating func process(fromProperty: TypeProperty, toProperty: TypeProperty, ignoreNames: Bool = false) { - if propertyHasMatching(toProperty, type: .to), !fromProperty.type.sameType(with: toProperty.type) { + if propertyHasMatching(toProperty, type: .to), !fromProperty.type.comparingRootType(with: toProperty.type) { return } // from here we now that the properties have the same cardinality, e.g. both optionals or both arrays... @@ -104,7 +99,7 @@ struct JSObjectScript { } // same cardinality, same name, same renamed type (e.g. User, UserNew) -> matching - if namesEqual, changes.typesAreRenamings(lhs: fromProperty.type, rhs: toProperty.type) { + if namesEqual, context.isPairOfRenamedTypes(lhs: fromProperty.type, rhs: toProperty.type) { return addMatching(from: fromProperty, to: toProperty) } } @@ -138,7 +133,7 @@ struct JSObjectScript { if let matchedName = matchedName { return "parsed\(type.inversed.rawValue).\(matchedName)" } - return JSONStringBuilder.jsonString(property.type, with: encoderConfiguration) + return JSONStringBuilder.jsonString(property.type, with: context.configuration.encoderConfiguration) }() return "\(property.name.singleQuoted): \(value)" } diff --git a/Sources/ApodiniMigratorCompare/JSConvert/JSScriptBuilder.swift b/Sources/ApodiniMigratorCompare/JSConvert/JSScriptBuilder.swift index 69b08433..61e2d317 100644 --- a/Sources/ApodiniMigratorCompare/JSConvert/JSScriptBuilder.swift +++ b/Sources/ApodiniMigratorCompare/JSConvert/JSScriptBuilder.swift @@ -13,38 +13,36 @@ import ApodiniMigratorClientSupport struct JSScriptBuilder { private let from: TypeInformation private let to: TypeInformation - private let changes: ChangeContextNode - private let encoderConfiguration: EncoderConfiguration + private let context: ChangeComparisonContext /// JScript converting from to to var convertFromTo: JSScript = "" /// JScript converting to to from var convertToFrom: JSScript = "" - /// Textual hint to be used in the change object if the convertion is not reliable + /// Textual hint to be used in the change object if the conversion is not reliable var hint: String? init( from: TypeInformation, to: TypeInformation, - changes: ChangeContextNode = .init(), - encoderConfiguration: EncoderConfiguration = .default + context: ChangeComparisonContext ) { self.from = from self.to = to - self.changes = changes - self.encoderConfiguration = encoderConfiguration + self.context = context construct() } private mutating func construct() { - let currentFrom = changes.currentVersion(of: from) + let currentFrom = context.currentVersion(of: from) + if case let .scalar(fromPrimitive) = currentFrom, case let .scalar(toPrimitive) = to { let primitiveScript = JSPrimitiveScript.script(from: fromPrimitive, to: toPrimitive) convertFromTo = primitiveScript.convertFromTo convertToFrom = primitiveScript.convertToFrom } else { if currentFrom.isObject, to.isObject { - let objectScript = JSObjectScript(from: currentFrom, to: to, changes: changes, encoderConfiguration: encoderConfiguration) + let objectScript = JSObjectScript(from: currentFrom, to: to, context: context) convertFromTo = objectScript.convertFromTo convertToFrom = objectScript.convertToFrom } else { @@ -52,11 +50,11 @@ struct JSScriptBuilder { hint = "'ApodiniMigrator' is not able to automatically generate convert scripts between two types with different cardinalities or root types. Convert methods must be provided by the developer of the web service. Otherwise, the respective types in the client applications that will consume this Migration Guide, will be initialized with these default scripts." convertFromTo = Self.stringify( argumentName: "ignoredFrom", - with: JSONStringBuilder.jsonString(to, with: encoderConfiguration) + with: JSONStringBuilder.jsonString(to, with: context.configuration.encoderConfiguration) ) convertToFrom = Self.stringify( argumentName: "ignoredTo", - with: JSONStringBuilder.jsonString(currentFrom, with: encoderConfiguration) + with: JSONStringBuilder.jsonString(currentFrom, with: context.configuration.encoderConfiguration) ) } } diff --git a/Sources/ApodiniMigratorCompare/Change/Changes/AddChange.swift b/Sources/ApodiniMigratorCompare/LegacyChangeModel/Changes/LegacyAddChange.swift similarity index 54% rename from Sources/ApodiniMigratorCompare/Change/Changes/AddChange.swift rename to Sources/ApodiniMigratorCompare/LegacyChangeModel/Changes/LegacyAddChange.swift index 74a6014c..c6e05de4 100644 --- a/Sources/ApodiniMigratorCompare/Change/Changes/AddChange.swift +++ b/Sources/ApodiniMigratorCompare/LegacyChangeModel/Changes/LegacyAddChange.swift @@ -9,7 +9,7 @@ import Foundation /// Represents an add change -public struct AddChange: Change { +struct LegacyAddChange: LegacyChange { // MARK: Private Inner Types private enum CodingKeys: String, CodingKey { case element @@ -22,35 +22,17 @@ public struct AddChange: Change { } /// Top-level changed element related to the change - public let element: ChangeElement + let element: LegacyChangeElement /// Type of change, always `.addition` - public let type: ChangeType + let type: LegacyChangeType /// The added value - public let added: ChangeValue + let added: LegacyChangeValue /// Default value of the added value - public let defaultValue: ChangeValue + let defaultValue: LegacyChangeValue /// Indicates whether the change is non-backward compatible - public let breaking: Bool + let breaking: Bool /// Indicates whether the change can be handled by `ApodiniMigrator` - public let solvable: Bool + let solvable: Bool /// Provider support field if `MigrationGuide.providerSupport` is set to `true` - public let providerSupport: ProviderSupport? - - /// Initializer for a new add change instance - init( - element: ChangeElement, - added: ChangeValue, - defaultValue: ChangeValue, - breaking: Bool, - solvable: Bool, - includeProviderSupport: Bool = false - ) { - self.element = element - self.added = added - self.defaultValue = defaultValue - self.breaking = breaking - self.solvable = solvable - self.providerSupport = includeProviderSupport ? .renameHint(Self.self) : nil - type = .addition - } + let providerSupport: LegacyProviderSupport? } diff --git a/Sources/ApodiniMigratorCompare/Change/Changes/DeleteChange.swift b/Sources/ApodiniMigratorCompare/LegacyChangeModel/Changes/LegacyDeleteChange.swift similarity index 53% rename from Sources/ApodiniMigratorCompare/Change/Changes/DeleteChange.swift rename to Sources/ApodiniMigratorCompare/LegacyChangeModel/Changes/LegacyDeleteChange.swift index b9eceb31..917008c1 100644 --- a/Sources/ApodiniMigratorCompare/Change/Changes/DeleteChange.swift +++ b/Sources/ApodiniMigratorCompare/LegacyChangeModel/Changes/LegacyDeleteChange.swift @@ -8,7 +8,7 @@ import Foundation -public struct DeleteChange: Change { +struct LegacyDeleteChange: LegacyChange { // MARK: Private Inner Types private enum CodingKeys: String, CodingKey { case element @@ -21,35 +21,17 @@ public struct DeleteChange: Change { } /// Top-level changed element related to the change - public let element: ChangeElement + let element: LegacyChangeElement /// Type of change, always `.deletion` - public let type: ChangeType + let type: LegacyChangeType /// Deleted value - public let deleted: ChangeValue + let deleted: LegacyChangeValue /// Fallback value for the deleted value - public let fallbackValue: ChangeValue + let fallbackValue: LegacyChangeValue /// Indicates whether the change is non-backward compatible - public let breaking: Bool + let breaking: Bool /// Indicates whether the change can be handled by `ApodiniMigrator` - public let solvable: Bool + let solvable: Bool /// Provider support field if `MigrationGuide.providerSupport` is set to `true` - public let providerSupport: ProviderSupport? - - /// Initializer for a new delete change instance - init( - element: ChangeElement, - deleted: ChangeValue, - fallbackValue: ChangeValue, - breaking: Bool, - solvable: Bool, - includeProviderSupport: Bool = false - ) { - self.element = element - self.deleted = deleted - self.fallbackValue = fallbackValue - self.breaking = breaking - self.solvable = solvable - self.providerSupport = includeProviderSupport ? .renameHint(Self.self) : nil - type = .deletion - } + let providerSupport: LegacyProviderSupport? } diff --git a/Sources/ApodiniMigratorCompare/Change/Changes/UnsupportedChange.swift b/Sources/ApodiniMigratorCompare/LegacyChangeModel/Changes/LegacyUnsupportedChange.swift similarity index 59% rename from Sources/ApodiniMigratorCompare/Change/Changes/UnsupportedChange.swift rename to Sources/ApodiniMigratorCompare/LegacyChangeModel/Changes/LegacyUnsupportedChange.swift index 839b1f57..5d703034 100644 --- a/Sources/ApodiniMigratorCompare/Change/Changes/UnsupportedChange.swift +++ b/Sources/ApodiniMigratorCompare/LegacyChangeModel/Changes/LegacyUnsupportedChange.swift @@ -10,24 +10,15 @@ import Foundation /// Represents an unsupported change from `ApodiniMigrator`, /// E.g. a type changes from an `enum` to an `object` or vice versa -public struct UnsupportedChange: Change { +struct LegacyUnsupportedChange: LegacyChange { /// Top-level changed element related to the change - public let element: ChangeElement + let element: LegacyChangeElement /// Type of the change, always `.unsupported` - public let type: ChangeType + let type: LegacyChangeType /// A textual description of the reason - public let description: String + let description: String /// Indicates whether the change is non-backward compatible, always `true` - public let breaking: Bool + let breaking: Bool /// Indicates whether the change can be handled by `ApodiniMigrator`, always `false` - public let solvable: Bool - - /// Initializer for an unsupported change - init(element: ChangeElement, description: String) { - self.element = element - self.breaking = true - self.solvable = false - self.description = description - type = .unsupported - } + let solvable: Bool } diff --git a/Sources/ApodiniMigratorCompare/LegacyChangeModel/Changes/LegacyUpdateChange.swift b/Sources/ApodiniMigratorCompare/LegacyChangeModel/Changes/LegacyUpdateChange.swift new file mode 100644 index 00000000..88a3328c --- /dev/null +++ b/Sources/ApodiniMigratorCompare/LegacyChangeModel/Changes/LegacyUpdateChange.swift @@ -0,0 +1,68 @@ +// +// This source file is part of the Apodini open source project +// +// SPDX-FileCopyrightText: 2019-2021 Paul Schmiedmayer and the Apodini project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + +import Foundation + +public enum ParameterChangeTarget: String, Decodable { + case necessity + case kind + case typeInformation = "type" +} + +/// Represents an update change of an arbitrary element from some old value to some new value, +/// the most frequent change that can appear in the Migration guide. Depending on the change element +/// and the target, the type of an update change can either be a generic `.update` or a `.rename`, `.propertyChange`, `.parameterChange` or `.responseChange`, +/// which can be initialized through different initializers +struct LegacyUpdateChange: LegacyChange { + // MARK: Private Inner Types + enum CodingKeys: String, CodingKey { + case element + case type = "change-type" + case parameterTarget = "parameter-target" + case targetID = "target-id" + case from + case to + case similarity = "similarity-score" + case necessityValue = "necessity-value" + case convertFromTo = "convert-from-to-script-id" + case convertToFrom = "convert-to-from-script-id" + case convertionWarning = "convertion-warning" + case breaking + case solvable + case providerSupport = "provider-support" + } + + /// Top-level changed element related to the change + let element: LegacyChangeElement + /// Type of change, can either be a generic `.update` or a `.rename`, `.propertyChange`, `.parameterChange` or `.responseChange` + let type: LegacyChangeType + /// Old value of the target + let from: LegacyChangeValue + /// New value of the target + let to: LegacyChangeValue + /// Similarity score from 0 to 1 for renaming + let similarity: Double? + /// Optional id of the target + let targetID: DeltaIdentifier? + /// A json id in case that the necessity of a property or a parameter changed + let necessityValue: LegacyChangeValue? + /// JS convert function to convert old type to new type + let convertFromTo: Int? + /// JS convert function to convert new type to old type, e.g. if the change element is an object and the target is property + let convertToFrom: Int? + /// Warning regarding the provided convert scripts + let convertionWarning: String? + /// The target of the parameter which is related to the change if type is a `parameterChange` + let parameterTarget: ParameterChangeTarget? + /// Indicates whether the change is non-backward compatible + let breaking: Bool + /// Indicates whether the change can be handled by `ApodiniMigrator` + let solvable: Bool + /// Provider support field if change type is a rename and `compare-config` of the Migration Guide is set to `true` for `include-provider-support` + let providerSupport: LegacyProviderSupport? +} diff --git a/Sources/ApodiniMigratorCompare/Change/Change.swift b/Sources/ApodiniMigratorCompare/LegacyChangeModel/LegacyChange.swift similarity index 53% rename from Sources/ApodiniMigratorCompare/Change/Change.swift rename to Sources/ApodiniMigratorCompare/LegacyChangeModel/LegacyChange.swift index 0f7df98d..23b68886 100644 --- a/Sources/ApodiniMigratorCompare/Change/Change.swift +++ b/Sources/ApodiniMigratorCompare/LegacyChangeModel/LegacyChange.swift @@ -9,28 +9,13 @@ import Foundation /// A protocol that represents a change that can appear in the Migration Guide -public protocol Change: Codable { +protocol LegacyChange: Decodable { /// Top-level changed element related to the change - var element: ChangeElement { get } + var element: LegacyChangeElement { get } /// Type of change - var type: ChangeType { get } + var type: LegacyChangeType { get } /// Indicates whether the change is non-backward compatible var breaking: Bool { get } /// Indicates whether the change can be handled by `ApodiniMigrator` var solvable: Bool { get } } - -// MARK: - Change default implementation -public extension Change { - /// Element ID of `element` - var elementID: DeltaIdentifier { element.deltaIdentifier } -} - - -// MARK: - Array -public extension Array where Element == Change { - /// Returns all changes of a `DeltaIdentifiable` instance - func of(_ deltaIdentifiable: D) -> [Change] { - filter { $0.elementID == deltaIdentifiable.deltaIdentifier } - } -} diff --git a/Sources/ApodiniMigratorCompare/LegacyChangeModel/LegacyChangeArray.swift b/Sources/ApodiniMigratorCompare/LegacyChangeModel/LegacyChangeArray.swift new file mode 100644 index 00000000..1dce7c72 --- /dev/null +++ b/Sources/ApodiniMigratorCompare/LegacyChangeModel/LegacyChangeArray.swift @@ -0,0 +1,808 @@ +// +// This source file is part of the Apodini open source project +// +// SPDX-FileCopyrightText: 2019-2021 Paul Schmiedmayer and the Apodini project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + +// swiftlint:disable file_length line_length + +// swiftlint:disable:next type_body_length +struct LegacyChangeArray: Decodable { + enum MigrationError: Error { + case unknownChangeType(message: String, path: [CodingKey]) + case malformedLegacyMigrationGuide(message: String) + case unexpectedState(message: String) + case unsupported(message: String) + } + + // swiftlint:disable:next identifier_name + private static let rootTypeUnsupportedChangeDescriptionSuffix = "Change from enum to object or vice versa is currently not supported" + // swiftlint:disable:next identifier_name + private static let rawValueTypeUnsupportedChangeDescriptionPrefix = "The raw value type of this enum has changed to" + + private var addChanges: [LegacyAddChange] = [] + private var deleteChanges: [LegacyDeleteChange] = [] + private var updateChanges: [LegacyUpdateChange] = [] + private var unsupportedChanges: [LegacyUnsupportedChange] = [] + + init(from decoder: Decoder) throws { + var container = try decoder.unkeyedContainer() + + var iterations = 0 + + while !container.isAtEnd { + if let value = try? container.decode(LegacyAddChange.self) { + addChanges.append(value) + } else if let value = try? container.decode(LegacyDeleteChange.self) { + deleteChanges.append(value) + } else if let value = try? container.decode(LegacyUpdateChange.self) { + updateChanges.append(value) + } else if let value = try? container.decode(LegacyUnsupportedChange.self) { + unsupportedChanges.append(value) + } else { + throw MigrationError.unknownChangeType(message: "Encountered unknown change type after \(iterations) iterations", path: decoder.codingPath) + } + iterations += 1 + } + } + + // swiftlint:disable:next function_body_length cyclomatic_complexity + func migrate( + serviceChanges: inout [ServiceInformationChange], + modelChanges: inout [ModelChange], + endpointChanges: inout [EndpointChange] + ) throws { + var changedEncoderConfiguration: (from: EncoderConfiguration, to: EncoderConfiguration)? + var changedDecoderConfiguration: (from: DecoderConfiguration, to: DecoderConfiguration)? + + for change in addChanges { + precondition(change.type == .addition) + switch change.element { + case let .endpoint(id, target): + switch target { + case .`self`: + guard case .none = change.defaultValue else { + throw MigrationError.malformedLegacyMigrationGuide(message: "Expecting .none defaultValue for endpoint!") + } + + guard case let .element(anyCodable) = change.added, + let endpoint = anyCodable.tryTyped(Endpoint.self) else { + throw MigrationError.malformedLegacyMigrationGuide(message: "Expecting .element with Endpoint for added endpoint.") + } + + endpointChanges.append(.addition( + id: id, + added: endpoint, + defaultValue: nil, + breaking: change.breaking, + solvable: change.solvable + )) + case .queryParameter, .pathParameter, .contentParameter: + let defaultValue: Int? + if case let .json(migrationId) = change.defaultValue { + defaultValue = migrationId + } else { + defaultValue = nil + } + + guard case let .element(anyCodable) = change.added, + let parameter = anyCodable.tryTyped(Parameter.self) else { + throw MigrationError.malformedLegacyMigrationGuide(message: "Expecting .element with Parameter for added parameter.") + } + + endpointChanges.append(.update( + id: id, + updated: .parameter(parameter: .addition( + id: parameter.deltaIdentifier, + added: parameter, + defaultValue: defaultValue, + breaking: change.breaking, + solvable: change.solvable + )), + breaking: change.breaking, + solvable: change.solvable + )) + default: + throw MigrationError.unexpectedState(message: "Didn't expect change target \(target) for endpoint addition change.") + } + case let .enum(id, target): + switch target { + case .`self`: + modelChanges.append(try migrateModelAdditionChange(change: change)) + case .case: + guard case .none = change.defaultValue else { + throw MigrationError.malformedLegacyMigrationGuide(message: "Expecting .none defaultValue for enum case.") + } + + guard case let .element(anyCodable) = change.added, + let enumCase = anyCodable.tryTyped(EnumCase.self) else { + throw MigrationError.malformedLegacyMigrationGuide(message: "Expecting .element with EnumCase for added case!") + } + + modelChanges.append(.update( + id: id, + updated: .case(case: .addition( + id: enumCase.deltaIdentifier, + added: enumCase, + defaultValue: nil, + breaking: change.breaking, + solvable: change.solvable + )), + breaking: change.breaking, + solvable: change.solvable + )) + default: + throw MigrationError.unexpectedState(message: "Didn't expect change target \(target) for enum addition change.") + } + case let .object(id, target): + switch target { + case.`self`: + modelChanges.append(try migrateModelAdditionChange(change: change)) + case .property: + let defaultValue: Int? + if case let .json(migrationId) = change.defaultValue { + defaultValue = migrationId + } else { + defaultValue = nil + } + + guard case let .element(anyCodable) = change.added, + let property = anyCodable.tryTyped(TypeProperty.self) else { + throw MigrationError.malformedLegacyMigrationGuide(message: "Expecting .element with TypeProperty for added property!") + } + + modelChanges.append(.update( + id: id, + updated: .property(property: .addition( + id: property.deltaIdentifier, + added: property, + defaultValue: defaultValue, + breaking: change.breaking, + solvable: change.solvable + )), + breaking: change.breaking, + solvable: change.solvable + )) + default: + throw MigrationError.unexpectedState(message: "Didn't expect change target \(target) for object addition change.") + } + case .networking: + throw MigrationError.unexpectedState(message: "Legacy Networking changes cannot be addition change") + } + } + + for change in deleteChanges { + precondition(change.type == .deletion) + switch change.element { + case let .endpoint(id, target): + switch target { + case .`self`: + guard case .none = change.fallbackValue else { + throw MigrationError.malformedLegacyMigrationGuide(message: "Expecting .none fallbackValue for endpoint!") + } + + guard case let .elementID(endpointId) = change.deleted else { + throw MigrationError.malformedLegacyMigrationGuide(message: "Expecting .elementID for removed endpoint!") + } + guard endpointId == id else { + throw MigrationError.unexpectedState(message: "Reached illegal state for removed endpoint. Non-matching ids!") + } + + endpointChanges.append(.removal( + id: id, + removed: nil, + fallbackValue: nil, + breaking: change.breaking, + solvable: change.solvable + )) + case .queryParameter, .pathParameter, .contentParameter: + guard case .none = change.fallbackValue else { + throw MigrationError.malformedLegacyMigrationGuide(message: "Expecting .none fallbackValue for parameter.") + } + + guard case let .elementID(parameterId) = change.deleted else { + throw MigrationError.malformedLegacyMigrationGuide(message: "Expecting .elementID for removed parameter.") + } + + endpointChanges.append(.update( + id: id, + updated: .parameter(parameter: .removal( + id: parameterId, + removed: nil, + fallbackValue: nil, + breaking: change.breaking, + solvable: change.solvable + )), + breaking: change.breaking, + solvable: change.solvable + )) + default: + throw MigrationError.unexpectedState(message: "Didn't expect change target \(target) for endpoint removal change.") + } + case let .enum(id, target): + switch target { + case .`self`: + modelChanges.append(try migrateModelRemovalChange(change: change)) + case .case: + guard case .none = change.fallbackValue else { + throw MigrationError.malformedLegacyMigrationGuide(message: "Expecting .none fallbackValue for enum case.") + } + + guard case let .elementID(enumCaseId) = change.deleted else { + throw MigrationError.malformedLegacyMigrationGuide(message: "Expecting .elementID for removed case!") + } + + modelChanges.append(.update( + id: id, + updated: .case(case: .removal( + id: enumCaseId, + removed: nil, + fallbackValue: nil, + breaking: change.breaking, + solvable: change.solvable + )), + breaking: change.breaking, + solvable: change.solvable + )) + default: + throw MigrationError.unexpectedState(message: "Didn't expect change target \(target) for enum removal change.") + } + case let .object(id, target): + switch target { + case .`self`: + modelChanges.append(try migrateModelRemovalChange(change: change)) + case .property: + let fallbackValue: Int? + if case let .json(migrationId) = change.fallbackValue { + fallbackValue = migrationId + } else { + fallbackValue = nil + } + + guard case let .elementID(parameterId) = change.deleted else { + throw MigrationError.malformedLegacyMigrationGuide(message: "Expecting .elementID for removed property!") + } + + modelChanges.append(.update( + id: id, + updated: .property(property: .removal( + id: parameterId, + removed: nil, + fallbackValue: fallbackValue, + breaking: change.breaking, + solvable: change.solvable + )), + breaking: change.breaking, + solvable: change.solvable + )) + default: + throw MigrationError.unexpectedState(message: "Didn't expect change target \(target) for object removal change.") + } + case .networking: + throw MigrationError.unexpectedState(message: "Legacy Networking changes cannot be removal change") + } + } + + for change in updateChanges { + switch change.element { + case let .endpoint(id, target): + switch target { + case .deltaIdentifier: + guard case .rename = change.type else { + throw MigrationError.malformedLegacyMigrationGuide(message: "Didn't expect \(change.type) for endpoint change.") + } + + guard case let .stringValue(fromName) = change.from, + case let .stringValue(toName) = change.to else { + throw MigrationError.malformedLegacyMigrationGuide(message: "Endpoint change value must be .stringValue!") + } + + guard fromName == id.rawValue else { + throw MigrationError.unexpectedState(message: "Reached illegal state for updated endpoint. Non-matching ids!") + } + + endpointChanges.append(.idChange( + from: id, + to: DeltaIdentifier(toName), + similarity: change.similarity, + breaking: change.breaking, + solvable: change.solvable + )) + case .resourcePath: + guard case .update = change.type else { + throw MigrationError.malformedLegacyMigrationGuide(message: "Didn't expect \(change.type) for resource path change.") + } + + guard case let .element(fromCodable) = change.from, + case let .element(toCodable) = change.to, + let fromPath = fromCodable.tryTyped(EndpointPath.self), + let toPath = toCodable.tryTyped(EndpointPath.self) else { + throw MigrationError.malformedLegacyMigrationGuide(message: "Expected .element EndpointPath for resource path change.") + } + + endpointChanges.append(.update( + id: id, + updated: .identifier(identifier: .update( + id: DeltaIdentifier(EndpointPath.identifierType), + updated: .init( + from: AnyEndpointIdentifier(from: fromPath), + to: AnyEndpointIdentifier(from: toPath) + ), + breaking: change.breaking, + solvable: change.solvable + )), + breaking: change.breaking, + solvable: change.solvable + )) + case .operation: + guard case .update = change.type else { + throw MigrationError.malformedLegacyMigrationGuide(message: "Didn't expect \(change.type) for operation change.") + } + + guard case let .element(fromCodable) = change.from, + case let .element(toCodable) = change.to, + let fromOperation = fromCodable.tryTyped(Operation.self), + let toOperation = toCodable.tryTyped(Operation.self) else { + throw MigrationError.malformedLegacyMigrationGuide(message: "Expected .element Operation for operation change.") + } + + endpointChanges.append(.update( + id: id, + updated: .identifier(identifier: .update( + id: DeltaIdentifier(Operation.identifierType), + updated: .init( + from: AnyEndpointIdentifier(from: fromOperation), + to: AnyEndpointIdentifier(from: toOperation) + ), + breaking: change.breaking, + solvable: change.solvable + )), + breaking: change.breaking, + solvable: change.solvable + )) + case .response: + guard case .responseChange = change.type else { + throw MigrationError.malformedLegacyMigrationGuide(message: "Didn't expect \(change.type) for response change.") + } + + guard case let .element(fromCodable) = change.from, + case let .element(toCodable) = change.to, + let fromResponse = fromCodable.tryTyped(TypeInformation.self), + let toResponse = toCodable.tryTyped(TypeInformation.self) else { + throw MigrationError.malformedLegacyMigrationGuide(message: "Expected .element TypeInformation for response change.") + } + + guard let convertToFrom = change.convertToFrom else { + throw MigrationError.malformedLegacyMigrationGuide(message: "Response change expects type migrations scripts in backward directions!") + } + + endpointChanges.append(.update( + id: id, + updated: .response( + from: fromResponse, + to: toResponse, + backwardsMigration: convertToFrom, + migrationWarning: change.convertionWarning + ), + breaking: change.breaking, + solvable: change.solvable + )) + case .queryParameter, .pathParameter, .contentParameter: + switch change.type { + case .rename: + guard case let .stringValue(fromName) = change.from, + case let .stringValue(toName) = change.to else { + throw MigrationError.malformedLegacyMigrationGuide(message: "Endpoint parameter change value must be .stringValue!") + } + + endpointChanges.append(.update( + id: id, + updated: .parameter(parameter: .idChange( + from: DeltaIdentifier(fromName), + to: DeltaIdentifier(toName), + similarity: change.similarity, + breaking: change.breaking, + solvable: change.solvable + )), + breaking: change.breaking, + solvable: change.solvable + )) + case .parameterChange: + guard let parameterTarget = change.parameterTarget else { + throw MigrationError.unexpectedState(message: "ParameterTarget is required for a parameter change.") + } + + guard case let .element(fromCodable) = change.from, + case let .element(toCodable) = change.to else { + throw MigrationError.malformedLegacyMigrationGuide(message: "Expected .element for parameter change.") + } + + guard let parameterId = change.targetID else { + throw MigrationError.malformedLegacyMigrationGuide(message: "Expected targetID field with parameter id.") + } + + let parameterUpdateChange: ParameterUpdateChange + + switch parameterTarget { + case .kind: + guard let fromType = fromCodable.tryTyped(ParameterType.self), + let toType = toCodable.tryTyped(ParameterType.self) else { + throw MigrationError.malformedLegacyMigrationGuide(message: "Expected ParameterType change value for kind change.") + } + + parameterUpdateChange = .parameterType(from: fromType, to: toType) + case .necessity: + guard let fromNecessity = fromCodable.tryTyped(Necessity.self), + let toNecessity = toCodable.tryTyped(Necessity.self) else { + throw MigrationError.malformedLegacyMigrationGuide(message: "Expected Necessity change value for necessity change.") + } + + guard case let .json(migrationId) = change.necessityValue else { + throw MigrationError.malformedLegacyMigrationGuide(message: "Expected .json for necessityValue for necessity change.") + } + + parameterUpdateChange = .necessity(from: fromNecessity, to: toNecessity, necessityMigration: migrationId) + case .typeInformation: + guard let fromType = fromCodable.tryTyped(TypeInformation.self), + let toType = toCodable.tryTyped(TypeInformation.self) else { + throw MigrationError.malformedLegacyMigrationGuide(message: "Expected TypeInformation change value for parameter type change.") + } + + guard let convertFromTo = change.convertFromTo else { + throw MigrationError.malformedLegacyMigrationGuide(message: "Expected convertFromTo for parameter type change.") + } + + parameterUpdateChange = .type( + from: fromType, + to: toType, + forwardMigration: convertFromTo, + conversionWarning: change.convertionWarning + ) + } + + endpointChanges.append(.update( + id: id, + updated: .parameter(parameter: .update( + id: parameterId, + updated: parameterUpdateChange, + breaking: change.breaking, + solvable: change.solvable + )), + breaking: change.breaking, + solvable: change.solvable + )) + default: + throw MigrationError.unexpectedState(message: "Didn't expect \(change.type) type for parameter change!") + } + default: + throw MigrationError.unexpectedState(message: "Didn't expect change target \(target) for enum update change.") + } + case let .enum(id, target): + switch target { + case .typeName: + modelChanges.append(try migrateModelUpdateTypeNameChange(change: change)) + case .case: + guard case .rename = change.type else { + throw MigrationError.malformedLegacyMigrationGuide(message: "Didn't expect \(change.type) for enum case change.") + } + + guard case let .stringValue(fromCaseName) = change.from, + case let .stringValue(toCaseName) = change.to else { + throw MigrationError.malformedLegacyMigrationGuide(message: "Enum case change value must be .stringValue!") + } + + modelChanges.append(.update( + id: id, + updated: .case(case: .idChange( + from: DeltaIdentifier(fromCaseName), + to: DeltaIdentifier(toCaseName), + similarity: change.similarity, + breaking: change.breaking, + solvable: change.solvable + )), + breaking: change.breaking, + solvable: change.solvable + )) + case .caseRawValue: + guard case .update = change.type else { + throw MigrationError.malformedLegacyMigrationGuide(message: "Didn't expect \(change.type) for enum case rawValue change.") + } + + guard case let .element(fromCodable) = change.from, + case let .element(toCodable) = change.to, + let fromCase = fromCodable.tryTyped(EnumCase.self), + let toCase = toCodable.tryTyped(EnumCase.self) else { + throw MigrationError.malformedLegacyMigrationGuide(message: "Expected .element EnumCase for .caseRawValue change.") + } + + modelChanges.append(.update( + id: id, + updated: .case(case: .update( + id: fromCase.deltaIdentifier, + updated: .rawValue(from: fromCase.rawValue, to: toCase.rawValue), + breaking: change.breaking, + solvable: change.solvable + )), + breaking: change.breaking, + solvable: change.solvable + )) + default: + throw MigrationError.unexpectedState(message: "Didn't expect change target \(target) for enum update change.") + } + case let .object(id, target): + switch target { + case .typeName: + modelChanges.append(try migrateModelUpdateTypeNameChange(change: change)) + case .property: + switch change.type { + case .rename: + guard case let .stringValue(fromPropertyName) = change.from, + case let .stringValue(toPropertyName) = change.to else { + throw MigrationError.malformedLegacyMigrationGuide(message: "Property change value must be .stringValue!") + } + + modelChanges.append(.update( + id: id, + updated: .property(property: .idChange( + from: DeltaIdentifier(fromPropertyName), + to: DeltaIdentifier(toPropertyName), + similarity: change.similarity, + breaking: change.breaking, + solvable: change.solvable + )), + breaking: change.breaking, + solvable: change.solvable + )) + case .propertyChange: + guard case let .element(fromCodable) = change.from, + case let .element(toCodable) = change.to, + let fromType = fromCodable.tryTyped(TypeInformation.self), + let toType = toCodable.tryTyped(TypeInformation.self) else { + throw MigrationError.malformedLegacyMigrationGuide(message: "Property necessity value must be .element of TypeInformation!") + } + + guard let parameterId = change.targetID else { + throw MigrationError.malformedLegacyMigrationGuide(message: "Property change expects targetID!") + } + + guard let convertFromTo = change.convertFromTo, + let convertToFrom = change.convertToFrom else { + throw MigrationError.malformedLegacyMigrationGuide(message: "Property change expects type migrations scripts in both directions!") + } + + modelChanges.append(.update( + id: id, + updated: .property(property: .update( + id: parameterId, + updated: .type( + from: fromType, + to: toType, + forwardMigration: convertFromTo, + backwardMigration: convertToFrom, + conversionWarning: change.convertionWarning + ), + breaking: change.breaking, + solvable: change.solvable + )), + breaking: change.breaking, + solvable: change.solvable + )) + default: + throw MigrationError.unexpectedState(message: "Didn't expect \(change.type) type for property change!") + } + case .necessity: + switch change.type { + case .update: + guard case let .element(fromCodable) = change.from, + case let .element(toCodable) = change.to, + let fromNecessity = fromCodable.tryTyped(Necessity.self), + let toNecessity = toCodable.tryTyped(Necessity.self) else { + throw MigrationError.malformedLegacyMigrationGuide(message: "Property necessity value must be .element of Necessity!") + } + + guard case let .json(necessityMigration) = change.necessityValue else { + throw MigrationError.malformedLegacyMigrationGuide(message: "Property necessity change expects .json necessity migration id!") + } + + guard let parameterId = change.targetID else { + throw MigrationError.malformedLegacyMigrationGuide(message: "Property necessity change expects targetID!") + } + + modelChanges.append(.update( + id: id, + updated: .property(property: .update( + id: parameterId, + updated: .necessity(from: fromNecessity, to: toNecessity, necessityMigration: necessityMigration), + breaking: change.breaking, + solvable: change.solvable + )), + breaking: change.breaking, + solvable: change.solvable + )) + default: + throw MigrationError.unexpectedState(message: "Didn't expect \(change.type) for property necessity.") + } + default: + throw MigrationError.unexpectedState(message: "Didn't expect change target \(target) for object update change.") + } + case let .networking(target): + guard case .update = change.type else { + throw MigrationError.malformedLegacyMigrationGuide(message: "Didn't expect \(change.type) for networking change.") + } + + switch target { + case .serverPath: + guard case let .stringValue(fromServerPath) = change.from, + case let .stringValue(toServerPath) = change.to else { + throw MigrationError.malformedLegacyMigrationGuide(message: "Network change path value must be .stringValue!") + } + + serviceChanges.append(.update( + id: ServiceInformation.deltaIdentifier, + updated: .http( + from: try HTTPInformation(fromLegacyServerPath: fromServerPath), + to: try HTTPInformation(fromLegacyServerPath: toServerPath) + ), + breaking: change.breaking, + solvable: change.solvable + )) + case .encoderConfiguration: + guard case let .element(fromCodable) = change.from, + case let .element(toCodable) = change.to, + let fromEncoder = fromCodable.tryTyped(EncoderConfiguration.self), + let toEncoder = toCodable.tryTyped(EncoderConfiguration.self) else { + throw MigrationError.malformedLegacyMigrationGuide(message: "Network change encoder value must be .element!") + } + + changedEncoderConfiguration = (fromEncoder, toEncoder) + case .decoderConfiguration: + guard case let .element(fromCodable) = change.from, + case let .element(toCodable) = change.to, + let fromDecoder = fromCodable.tryTyped(DecoderConfiguration.self), + let toDecoder = toCodable.tryTyped(DecoderConfiguration.self) else { + throw MigrationError.malformedLegacyMigrationGuide(message: "Network change decoder value must be .element!") + } + + changedDecoderConfiguration = (fromDecoder, toDecoder) + } + } + } + + for change in unsupportedChanges { + precondition(change.type == .unsupported) + + switch change.element { + case let .enum(id, target): + switch target { + case .`self`: + if change.description.hasSuffix(Self.rootTypeUnsupportedChangeDescriptionSuffix) { + modelChanges.append(.update( + id: id, + updated: .rootType(from: .enum, to: .object, newModel: TypeInformation.reference("UNSUPPORTED")), + breaking: change.breaking, + solvable: change.solvable + )) + } else if change.description.hasPrefix(Self.rawValueTypeUnsupportedChangeDescriptionPrefix) { + modelChanges.append(.update( + id: id, + updated: .rawValueType(from: .reference("UNSUPPORTED0"), to: .reference("UNSUPPORTED1")), + breaking: change.breaking, + solvable: change.solvable + )) + } else { + throw MigrationError.unexpectedState(message: "Encountered unknown enum unsupported change: \(change.description)") + } + default: + throw MigrationError.unexpectedState(message: "Encountered unknown unsupported change for enum target \(target): \(change.description)") + } + case let .object(id, target): + switch target { + case .`self`: + if change.description.hasSuffix(Self.rootTypeUnsupportedChangeDescriptionSuffix) { + modelChanges.append(.update( + id: id, + updated: .rootType(from: .object, to: .enum, newModel: TypeInformation.reference("UNSUPPORTED")), + breaking: change.breaking, + solvable: change.solvable + )) + } else { + throw MigrationError.unexpectedState(message: "Encountered unknown object unsupported change: \(change.description)") + } + default: + throw MigrationError.unexpectedState(message: "Encountered unknown unsupported change for object target \(target): \(change.description)") + } + default: + throw MigrationError.unexpectedState(message: "Encountered unknown unsupported change for element \(change.element): \(change.description)") + } + } + + if changedEncoderConfiguration != nil || changedDecoderConfiguration != nil { + // we can't properly reconstruct everything if it wasn't changed :// + // we do best effort here + let from = AnyExporterConfiguration(RESTExporterConfiguration( + encoderConfiguration: changedEncoderConfiguration?.from ?? .default, + decoderConfiguration: changedDecoderConfiguration?.from ?? .default + )) + let to = AnyExporterConfiguration(RESTExporterConfiguration( + encoderConfiguration: changedEncoderConfiguration?.to ?? .default, + decoderConfiguration: changedDecoderConfiguration?.to ?? .default + )) + + serviceChanges.append(.update( + id: ServiceInformation.deltaIdentifier, + updated: .exporter(exporter: .update( + id: from.deltaIdentifier, + updated: .init(from: from, to: to), + breaking: true, + solvable: true + )), + breaking: true, + solvable: true + )) + } + } + + private func migrateModelAdditionChange(change: LegacyAddChange) throws -> ModelChange { + guard case .none = change.defaultValue else { + throw MigrationError.malformedLegacyMigrationGuide(message: "Expecting .none defaultValue for model!") + } + + guard case let .element(anyCodable) = change.added, + let modelWithReferencedProperties = anyCodable.tryTyped(TypeInformation.self) else { + throw MigrationError.malformedLegacyMigrationGuide(message: "Expecting .element with TypeInformation for added model!") + } + + return .addition( + id: change.element.deltaIdentifier, + added: modelWithReferencedProperties, + defaultValue: nil, + breaking: change.breaking, + solvable: change.solvable + ) + } + + private func migrateModelRemovalChange(change: LegacyDeleteChange) throws -> ModelChange { + guard case .none = change.fallbackValue else { + throw MigrationError.malformedLegacyMigrationGuide(message: "Expecting .none fallbackValue for model!") + } + + guard case let .elementID(id) = change.deleted else { + throw MigrationError.malformedLegacyMigrationGuide(message: "Expecting .elementID for removed model!") + } + guard id == change.element.deltaIdentifier else { + throw MigrationError.unexpectedState(message: "Reached illegal state for removed model. Non-matching ids!") + } + + return .removal( + id: id, + removed: nil, + fallbackValue: nil, + breaking: change.breaking, + solvable: change.solvable + ) + } + + private func migrateModelUpdateTypeNameChange(change: LegacyUpdateChange) throws -> ModelChange { + guard case .rename = change.type else { + throw MigrationError.malformedLegacyMigrationGuide(message: "Didn't expect \(change.type) for type rename change") + } + + guard case let .stringValue(fromTypeName) = change.from, + case let .stringValue(toTypeName) = change.to else { + throw MigrationError.malformedLegacyMigrationGuide(message: "UpdateChange model .typeName values must be .stringValue!") + } + guard fromTypeName == change.element.deltaIdentifier.rawValue else { + throw MigrationError.unexpectedState(message: "Reached illegal state for update type name of model. Non-matching identifiers!") + } + + return .idChange( + from: change.element.deltaIdentifier, + to: DeltaIdentifier(toTypeName), + similarity: change.similarity, + breaking: change.breaking, + solvable: change.solvable + ) + } +} diff --git a/Sources/ApodiniMigratorCompare/LegacyChangeModel/LegacyChangeElement.swift b/Sources/ApodiniMigratorCompare/LegacyChangeModel/LegacyChangeElement.swift new file mode 100644 index 00000000..d5c79dc0 --- /dev/null +++ b/Sources/ApodiniMigratorCompare/LegacyChangeModel/LegacyChangeElement.swift @@ -0,0 +1,66 @@ +// +// This source file is part of the Apodini open source project +// +// SPDX-FileCopyrightText: 2019-2021 Paul Schmiedmayer and the Apodini project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + +import Foundation +import ApodiniMigratorCore + +/// Represents distinct top-level elements that are subject to change in the web service +enum LegacyChangeElement: Decodable { + // MARK: Private Inner Types + private enum CodingKeys: String, CodingKey { + case endpoint, `enum`, object, networking, target + } + + /// Represents an endpoint change element identified by its id and the corresponding endpoint change target + case endpoint(DeltaIdentifier, target: LegacyEndpointTarget) + + /// Represents an enum change element identified by its id and the corresponding enum change target + case `enum`(DeltaIdentifier, target: LegacyEnumTarget) + + /// Represents an object change element identified by its id and the corresponding object change target + case object(DeltaIdentifier, target: LegacyObjectTarget) + + /// Represents an networking change element and the corresponding networking change target + /// - Note: Networking change element always have `DeltaIdentifier("NetworkingService")` as id + case networking(target: LegacyNetworkingTarget) + + /// Creates a new instance by decoding from the given decoder + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + let keys = container.allKeys + + if keys.contains(.endpoint) { + let target = try container.decode(LegacyEndpointTarget.self, forKey: .target) + self = .endpoint(try container.decode(DeltaIdentifier.self, forKey: .endpoint), target: target) + } else if keys.contains(.enum) { + let target = try container.decode(LegacyEnumTarget.self, forKey: .target) + self = .enum(try container.decode(DeltaIdentifier.self, forKey: .enum), target: target) + } else if keys.contains(.object) { + let target = try container.decode(LegacyObjectTarget.self, forKey: .target) + self = .object(try container.decode(DeltaIdentifier.self, forKey: .object), target: target) + } else if keys.contains(.networking) { + let target = try container.decode(LegacyNetworkingTarget.self, forKey: .target) + self = .networking(target: target) + } else { + throw DecodingError.dataCorrupted(.init(codingPath: keys, debugDescription: "Failed to decode \(Self.self)")) + } + } +} + +// MARK: - ChangeElement +extension LegacyChangeElement { + /// Returns the delta identifier of the change element + var deltaIdentifier: DeltaIdentifier { + switch self { + case let .endpoint(deltaIdentifier, _): return deltaIdentifier + case let .enum(deltaIdentifier, _): return deltaIdentifier + case let .object(deltaIdentifier, _): return deltaIdentifier + case .networking: return "NetworkingService" + } + } +} diff --git a/Sources/ApodiniMigratorCompare/Change/ChangeTargets.swift b/Sources/ApodiniMigratorCompare/LegacyChangeModel/LegacyChangeTargets.swift similarity index 79% rename from Sources/ApodiniMigratorCompare/Change/ChangeTargets.swift rename to Sources/ApodiniMigratorCompare/LegacyChangeModel/LegacyChangeTargets.swift index 3c3aac5d..89e16cb3 100644 --- a/Sources/ApodiniMigratorCompare/Change/ChangeTargets.swift +++ b/Sources/ApodiniMigratorCompare/LegacyChangeModel/LegacyChangeTargets.swift @@ -9,7 +9,7 @@ import Foundation /// Distinct cases of endpoint targets that are subject to change -public enum EndpointTarget: String, Value { +enum LegacyEndpointTarget: String, Decodable { /// Indicates a change that relates to the endpoint itself, e.g. a deleted or added endpoint case `self` /// Indicates a change that relates to the identifier of the endpoint, e.g. updated to some new id @@ -28,19 +28,10 @@ public enum EndpointTarget: String, Value { case errors /// Response target case response - - /// An internal convenience static method to return the corresponding `EndpointTarget` of a parameter - static func target(for parameter: Parameter) -> EndpointTarget { - switch parameter.parameterType { - case .lightweight: return .queryParameter - case .content: return .contentParameter - case .path: return .pathParameter - } - } } /// Distinct cases of object targets that are subject to change -public enum ObjectTarget: String, Value { +enum LegacyObjectTarget: String, Decodable { /// Indicates a change that relates to the object itself, e.g. a deleted or added object case `self` /// TypeName target @@ -52,7 +43,7 @@ public enum ObjectTarget: String, Value { } /// Distinct cases of enum targets that are subject to change -public enum EnumTarget: String, Value { +enum LegacyEnumTarget: String, Decodable { /// Indicates a change that relates to the enum itself, e.g. a deleted or added enum case `self` /// TypeName target @@ -66,7 +57,7 @@ public enum EnumTarget: String, Value { } /// Distinct cases of networking service targets that are subject to change -public enum NetworkingTarget: String, Value { +enum LegacyNetworkingTarget: String, Decodable { /// Server path target, including the version path component case serverPath = "base-url" /// Encoder configuration target diff --git a/Sources/ApodiniMigratorCompare/Change/ChangeType.swift b/Sources/ApodiniMigratorCompare/LegacyChangeModel/LegacyChangeType.swift similarity index 95% rename from Sources/ApodiniMigratorCompare/Change/ChangeType.swift rename to Sources/ApodiniMigratorCompare/LegacyChangeModel/LegacyChangeType.swift index 3e8196e3..e1041b8e 100644 --- a/Sources/ApodiniMigratorCompare/Change/ChangeType.swift +++ b/Sources/ApodiniMigratorCompare/LegacyChangeModel/LegacyChangeType.swift @@ -9,7 +9,7 @@ import Foundation /// Distinct cases of change types that can appear in the Migration Guide -public enum ChangeType: String, Value { +enum LegacyChangeType: String, Decodable { /// An AddChange case addition /// A DeleteChange diff --git a/Sources/ApodiniMigratorCompare/Change/ChangeValue.swift b/Sources/ApodiniMigratorCompare/LegacyChangeModel/LegacyChangeValue.swift similarity index 50% rename from Sources/ApodiniMigratorCompare/Change/ChangeValue.swift rename to Sources/ApodiniMigratorCompare/LegacyChangeModel/LegacyChangeValue.swift index 855af5d7..5418e290 100644 --- a/Sources/ApodiniMigratorCompare/Change/ChangeValue.swift +++ b/Sources/ApodiniMigratorCompare/LegacyChangeModel/LegacyChangeValue.swift @@ -9,7 +9,7 @@ import Foundation /// Represents distinct cases of values that can appear in sections of Migration Guide, e.g. as default-values, fallback-values or identifiers -public enum ChangeValue: Value { +enum LegacyChangeValue: Decodable { private enum ChangeValueCodingError: Error { case notNone } @@ -24,17 +24,9 @@ public enum ChangeValue: Value { /// Holds a type-erasured codable element of one of the models of `ApodiniMigrator` that are subject to change case element(AnyCodableElement) - /// An internal convenience method to initialize `.element` case ouf of an `AnyCodableElementValue` - static func element(_ element: A) -> ChangeValue { - .element(element.asAnyCodableElement) - } /// A case where there is no need to provide an element, since the element is part of the old version and can be simply identified based on the `id` case elementID(DeltaIdentifier) - /// An internal convenience method to initialize `.elementID` case ouf of an `DeltaIdentifiable` - static func id(from identifiable: D) -> ChangeValue { - .elementID(identifiable.deltaIdentifier) - } /// A case to hold string values case stringValue(String) @@ -43,62 +35,13 @@ public enum ChangeValue: Value { /// and are subject to change. E.g. for a new added property of type User, the string of this case would be `{ "name": "", "id": 0 }`, /// which can then be decoded in the client library accordingly case json(Int) - /// An internal convenience method to initalize `.json` case from the typeInformation of the type - /// Encoder configuration is passed from the new version in order to encode the default values with the correct encoder configuration - static func value(from typeInformation: TypeInformation, with configuration: EncoderConfiguration, changes: ChangeContextNode) -> ChangeValue { - let jsonValue = JSONValue(JSONStringBuilder.jsonString(typeInformation, with: configuration)) - return .json(changes.store(jsonValue: jsonValue)) - } - - /// Returns the nested string value of the cases, or `nil` if `self` is `.element` - public var value: String? { - switch self { - case .none: return "none" - case let .elementID(id): return id.rawValue - case let .stringValue(string): return string - case let .json(id): return "\(id)" - default: return nil - } - } - - /// Encodes `self` into the given encoder - public func encode(to encoder: Encoder) throws { - if case .none = self, let value = value { - var singleValueContainer = encoder.singleValueContainer() - return try singleValueContainer.encode(value) - } - - var container = encoder.container(keyedBy: CodingKeys.self) - - if case let .element(element) = self { - return try container.encode(element, forKey: .element) - } - - var key: CodingKeys? - switch self { - case .elementID: key = .elementID - case .stringValue: key = .stringValue - case .json: key = .json - default: break - } - - guard let codingKey = key else { - throw EncodingError.invalidValue((), EncodingError.Context(codingPath: [], debugDescription: "\(Self.self) did not encode any value")) - } - - if codingKey == .json { - return try container.encode(Int(value ?? ""), forKey: codingKey) - } - - try container.encode(value, forKey: codingKey) - } /// Creates a new instance by decoding from the given decoder - public init(from decoder: Decoder) throws { + init(from decoder: Decoder) throws { do { let singleValueContainer = try decoder.singleValueContainer() let string = try singleValueContainer.decode(String.self) - if string == ChangeValue.none.value { + if string == "none" { self = .none } else { throw ChangeValueCodingError.notNone diff --git a/Sources/ApodiniMigratorCompare/LegacyChangeModel/LegacyCompareConfiguration.swift b/Sources/ApodiniMigratorCompare/LegacyChangeModel/LegacyCompareConfiguration.swift new file mode 100644 index 00000000..d2fa2f63 --- /dev/null +++ b/Sources/ApodiniMigratorCompare/LegacyChangeModel/LegacyCompareConfiguration.swift @@ -0,0 +1,30 @@ +// +// This source file is part of the Apodini open source project +// +// SPDX-FileCopyrightText: 2019-2021 Paul Schmiedmayer and the Apodini project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + +import Foundation + +struct LegacyCompareConfiguration: Decodable { + private enum CodingKeys: String, CodingKey { + case includeProviderSupport = "include-provider-support" + case allowEndpointIdentifierUpdate = "allowed-endpoint-id-update" + case allowTypeRename = "allowed-type-rename" + } + + let includeProviderSupport: Bool + let allowEndpointIdentifierUpdate: Bool + let allowTypeRename: Bool +} + +extension CompareConfiguration { + init(from configuration: LegacyCompareConfiguration) { + self.includeProviderSupport = configuration.includeProviderSupport + self.allowEndpointIdentifierUpdate = configuration.allowEndpointIdentifierUpdate + self.allowTypeRename = configuration.allowTypeRename + self.encoderConfiguration = .default // best effort + } +} diff --git a/Sources/ApodiniMigratorCompare/LegacyChangeModel/LegacyProviderSupport.swift b/Sources/ApodiniMigratorCompare/LegacyChangeModel/LegacyProviderSupport.swift new file mode 100644 index 00000000..925c30e7 --- /dev/null +++ b/Sources/ApodiniMigratorCompare/LegacyChangeModel/LegacyProviderSupport.swift @@ -0,0 +1,46 @@ +// +// This source file is part of the Apodini open source project +// +// SPDX-FileCopyrightText: 2019-2021 Paul Schmiedmayer and the Apodini project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + +import Foundation + +/// A util object to correct wrong classification of changes in the migration guide. +/// `ProviderSupport` is included as a field in changes of type addition, deletion or rename +struct LegacyProviderSupport: Decodable { + // MARK: Private Inner Types + private enum CodingKeys: String, CodingKey { + case hint, renamedFrom = "renamed-from", renamedTo = "renamed-to", renameIsValid = "rename-is-valid", warning + } + + /// Textual explanation how to adjust the change object + var hint: String + /// Element id if an addi +// +// SPDX-License-Identifier: MIT +// + +import Foundation + +/// This enum describes the document format version of the ``MigrationGuide``. +/// ``MigrationGuideDocumentVersion`` follows the SemVer versioning scheme. +public enum MigrationGuideDocumentVersion: String, Codable { + /// The original/legacy document format introduced with version 0.1.0. + /// - Note: This version is assumed when no `version` field is present in the document root. + /// ApodiniMigrator supports parsing legacy documents till 0.3.0. + case v1 = "1.0.0" + /// The current document format and updated change model introduced with version 0.2.0. + case v2 = "2.0.0" +} + +/// Migration guide +public struct MigrationGuide { + /// A default summary. A free filed to use by providers of MigrationGuides. + static let defaultSummary = "A summary of what changed between versions" + + /// A textual description of the migration guide + public let summary: String + + /// Id of the old document from which the migration guide was generated + public let id: UUID + /// Old version + public let from: Version + /// New version + public let to: Version + private let _compareConfiguration: CompareConfiguration? + /// Configuration used for the Comparators while generating the MigrationGuide. + public var compareConfiguration: CompareConfiguration { + self._compareConfiguration ?? .default + } + + /// Captures any changes happening to the `ServiceInformation`, describing the web service. + public let serviceChanges: [ServiceInformationChange] + /// Captures any changes done to web service models. + public let modelChanges: [ModelChange] + /// Captures any changes done to web service endpoints. + public let endpointChanges: [EndpointChange] + + + /// Dictionary holding all registered convert scripts which are referenced from change objects + public let scripts: [Int: JSScript] + /// Dictionary holding all registered json values which are referenced from change objects + public let jsonValues: [Int: JSONValue] + /// A property that holds json representation of models that had a breaking change on their properties, e.g. rename, addition, deletion or property type change. + /// This property is used for test cases in the client application + public let objectJSONs: [String: JSONValue] + + /// An empty migration guide with no changes + public static func empty(id: UUID = UUID()) -> MigrationGuide { + .init( + summary: Self.defaultSummary, + id: id, + from: .default, + to: .default, + comparisonContext: ChangeComparisonContext(configuration: nil, latestModels: []) + ) + } + + /// Internal initializer of the Migration Guide + init( + summary: String, + id: UUID, + from: Version, + to: Version, + comparisonContext: ChangeComparisonContext + ) { + self.summary = summary + self.id = id + self.from = from + self.to = to + self._compareConfiguration = comparisonContext.configuration + + self.serviceChanges = comparisonContext.serviceChanges + self.modelChanges = comparisonContext.modelChanges + self.endpointChanges = comparisonContext.endpointChanges + + self.scripts = comparisonContext.scripts + self.jsonValues = comparisonContext.jsonValues + self.objectJSONs = comparisonContext.objectJSONs + } + + /// Initializes the Migration Guide out of two Documents. Documents get compared + /// and the changes can be accessed via `changes` of the new Migration Guide instance + public init(for lhs: APIDocument, rhs: APIDocument, compareConfiguration: CompareConfiguration? = nil) { + let documentsComparator = DocumentComparator(configuration: compareConfiguration, lhs: lhs, rhs: rhs) + documentsComparator.compare() + + self.init( + summary: Self.defaultSummary, + id: lhs.id, + from: lhs.serviceInformation.version, + to: rhs.serviceInformation.version, + comparisonContext: documentsComparator.context + ) + } + + /// Returns the migration guide by comparing documents at the specified paths + public static func from( + _ lhsDocumentPath: Path, + _ rhsDocumentPath: Path, + compareConfiguration: CompareConfiguration = .default + ) throws -> MigrationGuide { + .init(for: try .decode(from: lhsDocumentPath), rhs: try .decode(from: rhsDocumentPath), compareConfiguration: compareConfiguration) + } + + /// Returns the migration guide by comparing documents at the specified paths + public static func from( + _ lhsDocumentPath: String, + _ rhsDocumentPath: String, + compareConfiguration: CompareConfiguration = .default + ) throws -> MigrationGuide { + try .from(Path(lhsDocumentPath), Path(rhsDocumentPath), compareConfiguration: compareConfiguration) + } +} + +extension MigrationGuide: Codable { + public enum CodingError: Error { + case unsupportedDocumentVersion(version: String) + } + + private enum CodingKeys: String, CodingKey { + case summary + case id = "document-id" + case documentVersion = "version" + case from + case to + case compareConfiguration = "compare-config" + case serviceChanges + case modelChanges + case endpointChanges + case scripts + case jsonValues = "json-values" + case objectJSONs = "updated-json-representations" + + // legacy fields + case changes + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + let documentVersion: MigrationGuideDocumentVersion + do { + documentVersion = try container.decodeIfPresent(MigrationGuideDocumentVersion.self, forKey: .documentVersion) ?? .v1 + } catch { + // failed to decode APIDocumentVersion, probably because its an unknown version! + throw CodingError.unsupportedDocumentVersion(version: try container.decode(String.self, forKey: .documentVersion)) + } + + // decode fields which are the same in all versions + summary = try container.decode(String.self, forKey: .summary) + id = try container.decode(UUID.self, forKey: .id) + from = try container.decode(Version.self, forKey: .from) + to = try container.decode(Version.self, forKey: .to) + + scripts = try container.decode([Int: JSScript].self, forKey: .scripts) + jsonValues = try container.decode([Int: JSONValue].self, forKey: .jsonValues) + objectJSONs = try container.decode([String: JSONValue].self, forKey: .objectJSONs) + + switch documentVersion { + case .v1: + if let legacyConfiguration = try container.decodeIfPresent(LegacyCompareConfiguration.self, forKey: .compareConfiguration) { + self._compareConfiguration = CompareConfiguration(from: legacyConfiguration) + } else { + self._compareConfiguration = nil + } + + let changes = try container.decode(LegacyChangeArray.self, forKey: .changes) + + var serviceChanges = [ServiceInformationChange]() + var modelChanges = [ModelChange]() + var endpointChanges = [EndpointChange]() + + try changes.migrate(serviceChanges: &serviceChanges, modelChanges: &modelChanges, endpointChanges: &endpointChanges) + + self.serviceChanges = serviceChanges + self.modelChanges = modelChanges + self.endpointChanges = endpointChanges + case .v2: + _compareConfiguration = try container.decodeIfPresent(CompareConfiguration.self, forKey: .compareConfiguration) + + serviceChanges = try container.decodeIfPresentOrInitEmpty([ServiceInformationChange].self, forKey: .serviceChanges) + modelChanges = try container.decodeIfPresentOrInitEmpty([ModelChange].self, forKey: .modelChanges) + endpointChanges = try container.decodeIfPresentOrInitEmpty([EndpointChange].self, forKey: .endpointChanges) + } + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + + try container.encode(summary, forKey: .summary) + try container.encodeIfPresent(id, forKey: .id) + try container.encode(MigrationGuideDocumentVersion.v2, forKey: .documentVersion) + try container.encode(from, forKey: .from) + try container.encode(to, forKey: .to) + try container.encodeIfPresent(compareConfiguration, forKey: .compareConfiguration) + + try container.encodeIfNotEmpty(serviceChanges, forKey: .serviceChanges) + try container.encodeIfNotEmpty(modelChanges, forKey: .modelChanges) + try container.encodeIfNotEmpty(endpointChanges, forKey: .endpointChanges) + + try container.encode(scripts, forKey: .scripts) + try container.encode(jsonValues, forKey: .jsonValues) + try container.encode(objectJSONs, forKey: .objectJSONs) + } +} + +extension MigrationGuide: Equatable { + /// :nodoc: + public static func == (lhs: MigrationGuide, rhs: MigrationGuide) -> Bool {lhs.summary == rhs.summary + && lhs.id == rhs.id + && lhs.from == rhs.from + && lhs.to == rhs.to + && lhs.compareConfiguration == rhs.compareConfiguration + && lhs.serviceChanges.json(prettyPrinted: false) == rhs.serviceChanges.json(prettyPrinted: false) + && lhs.modelChanges.json(prettyPrinted: false) == rhs.modelChanges.json(prettyPrinted: false) + && lhs.endpointChanges.json(prettyPrinted: false) == rhs.endpointChanges.json(prettyPrinted: false) + && lhs.scripts == rhs.scripts + && lhs.jsonValues == rhs.jsonValues + && lhs.objectJSONs == rhs.objectJSONs + } +} diff --git a/Sources/ApodiniMigratorCompare/MigrationGuide/MigrationGuide.swift b/Sources/ApodiniMigratorCompare/MigrationGuide/MigrationGuide.swift deleted file mode 100644 index 0c0236c2..00000000 --- a/Sources/ApodiniMigratorCompare/MigrationGuide/MigrationGuide.swift +++ /dev/null @@ -1,162 +0,0 @@ -// -// This source file is part of the Apodini open source project -// -// SPDX-FileCopyrightText: 2019-2021 Paul Schmiedmayer and the Apodini project authors (see CONTRIBUTORS.md) -// -// SPDX-License-Identifier: MIT -// - -import Foundation - -/// Migration guide -public struct MigrationGuide: Codable { - /// A default summary - static let defaultSummary = "A summary of what changed between versions" - - // MARK: Private Inner Types - private enum CodingKeys: String, CodingKey { - case summary - case serviceType = "service-type" - case specificationType = "api-spec" - case id = "document-id" - case from - case to - case compareConfiguration = "compare-config" - case changeContextNode = "changes" - case scripts - case jsonValues = "json-values" - case objectJSONs = "updated-json-representations" - } - - /// A textual description of the migration guide - public let summary: String - /// The service type the the content of the migration guide corresponds to - public let serviceType: ServiceType - /// The specification type - public let specificationType: SpecificationType - /// Id of the old document from which the migration guide was generated - public let id: UUID? - /// Old version - public let from: Version - /// New version - public let to: Version - /// Configuration used for comparison - public let compareConfiguration: CompareConfiguration? - /// Private change context node that holds, encodes and decodes the changes - private let changeContextNode: ChangeContextNode - /// List of changes in the Migration Guide - public var changes: [Change] { - changeContextNode.changes - } - /// Dictionary holding all registered convert scripts which are referenced from change objects - public var scripts: [Int: JSScript] - /// Dictionary holding all registered json values which are referenced from change objects - public var jsonValues: [Int: JSONValue] - - /// A property that holds json representation of models that had a breaking change on their properties, e.g. rename, addition, deletion or property type change. - /// This property is used for test cases in the client application - public var objectJSONs: [String: JSONValue] - - /// An empty migration guide with no changes - public static var empty: MigrationGuide { - .init( - summary: Self.defaultSummary, - serviceType: .rest, - specificationType: .apodini, - id: nil, - from: .default, - to: .default, - compareConfiguration: nil, - changeContextNode: .init() - ) - } - - /// Internal initializer of the Migration Guide - init( - summary: String, - serviceType: ServiceType, - specificationType: SpecificationType, - id: UUID?, - from: Version, - to: Version, - compareConfiguration: CompareConfiguration?, - changeContextNode: ChangeContextNode - ) { - self.summary = summary - self.serviceType = serviceType - self.specificationType = specificationType - self.id = id - self.from = from - self.to = to - self.changeContextNode = changeContextNode - self.compareConfiguration = compareConfiguration - self.scripts = changeContextNode.scripts - self.jsonValues = changeContextNode.jsonValues - self.objectJSONs = changeContextNode.objectJSONs - } - - /// Initializes the Migration Guide out of two Documents. Documents get compared - /// and the changes can be accessed via `changes` of the new Migration Guide instance - public init(for lhs: Document, rhs: Document, compareConfiguration: CompareConfiguration = .default) { - let changeContextNode = ChangeContextNode(compareConfiguration: compareConfiguration) - - let documentsComparator = DocumentComparator( - lhs: lhs, - rhs: rhs, - changes: changeContextNode, - configuration: rhs.metaData.encoderConfiguration - ) - - // Triggers the compare logic for all elements of both documents, and registers the changes in changeContextNode - documentsComparator.compare() - - self.init( - summary: Self.defaultSummary, - serviceType: .rest, - specificationType: .apodini, - id: lhs.id, - from: lhs.metaData.version, - to: rhs.metaData.version, - compareConfiguration: changeContextNode.compareConfiguration, - changeContextNode: changeContextNode - ) - } - - /// Returns the migration guide by comparing documents at the specified paths - public static func from( - _ lhsDocumentPath: Path, - _ rhsDocumentPath: Path, - compareConfiguration: CompareConfiguration = .default - ) throws -> MigrationGuide { - .init(for: try .decode(from: lhsDocumentPath), rhs: try .decode(from: rhsDocumentPath), compareConfiguration: compareConfiguration) - } - - /// Returns the migration guide by comparing documents at the specified paths - public static func from( - _ lhsDocumentPath: String, - _ rhsDocumentPath: String, - compareConfiguration: CompareConfiguration = .default - ) throws -> MigrationGuide { - try .from(lhsDocumentPath.asPath, rhsDocumentPath.asPath, compareConfiguration: compareConfiguration) - } -} - -extension MigrationGuide: Equatable { - /// :nodoc: - public static func == (lhs: MigrationGuide, rhs: MigrationGuide) -> Bool { - var mutableLhs = lhs - var mutableRhs = rhs - mutableLhs.scripts = [:] - mutableLhs.jsonValues = [:] - mutableLhs.objectJSONs = [:] - - mutableRhs.scripts = [:] - mutableRhs.jsonValues = [:] - mutableRhs.objectJSONs = [:] - - return mutableLhs.json == mutableRhs.json - && lhs.scripts == rhs.scripts - && lhs.jsonValues == rhs.jsonValues - && lhs.objectJSONs == rhs.objectJSONs - } -} diff --git a/Sources/ApodiniMigratorCompare/RelaxedDeltaIdentifiable.swift b/Sources/ApodiniMigratorCompare/RelaxedDeltaIdentifiable.swift index b91160c9..cc8840c3 100644 --- a/Sources/ApodiniMigratorCompare/RelaxedDeltaIdentifiable.swift +++ b/Sources/ApodiniMigratorCompare/RelaxedDeltaIdentifiable.swift @@ -61,7 +61,8 @@ extension RelaxedDeltaIdentifiable { /// Endpoint extension to `RelaxedDeltaIdentifiable` extension Endpoint: RelaxedDeltaIdentifiable { static func ?= (lhs: Endpoint, rhs: Endpoint) -> Bool { - lhs.operation == rhs.operation && lhs.path == rhs.path + lhs.identifier(for: Operation.self) == rhs.identifier(for: Operation.self) + && lhs.identifier(for: EndpointPath.self) == rhs.identifier(for: EndpointPath.self) } } @@ -85,7 +86,9 @@ extension EnumCase: RelaxedDeltaIdentifiable { extension TypeProperty: DeltaIdentifiable { /// DeltaIdentifier of the property initialized from the `name` - public var deltaIdentifier: DeltaIdentifier { .init(name) } + public var deltaIdentifier: DeltaIdentifier { + .init(name) + } } extension TypeProperty: RelaxedDeltaIdentifiable { @@ -96,7 +99,9 @@ extension TypeProperty: RelaxedDeltaIdentifiable { extension TypeName: DeltaIdentifiable { /// DeltaIdentifier of the type name initialized from the `name` - public var deltaIdentifier: DeltaIdentifier { .init(name) } + public var deltaIdentifier: DeltaIdentifier { + .init(buildName()) + } } extension TypeName: RelaxedDeltaIdentifiable { @@ -124,8 +129,8 @@ extension TypeName: RelaxedDeltaIdentifiable { extension TypeInformation: DeltaIdentifiable { public var deltaIdentifier: DeltaIdentifier { - if case let .reference(key) = self { - return .init(key.rawValue) + if case .reference = self { + fatalError("Cannot retrieve the deltaIdentifier of a .reference!") } return typeName.deltaIdentifier } diff --git a/Sources/ApodiniMigratorCompare/ServiceInformation+MigrationGuide.swift b/Sources/ApodiniMigratorCompare/ServiceInformation+MigrationGuide.swift new file mode 100644 index 00000000..c16f1778 --- /dev/null +++ b/Sources/ApodiniMigratorCompare/ServiceInformation+MigrationGuide.swift @@ -0,0 +1,40 @@ +// +// This source file is part of the Apodini open source project +// +// SPDX-FileCopyrightText: 2019-2021 Paul Schmiedmayer and the Apodini project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + +import Foundation + +public extension ServiceInformation { + /// Retrieve the `ExporterConfiguration` if its present. + /// This method considers updates of the supplied ``MigrationGuide`` (e.g. if a ExporterConfiguration was removed in the update `APIDocument`). + func exporterIfPresent( + for type: Exporter.Type = Exporter.self, + migrationGuide: MigrationGuide + ) -> Exporter? { + guard let exporter = exporters[Exporter.type] else { + return nil + } + + for changes in migrationGuide.serviceChanges { + guard let updateChange = changes.modeledUpdateChange, + case let .exporter(exporterChange) = updateChange.updated else { + continue + } + + if let exporterUpdate = exporterChange.modeledUpdateChange { // if it was updated, return the update configuration! + if let exporter = exporterUpdate.updated.to.tryTyped(of: Exporter.self) { + return exporter + } + } else if let exporterRemoval = exporterChange.modeledRemovalChange, + exporterRemoval.id == Exporter.deltaIdentifier { // if it was removed, return no exporter + return nil + } + } + + return exporter.typed() + } +} diff --git a/Sources/ApodiniMigratorCore/APIDocument.swift b/Sources/ApodiniMigratorCore/APIDocument.swift new file mode 100644 index 00000000..31b77552 --- /dev/null +++ b/Sources/ApodiniMigratorCore/APIDocument.swift @@ -0,0 +1,136 @@ +// +// This source file is part of the Apodini open source project +// +// SPDX-FileCopyrightText: 2019-2021 Paul Schmiedmayer and the Apodini project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + +import Foundation +import ApodiniTypeInformation + +/// This enum describes the document format version of the ``APIDocument``. +/// ``APIDocumentVersion`` follows the SemVer versioning scheme. +public enum APIDocumentVersion: String, Codable { + /// The original/legacy document format introduced with version 0.1.0. + /// - Note: This version is assumed when no `version` field is present in the document root. + /// ApodiniMigrator supports parsing legacy documents till 0.3.0. + case v1 = "1.0.0" + /// The current document format introduced with version 0.2.0. + case v2 = "2.0.0" +} + +/// A API document describing an Apodini Web Service. +public struct APIDocument: Value { + /// Id of the document + public let id: UUID + /// Metadata + public var serviceInformation: ServiceInformation + /// Endpoints + private var _endpoints: [Endpoint] + public var endpoints: [Endpoint] { + _endpoints + .map { + var endpoint = $0 + endpoint.dereference(in: types) + return endpoint + } + } + + private var types: TypesStore + + public var models: [TypeInformation] { + Array(types.keys) + .map { TypeInformation.reference($0) } + .map { types.construct(from: $0) } + } + + /// Name of the file, constructed as `api_{version}` + public var fileName: String { + "api_\(serviceInformation.version.string.replacingOccurrences(of: "_", with: ""))" + } + + /// Initializes a new Apodini API document. + public init(serviceInformation: ServiceInformation) { + self.id = .init() + self.serviceInformation = serviceInformation + self._endpoints = [] + self.types = TypesStore() + } + + /// Adds a new endpoint + public mutating func add(endpoint: Endpoint) { + precondition( + !_endpoints.contains(where: { $0.deltaIdentifier == endpoint.deltaIdentifier }), + "Tried adding `Endpoint` to `APIDocument` with colliding identifiers (or just added it twice)." + ) + + var endpoint = endpoint + endpoint.reference(in: &types) + _endpoints.append(endpoint) + } + + public mutating func add(exporter: Configuration) { + serviceInformation.add(exporter: exporter) + } +} + +// MARK: Codable +extension APIDocument: Codable { + public enum CodingError: Error { + case unsupportedDocumentVersion(version: String) + } + + // MARK: Private Inner Types + private enum CodingKeys: String, CodingKey { + case id + case documentVersion = "version" + case serviceInformation = "service" + case endpoints + case types + + case legacyServiceInformation = "info" + case legacyTypes = "components" + } + + /// Creates a new instance by decoding from the given decoder. + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + let documentVersion: APIDocumentVersion + do { + try documentVersion = container.decodeIfPresent(APIDocumentVersion.self, forKey: .documentVersion) ?? .v1 + } catch { + // failed to decode APIDocumentVersion, probably because its an unknown version! + throw CodingError.unsupportedDocumentVersion(version: try container.decode(String.self, forKey: .documentVersion)) + } + + try id = container.decode(UUID.self, forKey: .id) + + switch documentVersion { + case .v1: + let legacyInformation = try container.decode(LegacyServiceInformation.self, forKey: .legacyServiceInformation) + try self.serviceInformation = ServiceInformation(from: legacyInformation) + + let endpoints = try container.decode([LegacyEndpoint].self, forKey: .endpoints) + self._endpoints = endpoints.map { Endpoint(from: $0) } + + try types = container.decode(TypesStore.self, forKey: .legacyTypes) + case .v2: + try serviceInformation = container.decode(ServiceInformation.self, forKey: .serviceInformation) + try _endpoints = container.decode([Endpoint].self, forKey: .endpoints) + try types = container.decode(TypesStore.self, forKey: .types) + } + } + + /// Encodes self into the given encoder. + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + + try container.encode(id, forKey: .id) + try container.encode(APIDocumentVersion.v2, forKey: .documentVersion) + try container.encode(serviceInformation, forKey: .serviceInformation) + try container.encode(_endpoints, forKey: .endpoints) + try container.encode(types, forKey: .types) + } +} diff --git a/Sources/ApodiniMigratorCore/Shared/DeltaIdentifier.swift b/Sources/ApodiniMigratorCore/Shared/DeltaIdentifier.swift index 70368143..e28f1793 100644 --- a/Sources/ApodiniMigratorCore/Shared/DeltaIdentifier.swift +++ b/Sources/ApodiniMigratorCore/Shared/DeltaIdentifier.swift @@ -31,7 +31,7 @@ public struct DeltaIdentifier: Value, RawRepresentable { /// Creates a new instance by decoding from the given decoder. public init(from decoder: Decoder) throws { - rawValue = try decoder.singleValueContainer().decode(String.self) + try rawValue = decoder.singleValueContainer().decode(String.self) } /// Encodes self into the given encoder. @@ -56,6 +56,9 @@ extension DeltaIdentifier: ExpressibleByStringLiteral { } } +// MARK: - ExpressibleByStringInterpolation +extension DeltaIdentifier: ExpressibleByStringInterpolation {} + extension DeltaIdentifier: CustomStringConvertible { /// String representation of self public var description: String { rawValue } diff --git a/Sources/ApodiniMigratorCore/Shared/Value.swift b/Sources/ApodiniMigratorCore/Shared/Value.swift index 2c4aa9c0..c9f47edb 100644 --- a/Sources/ApodiniMigratorCore/Shared/Value.swift +++ b/Sources/ApodiniMigratorCore/Shared/Value.swift @@ -11,3 +11,5 @@ import Foundation /// A protocol that requires conformance to `Codable` and `Hashable` (also `Equatable`), /// that most of the objects in `ApodiniMigrator` conform to public protocol Value: Codable, Hashable {} + +extension Array: Value where Element: Value {} diff --git a/Sources/ApodiniMigratorCore/TypeInformation/TypeInformation+FileRenderable.swift b/Sources/ApodiniMigratorCore/TypeInformation/TypeInformation+FileRenderable.swift index e97aaa5d..d12c0711 100644 --- a/Sources/ApodiniMigratorCore/TypeInformation/TypeInformation+FileRenderable.swift +++ b/Sources/ApodiniMigratorCore/TypeInformation/TypeInformation+FileRenderable.swift @@ -35,22 +35,6 @@ public extension TypeInformation { func fileRenderableTypes() -> [TypeInformation] { filter(\.isEnumOrObject) } - - /// Returns the referenced version of self - func referenced() -> TypeInformation { - switch self { - case .scalar, .reference: - return self - case let .repeated(element): - return .repeated(element: element.referenced()) - case let .dictionary(key, value): - return .dictionary(key: key, value: value.referenced()) - case let .optional(wrappedValue): - return .optional(wrappedValue: wrappedValue.referenced()) - case .object, .enum: - return .reference(typeName.absoluteName()) - } - } } public extension Array where Element == TypeInformation { diff --git a/Sources/ApodiniMigratorCore/WebService/Document.swift b/Sources/ApodiniMigratorCore/WebService/Document.swift deleted file mode 100644 index 112c9459..00000000 --- a/Sources/ApodiniMigratorCore/WebService/Document.swift +++ /dev/null @@ -1,137 +0,0 @@ -// -// This source file is part of the Apodini open source project -// -// SPDX-FileCopyrightText: 2019-2021 Paul Schmiedmayer and the Apodini project authors (see CONTRIBUTORS.md) -// -// SPDX-License-Identifier: MIT -// - -import Foundation -import ApodiniTypeInformation - -public struct MetaData: Value { - /// Server path - var serverPath: String - /// Version - public var version: Version - /// Encoder configuration - public var encoderConfiguration: EncoderConfiguration - /// Decoder configuration - public var decoderConfiguration: DecoderConfiguration - - /// Server path appending the description of the version - public var versionedServerPath: String { - serverPath + "/" + version.description - } - - init() { - serverPath = "" - version = .default - encoderConfiguration = .default - decoderConfiguration = .default - } - - init(serverPath: String, version: Version, encoderConfiguration: EncoderConfiguration, decoderConfiguration: DecoderConfiguration) { - self.serverPath = serverPath - self.version = version - self.encoderConfiguration = encoderConfiguration - self.decoderConfiguration = decoderConfiguration - } -} - -public struct Document: Value { - // MARK: Private Inner Types - private enum CodingKeys: String, CodingKey { - case id, metaData = "info", endpoints, components - } - - /// Id of the document - public let id: UUID - - /// Metadata - public var metaData: MetaData - /// Endpoints - public var endpoints: [Endpoint] - - /// Name of the file, constructed as `api_{version}` - public var fileName: String { - "api_\(metaData.version.string.without("_"))" - } - - /// Initializes an empty document - public init() { - id = .init() - metaData = .init() - endpoints = [] - } - - /// Adds a new enpoint - public mutating func add(endpoint: Endpoint) { - endpoints.append(endpoint) - } - - /// Sets the server path to metadata - public mutating func setServerPath(_ path: String) { - metaData.serverPath = path - } - - /// Sets the version to metadata - public mutating func setVersion(_ version: Version) { - metaData.version = version - } - - /// Sets coder configurations to metada - public mutating func setCoderConfigurations( - _ encoderConfiguration: EncoderConfiguration, - _ decoderConfiguration: DecoderConfiguration - ) { - metaData.encoderConfiguration = encoderConfiguration - metaData.decoderConfiguration = decoderConfiguration - } - - /// Encodes self into the given encoder. - public func encode(to encoder: Encoder) throws { - var container = encoder.container(keyedBy: CodingKeys.self) - try container.encode(id, forKey: .id) - try container.encode(metaData, forKey: .metaData) - var typesStore = TypesStore() - - let referencedEndpoints: [Endpoint] = endpoints.map { - var endpoint = $0 - endpoint.reference(in: &typesStore) - return endpoint - } - - try container.encode(referencedEndpoints, forKey: .endpoints) - try container.encode(typesStore.storage, forKey: .components) - } - - /// Creates a new instance by decoding from the given decoder. - public init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - id = try container.decode(UUID.self, forKey: .id) - metaData = try container.decode(MetaData.self, forKey: .metaData) - - var typesStore = TypesStore() - typesStore.storage = try container.decode([String: TypeInformation].self, forKey: .components) - - let endpoints = try container.decode([Endpoint].self, forKey: .endpoints) - self.endpoints = endpoints.map { - var endpoint = $0 - endpoint.dereference(in: &typesStore) - return endpoint - } - } - - public func allModels() -> [TypeInformation] { - endpoints.reduce(into: Set()) { result, current in - result.insert(current.response) - current.parameters.forEach { parameter in - result.insert(parameter.typeInformation) - } - } - .asArray - .fileRenderableTypes() - .sorted(by: \.typeName) - } -} diff --git a/Sources/ApodiniMigratorCore/WebService/Endpoint.swift b/Sources/ApodiniMigratorCore/WebService/Endpoint.swift deleted file mode 100644 index 41389909..00000000 --- a/Sources/ApodiniMigratorCore/WebService/Endpoint.swift +++ /dev/null @@ -1,126 +0,0 @@ -// -// This source file is part of the Apodini open source project -// -// SPDX-FileCopyrightText: 2019-2021 Paul Schmiedmayer and the Apodini project authors (see CONTRIBUTORS.md) -// -// SPDX-License-Identifier: MIT -// - -import Foundation - -/// A typealias of an array of `Parameter` -public typealias EndpointInput = [Parameter] - -/// Represents an endpoint -public struct Endpoint: Value, DeltaIdentifiable { - /// Name of the handler - public let handlerName: String - - /// Identifier of the handler - public let deltaIdentifier: DeltaIdentifier - - /// The operation of the endpoint - public let operation: Operation - - /// The path string of the endpoint - public let path: EndpointPath - - /// Parameters of the endpoint - public var parameters: EndpointInput - - /// The reference of the `typeInformation` of the response - public var response: TypeInformation - - /// Errors - public let errors: [ErrorCode] - - /// Initializes a new endpoint instance - public init( - handlerName: String, - deltaIdentifier: String, - operation: Operation, - absolutePath: String, - parameters: EndpointInput, - response: TypeInformation, - errors: [ErrorCode] - ) { - self.handlerName = handlerName.without(strings: ">", "<", ",") - var identifier = deltaIdentifier - if !identifier.split(string: ".").compactMap({ Int($0) }).isEmpty { - identifier = handlerName.lowerFirst - } - self.deltaIdentifier = .init(identifier) - self.operation = operation - self.path = .init(absolutePath) - self.parameters = Self.wrappContentParameters(from: parameters, with: handlerName) - self.response = response - self.errors = errors - } - - mutating func dereference(in typeStore: inout TypesStore) { - response = typeStore.construct(from: response) - self.parameters = parameters.map { - var param = $0 - param.dereference(in: &typeStore) - return param - } - } - - mutating func reference(in typeStore: inout TypesStore) { - response = typeStore.store(response) - self.parameters = parameters.map { - var param = $0 - param.reference(in: &typeStore) - return param - } - } - - /// Returns a version of self where occurrencies of type informations (response or parameters) are references - public func referencedTypes() -> Endpoint { - var retValue = self - var typesStore = TypesStore() - retValue.reference(in: &typesStore) - return retValue - } -} - -private extension Endpoint { - static func wrappContentParameters(from parameters: EndpointInput, with handlerName: String) -> EndpointInput { - let contentParameters = parameters.filter { $0.parameterType == .content } - guard !contentParameters.isEmpty else { - return parameters - } - - var contentParameter: Parameter? - - switch contentParameters.count { - case 1: - contentParameter = contentParameters.first - default: - let typeInformation = TypeInformation.object( - name: Parameter.wrappedContentParameterTypeName(from: handlerName), - properties: contentParameters.map { - TypeProperty( - name: $0.name, - type: $0.necessity == .optional ? $0.typeInformation.asOptional : $0.typeInformation - ) - } - ) - - contentParameter = .init( - name: Parameter.wrappedContentParameter, - typeInformation: typeInformation, - parameterType: .content, - isRequired: contentParameters.contains(where: { $0.necessity == .required }) - ) - } - - var result = parameters.filter { $0.parameterType != .content } - - contentParameter.map { - result.append($0) - } - - return result - } -} diff --git a/Sources/ApodiniMigratorCore/WebService/Endpoint/CommunicationalPattern.swift b/Sources/ApodiniMigratorCore/WebService/Endpoint/CommunicationalPattern.swift new file mode 100644 index 00000000..ec27e34c --- /dev/null +++ b/Sources/ApodiniMigratorCore/WebService/Endpoint/CommunicationalPattern.swift @@ -0,0 +1,21 @@ +// +// This source file is part of the Apodini open source project +// +// SPDX-FileCopyrightText: 2019-2021 Paul Schmiedmayer and the Apodini project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + +import Foundation + +/// Defines the communicational pattern of a given endpoint. +public enum CommunicationalPattern: String, CaseIterable, Value { + /// **One** client message followed by **one** service message + case requestResponse + /// **Any amount** of client messages followed by **one** service message + case clientSideStream + /// **One** client message followed by **any amount** of service messages + case serviceSideStream + /// **Any amount** of client messages and **any amount** of service messages in an **undefined order** + case bidirectionalStream +} diff --git a/Sources/ApodiniMigratorCore/WebService/Endpoint/Endpoint.swift b/Sources/ApodiniMigratorCore/WebService/Endpoint/Endpoint.swift new file mode 100644 index 00000000..74fba46c --- /dev/null +++ b/Sources/ApodiniMigratorCore/WebService/Endpoint/Endpoint.swift @@ -0,0 +1,267 @@ +// +// This source file is part of the Apodini open source project +// +// SPDX-FileCopyrightText: 2019-2021 Paul Schmiedmayer and the Apodini project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + +import Foundation +import OrderedCollections + +/// Represents an endpoint +public struct Endpoint: Value, DeltaIdentifiable { + /// Identifier of the handler + public let deltaIdentifier: DeltaIdentifier + + /// Storage structure for any kind of identifier of a ``Endpoint``. + /// Use ``add(identifier:)`` to add any ``EndpointIdentifier``s. + /// Every ``Endpoint`` has the following identifiers by standard: + /// - ``TypeName`` (for the handlerName) + /// - ``Operation`` + /// - ``EndpointPath`` + /// + /// Use ``identifier(for:)`` or ``identifierIfAvailable(for:)`` to retrieve an ``EndpointIdentifier``. + /// Or use ``handlerName``, ``operation`` or ``path`` computed properties for quick access. + public var identifiers: OrderedDictionary + + /// The communicational pattern of the endpoint. + public let communicationalPattern: CommunicationalPattern + /// Parameters of the endpoint + public var parameters: [Parameter] + /// The reference of the `typeInformation` of the response + public var response: TypeInformation + /// Errors + public let errors: [ErrorCode] + + public var handlerName: TypeName { + self.identifier() + } + + public var operation: Operation { + self.identifier() + } + + public var path: EndpointPath { + self.identifier() + } + + /// Initializes a new endpoint instance + public init( + handlerName: String, + deltaIdentifier: String, + operation: Operation, + communicationalPattern: CommunicationalPattern, + absolutePath: String, + parameters: [Parameter], + response: TypeInformation, + errors: [ErrorCode] + ) { + let typeName = TypeName(rawValue: handlerName) + + var identifier = deltaIdentifier + // checks for "x.x.x." style Apodini identifiers! + if !identifier.split(separator: ".").compactMap({ Int($0) }).isEmpty { + identifier = typeName.buildName() + } + + self.deltaIdentifier = .init(identifier) + self.identifiers = [:] + + self.parameters = Self.wrapContentParameters(from: parameters, with: typeName.buildName()) + self.communicationalPattern = communicationalPattern + self.response = response + self.errors = errors + + self.add(identifier: typeName) + self.add(identifier: operation) + self.add(identifier: EndpointPath(absolutePath)) + } + + /// Initializes a new endpoint instance + public init( + handlerName: TypeName, + deltaIdentifier: String, + operation: Operation, + communicationalPattern: CommunicationalPattern, + absolutePath: String, + parameters: [Parameter], + response: TypeInformation, + errors: [ErrorCode] + ) { + self.init( + handlerName: handlerName.rawValue, + deltaIdentifier: deltaIdentifier, + operation: operation, + communicationalPattern: communicationalPattern, + absolutePath: absolutePath, + parameters: parameters, + response: response, + errors: errors + ) + } + + public mutating func add(identifier: Identifier) { + self.identifiers[Identifier.identifierType] = AnyEndpointIdentifier(from: identifier) + } + + public func identifierIfPresent(for type: Identifier.Type = Identifier.self) -> Identifier? { + guard let rawValue = self.identifiers[Identifier.identifierType]?.value else { + return nil + } + + return Identifier(rawValue: rawValue) + } + + public func identifier(for type: Identifier.Type = Identifier.self) -> Identifier { + guard let identifier = identifierIfPresent(for: Identifier.self) else { + fatalError("Failed to retrieve required Identifier \(type) which wasn't present on endpoint \(deltaIdentifier).") + } + + return identifier + } + + mutating func dereference(in typeStore: TypesStore) { + response = typeStore.construct(from: response) + self.parameters = parameters.map { + var param = $0 + param.dereference(in: typeStore) + return param + } + } + + mutating func reference(in typeStore: inout TypesStore) { + response = typeStore.store(response) + self.parameters = parameters.map { + var param = $0 + param.reference(in: &typeStore) + return param + } + } + + /// Returns a version of self where occurrences of type information (response or parameters) are references + public func referencedTypes() -> Endpoint { + var retValue = self + var typesStore = TypesStore() + retValue.reference(in: &typesStore) + return retValue + } +} + +// MARK: Codable +extension Endpoint: Codable { + private enum CodingKeys: String, CodingKey { + case deltaIdentifier + case identifiers + case communicationalPattern + case parameters + case response + case errors + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + self.deltaIdentifier = try container.decode(DeltaIdentifier.self, forKey: .deltaIdentifier) + self.identifiers = try container.decode([String: String].self, forKey: .identifiers) + .reduce(into: [:]) { result, entry in + result[entry.key] = AnyEndpointIdentifier(id: entry.key, value: entry.value) + } + self.communicationalPattern = try container.decode(CommunicationalPattern.self, forKey: .communicationalPattern) + self.parameters = try container.decode([Parameter].self, forKey: .parameters) + self.response = try container.decode(TypeInformation.self, forKey: .response) + self.errors = try container.decode([ErrorCode].self, forKey: .errors) + } + + public func encode(to encoder: Encoder) throws { + struct AnyCodingKey: CodingKey { + var stringValue: String + + init(stringValue: String) { + self.stringValue = stringValue + } + + var intValue: Int? { + fatalError("Can't access intValue for AnyCodingKey!") + } + + init?(intValue: Int) { + fatalError("Can't init from intValue for AnyCodingKey!") + } + } + + var container = encoder.container(keyedBy: CodingKeys.self) + + try container.encode(deltaIdentifier, forKey: .deltaIdentifier) + + var identifierContainer = container.nestedContainer(keyedBy: AnyCodingKey.self, forKey: .identifiers) + var sortedIdentifiers = self.identifiers + sortedIdentifiers.sort() + for (key, value) in sortedIdentifiers { + try identifierContainer.encode(value.value, forKey: AnyCodingKey(stringValue: key)) + } + + try container.encode(communicationalPattern, forKey: .communicationalPattern) + try container.encode(parameters, forKey: .parameters) + try container.encode(response, forKey: .response) + try container.encode(errors, forKey: .errors) + } +} + +// MARK: Equatable +extension Endpoint: Equatable { + public static func == (lhs: Endpoint, rhs: Endpoint) -> Bool { + var lhsIdentifiers = lhs.identifiers + var rhsIdentifiers = rhs.identifiers + lhsIdentifiers.sort() + rhsIdentifiers.sort() + + return lhs.deltaIdentifier == rhs.deltaIdentifier + && lhsIdentifiers == rhsIdentifiers + && lhs.communicationalPattern == rhs.communicationalPattern + && lhs.parameters == rhs.parameters + && lhs.response == rhs.response + && lhs.errors == rhs.errors + } +} + +private extension Endpoint { + static func wrapContentParameters(from parameters: [Parameter], with handlerName: String) -> [Parameter] { + let contentParameters = parameters.filter { $0.parameterType == .content } + guard !contentParameters.isEmpty else { + return parameters + } + + var contentParameter: Parameter? + + switch contentParameters.count { + case 1: + contentParameter = contentParameters.first + default: + let typeInformation = TypeInformation.object( + name: Parameter.wrappedContentParameterTypeName(from: handlerName), + properties: contentParameters.map { + TypeProperty( + name: $0.name, + type: $0.necessity == .optional ? $0.typeInformation.asOptional : $0.typeInformation + ) + } + ) + + contentParameter = .init( + name: Parameter.wrappedContentParameter, + typeInformation: typeInformation, + parameterType: .content, + isRequired: contentParameters.contains(where: { $0.necessity == .required }) + ) + } + + var result = parameters.filter { $0.parameterType != .content } + + contentParameter.map { + result.append($0) + } + + return result + } +} diff --git a/Sources/ApodiniMigratorCore/WebService/Endpoint/EndpointIdentifier.swift b/Sources/ApodiniMigratorCore/WebService/Endpoint/EndpointIdentifier.swift new file mode 100644 index 00000000..7fd5f1d4 --- /dev/null +++ b/Sources/ApodiniMigratorCore/WebService/Endpoint/EndpointIdentifier.swift @@ -0,0 +1,60 @@ +// +// This source file is part of the Apodini open source project +// +// SPDX-FileCopyrightText: 2019-2021 Paul Schmiedmayer and the Apodini project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + +import Foundation + +/// Some sort of identifier of an ``Endpoint``. +public protocol EndpointIdentifier: RawRepresentable where Self.RawValue == String { + /// A string identifier, to uniquely identify this ``EndpointIdentifier``. + static var identifierType: String { get } +} + +public extension EndpointIdentifier { + /// Default implementation. Uses the type name. + static var identifierType: String { + "\(Self.self)" + } +} + +// MARK: Handler Name +extension TypeName: EndpointIdentifier { + public static var identifierType: String { + "HandlerName" + } +} + + +public struct AnyEndpointIdentifier: Value, DeltaIdentifiable, Hashable { + public let id: String + public let value: String + + public var deltaIdentifier: DeltaIdentifier { + DeltaIdentifier(rawValue: id) + } + + public init(id: String, value: String) { + self.id = id + self.value = value + } + + public init(from identifier: Identifier) { + self.id = Identifier.identifierType + self.value = identifier.rawValue + } + + public func typed(of type: Identifier.Type = Identifier.self) -> Identifier { + guard id == Identifier.identifierType else { + fatalError("Tired to cast \(self) to \(type) with non matching id \(Identifier.identifierType)!") + } + + guard let typedValue = Identifier(rawValue: value) else { + fatalError("Unexpected error when creating typed version of \(Identifier.self) from \(self)!") + } + return typedValue + } +} diff --git a/Sources/ApodiniMigratorCore/WebService/EndpointPath.swift b/Sources/ApodiniMigratorCore/WebService/Endpoint/EndpointPath.swift similarity index 90% rename from Sources/ApodiniMigratorCore/WebService/EndpointPath.swift rename to Sources/ApodiniMigratorCore/WebService/Endpoint/EndpointPath.swift index fa2c6327..3ef7944a 100644 --- a/Sources/ApodiniMigratorCore/WebService/EndpointPath.swift +++ b/Sources/ApodiniMigratorCore/WebService/Endpoint/EndpointPath.swift @@ -34,7 +34,7 @@ private enum PathComponent: CustomStringConvertible { private typealias Components = [Int: PathComponent] /// Represents an endpoint path -public struct EndpointPath: Value, CustomStringConvertible { +public struct EndpointPath: Value, CustomStringConvertible, EndpointIdentifier { /// Separator of components private static let separator = "/" @@ -47,6 +47,10 @@ public struct EndpointPath: Value, CustomStringConvertible { .map { "\($0.value)" } .joined(separator: Self.separator) } + + public var rawValue: String { + description + } /// Path excluding the first component which corresponds to the version of the web service public var resourcePath: String { @@ -57,11 +61,16 @@ public struct EndpointPath: Value, CustomStringConvertible { .joined(separator: Self.separator) } + public init(rawValue string: String) { + self.init(string) + } + /// Initializes an instance out of string representation of the path e.g. `/v1/users/{id}` public init(_ string: String) { var components: Components = .init() string - .split(string: Self.separator, ignoreEmptyComponents: true) + .split(separator: "/") + .map { String($0) } .enumerated() .forEach { index, component in components[index] = PathComponent(stringValue: component) @@ -106,7 +115,9 @@ private extension String { /// Returns a version of self without surrounding curly brackets var dropCurlyBrackets: String { - without("{").without("}") + self + .replacingOccurrences(of: "{", with: "") + .replacingOccurrences(of: "}", with: "") } /// Returns a version of self with surrounding curly brackets diff --git a/Sources/ApodiniMigratorCore/WebService/ErrorCode.swift b/Sources/ApodiniMigratorCore/WebService/Endpoint/ErrorCode.swift similarity index 100% rename from Sources/ApodiniMigratorCore/WebService/ErrorCode.swift rename to Sources/ApodiniMigratorCore/WebService/Endpoint/ErrorCode.swift diff --git a/Sources/ApodiniMigratorCore/WebService/Operation.swift b/Sources/ApodiniMigratorCore/WebService/Endpoint/Operation.swift similarity index 89% rename from Sources/ApodiniMigratorCore/WebService/Operation.swift rename to Sources/ApodiniMigratorCore/WebService/Endpoint/Operation.swift index 1cf8aa03..570e4340 100644 --- a/Sources/ApodiniMigratorCore/WebService/Operation.swift +++ b/Sources/ApodiniMigratorCore/WebService/Endpoint/Operation.swift @@ -9,7 +9,7 @@ import Foundation /// Defines the CRUD operation of a given endpoint -public enum Operation: String, CaseIterable, Value { +public enum Operation: String, CaseIterable, Value, EndpointIdentifier { /// The associated endpoint is used for a `create` operation case create /// The associated endpoint is used for a `read` operation diff --git a/Sources/ApodiniMigratorCore/WebService/Parameter.swift b/Sources/ApodiniMigratorCore/WebService/Endpoint/Parameter.swift similarity index 93% rename from Sources/ApodiniMigratorCore/WebService/Parameter.swift rename to Sources/ApodiniMigratorCore/WebService/Endpoint/Parameter.swift index e60e14b0..9911e633 100644 --- a/Sources/ApodiniMigratorCore/WebService/Parameter.swift +++ b/Sources/ApodiniMigratorCore/WebService/Endpoint/Parameter.swift @@ -45,7 +45,7 @@ public struct Parameter: Value { /// and the parameter type is `.content` public var isWrapped: Bool { name == Self.wrappedContentParameter - && typeInformation.typeName.name.hasSuffix("WrappedContent") + && typeInformation.typeName.mangledName.hasSuffix("WrappedContent") && parameterType == .content } @@ -61,24 +61,25 @@ public struct Parameter: Value { self.parameterType = parameterType self.necessity = isRequired ? .required : .optional } - - mutating func dereference(in typeStore: inout TypesStore) { - typeInformation = typeStore.construct(from: typeInformation) - } - + mutating func reference(in typeStore: inout TypesStore) { typeInformation = typeStore.store(typeInformation) } - + + mutating func dereference(in typeStore: TypesStore) { + typeInformation = typeStore.construct(from: typeInformation) + } + + static func wrappedContentParameterTypeName(from handlerName: String) -> TypeName { - .init(name: handlerName.without("Handler") + "WrappedContent") + TypeName(rawValue: handlerName.replacingOccurrences(of: "Handler", with: "").upperFirst + "WrappedContent") } /// Returns a version of self where the typeInformation is a reference if a complex object or enum public func referencedType() -> Parameter { .init( name: name, - typeInformation: typeInformation.referenced(), + typeInformation: typeInformation.asReference(), parameterType: parameterType, isRequired: necessity == .required ) diff --git a/Sources/ApodiniMigratorCompare/AnyCodableElement.swift b/Sources/ApodiniMigratorCore/WebService/Legacy/AnyCodableElement.swift similarity index 87% rename from Sources/ApodiniMigratorCompare/AnyCodableElement.swift rename to Sources/ApodiniMigratorCore/WebService/Legacy/AnyCodableElement.swift index 955ce1e2..7c198bdc 100644 --- a/Sources/ApodiniMigratorCompare/AnyCodableElement.swift +++ b/Sources/ApodiniMigratorCore/WebService/Legacy/AnyCodableElement.swift @@ -19,7 +19,7 @@ extension AnyCodableElementValue { } // MARK: - AnyCodableElementValue conformance -extension Document: AnyCodableElementValue {} +extension APIDocument: AnyCodableElementValue {} extension DeltaIdentifier: AnyCodableElementValue {} extension Endpoint: AnyCodableElementValue {} extension EndpointPath: AnyCodableElementValue {} @@ -39,26 +39,28 @@ public final class AnyCodableElement: Value, CustomStringConvertible { /// Value let value: Any - /// JSON string representation of `value`, with `.sortedKeys` outputformatting + /// JSON string representation of `value`, with `.sortedKeys` output formatting public var description: String { // swiftlint:disable:next force_cast (value as! Encodable).json() } - // MARK: - Intializer + // MARK: - Initializer /// Internal initializer that restricts the initialization only with values of known types that can be encoded and decoded by `AnyCodableElement` init(_ value: A) { self.value = value } /// Encodes `self` into the given encoder by encoding value into a singleValueContainer - public func encode(to encoder: Encoder) throws { + public func encode(to encoder: Encoder) throws { // swiftlint:disable:this cyclomatic_complexity var singleValueContainer = encoder.singleValueContainer() - if let value = value as? Document { + if let value = value as? APIDocument { try singleValueContainer.encode(value) } else if let value = value as? DeltaIdentifier { try singleValueContainer.encode(value) + } else if let value = value as? LegacyEndpoint { + try singleValueContainer.encode(value) } else if let value = value as? Endpoint { try singleValueContainer.encode(value) } else if let value = value as? EndpointPath { @@ -87,10 +89,10 @@ public final class AnyCodableElement: Value, CustomStringConvertible { } /// Creates a new instance by decoding from the given decoder - public init(from decoder: Decoder) throws { + public init(from decoder: Decoder) throws { // swiftlint:disable:this cyclomatic_complexity let container = try decoder.singleValueContainer() - if let value = try? container.decode(Document.self) { + if let value = try? container.decode(APIDocument.self) { self.value = value } else if let value = try? container.decode(Necessity.self) { self.value = value @@ -98,6 +100,8 @@ public final class AnyCodableElement: Value, CustomStringConvertible { self.value = value } else if let value = try? container.decode(ApodiniMigratorCore.Operation.self) { self.value = value + } else if let value = try? container.decode(LegacyEndpoint.self) { + self.value = Endpoint(from: value) } else if let value = try? container.decode(Endpoint.self) { self.value = value } else if let value = try? container.decode(EndpointPath.self) { @@ -116,17 +120,15 @@ public final class AnyCodableElement: Value, CustomStringConvertible { self.value = value } else if let value = try? container.decode(EnumCase.self) { self.value = value - } else { throw DecodingError.dataCorruptedError(in: container, debugDescription: "Failed to decode \(Self.self)") } + } else { + throw DecodingError.dataCorruptedError(in: container, debugDescription: "Failed to decode \(Self.self)") + } } - + /// Returns the typed value. The method is to be used by migrator objects to cast the element of change after ensuring the type /// via the target value of the element. E.g. for an element `.endpoint(id, target: .operation)`, the value can be casted as `.typed(Operation.self)` - /// - Note: Results in `fatalError` if casting fails - public func typed(_ type: C.Type) -> C { - guard let value = value as? C else { - fatalError("Failed to cast value to \(C.self)") - } - return value + public func tryTyped(_ type: C.Type = C.self) -> C? { + value as? C } } diff --git a/Sources/ApodiniMigratorCore/WebService/Legacy/LegacyEndpoint.swift b/Sources/ApodiniMigratorCore/WebService/Legacy/LegacyEndpoint.swift new file mode 100644 index 00000000..c37fccce --- /dev/null +++ b/Sources/ApodiniMigratorCore/WebService/Legacy/LegacyEndpoint.swift @@ -0,0 +1,36 @@ +// +// This source file is part of the Apodini open source project +// +// SPDX-FileCopyrightText: 2019-2021 Paul Schmiedmayer and the Apodini project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + +import Foundation + +struct LegacyEndpoint: Codable { + let handlerName: String + let deltaIdentifier: DeltaIdentifier + let operation: Operation + let path: EndpointPath + let parameters: [Parameter] + let response: TypeInformation + let errors: [ErrorCode] +} + +extension Endpoint { + init(from endpoint: LegacyEndpoint) { + self.deltaIdentifier = endpoint.deltaIdentifier + self.identifiers = [:] + self.communicationalPattern = .requestResponse + self.parameters = endpoint.parameters + self.response = endpoint.response + self.errors = endpoint.errors + + let handlerName = TypeName(rawValue: endpoint.handlerName) + + self.add(identifier: handlerName) + self.add(identifier: endpoint.operation) + self.add(identifier: endpoint.path) + } +} diff --git a/Sources/ApodiniMigratorCore/WebService/Legacy/LegacyServiceInformation.swift b/Sources/ApodiniMigratorCore/WebService/Legacy/LegacyServiceInformation.swift new file mode 100644 index 00000000..a7cc638e --- /dev/null +++ b/Sources/ApodiniMigratorCore/WebService/Legacy/LegacyServiceInformation.swift @@ -0,0 +1,81 @@ +// +// This source file is part of the Apodini open source project +// +// SPDX-FileCopyrightText: 2019-2021 Paul Schmiedmayer and the Apodini project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + +import Foundation + +struct LegacyServiceInformation: Codable { + enum MigrationError: Error { + case failedMath(path: String) + case failedHostnameExtraction(path: String) + case failedPortConversion(path: String) + } + + let version: Version + let serverPath: String + let encoderConfiguration: EncoderConfiguration + let decoderConfiguration: DecoderConfiguration +} + +extension HTTPInformation { + /// Initialize a `HTTPInformation` from a legacy style server path string. + /// - Parameter serverPath: The server path string e.g. formatted as follows: "http://:[/optional-path]" + /// - Throws: Throws `LegacyServiceInformation.MigrationError` if encountering malformed format. + public init(fromLegacyServerPath serverPath: String) throws { + let range = NSRange(serverPath.startIndex..., in: serverPath) + // swiftlint:disable:next force_try + let regex = try! NSRegularExpression(pattern: "^http://(.+):([0-9]+)(/(\\w|\\d)+)?$") + + guard let match = regex.firstMatch(in: serverPath, range: range) else { + throw LegacyServiceInformation.MigrationError.failedMath(path: serverPath) + } + + guard let hostname = serverPath.retrieveMatch(match: match, at: 1), + let portString = serverPath.retrieveMatch(match: match, at: 2) else { + throw LegacyServiceInformation.MigrationError.failedHostnameExtraction(path: serverPath) + } + + guard let port = Int(portString) else { + throw LegacyServiceInformation.MigrationError.failedPortConversion(path: serverPath) + } + + self.hostname = hostname + self.port = port + } +} + +extension ServiceInformation { + init(from information: LegacyServiceInformation) throws { + let http = try HTTPInformation(fromLegacyServerPath: information.serverPath) + + self.init( + version: information.version, + http: http, + exporters: [ + RESTExporterConfiguration( + encoderConfiguration: information.encoderConfiguration, + decoderConfiguration: information.decoderConfiguration) + ] + ) + } +} + +private extension String { + /// Retrieves the substring of a matched group of a `NSTextCheckingResult`. + /// - Parameters: + /// - match: The match (corresponding to the self String) + /// - at: The group number to retrieve + /// - Returns: The matched substring for the given group + func retrieveMatch(match: NSTextCheckingResult, at: Int) -> String? { + let rangeBounds = match.range(at: at) + guard let range = Range(rangeBounds, in: self) else { + return nil + } + + return String(self[range]) + } +} diff --git a/Sources/ApodiniMigratorCore/WebService/ServiceInformation/ExporterConfiguration.swift b/Sources/ApodiniMigratorCore/WebService/ServiceInformation/ExporterConfiguration.swift new file mode 100644 index 00000000..682f6e78 --- /dev/null +++ b/Sources/ApodiniMigratorCore/WebService/ServiceInformation/ExporterConfiguration.swift @@ -0,0 +1,150 @@ +// +// This source file is part of the Apodini open source project +// +// SPDX-FileCopyrightText: 2019-2021 Paul Schmiedmayer and the Apodini project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + +import Foundation + +/// Describes an Apodini exporter type. +public enum ApodiniExporterType: String, Value, CodingKey, CaseIterable { + /// The `ApodiniREST` exporter. + case rest + + public func anyDecode(from container: KeyedDecodingContainer, forKey key: Key) throws -> AnyExporterConfiguration { + switch self { + case .rest: + return AnyExporterConfiguration(try container.decode(RESTExporterConfiguration.self, forKey: key)) + } + } +} + + +/// Any conforming type, describes a configured exporter on a Apodini web service. +public protocol ExporterConfiguration: _ExporterConfiguration, Hashable {} + +public protocol _ExporterConfiguration: Codable { + /// The ``ApodiniExporterType`` of the configuration. + static var type: ApodiniExporterType { get } + + /// A type erased `==` function. This method is implemented by default when conforming to `Equatable`. + func compare(to exporter: _ExporterConfiguration) -> Bool + + /// A type erased `hash(into:)`. This method is implemented by default wehn conforming to `Hashable`. + func anyHash(into hasher: inout Hasher) +} + +extension _ExporterConfiguration { + /// The ``ApodiniExporterType`` of the configuration. + public var type: ApodiniExporterType { + Self.type + } + + /// The `DeltaIdentifier` of the Exporter. + public static var deltaIdentifier: DeltaIdentifier { + "\(type.rawValue)" + } + + func anyEncode(into container: inout KeyedEncodingContainer, forKey key: Key) throws { + try container.encode(self, forKey: key) + } +} + + +public struct AnyExporterConfiguration: Hashable, DeltaIdentifiable { + private let exporter: _ExporterConfiguration + + public var deltaIdentifier: DeltaIdentifier { + "\(exporter.type.rawValue)" + } + + init(untyped exporter: _ExporterConfiguration) { + self.exporter = exporter + } + + public init(_ exporter: Exporter) { + self.exporter = exporter + } + + public func tryTyped(of exporter: Exporter.Type = Exporter.self) -> Exporter? { + self.exporter as? Exporter + } + + public func typed(of exporter: Exporter.Type = Exporter.self) -> Exporter { + guard let castedExporter = self.exporter as? Exporter else { + fatalError("Failed to cast exporter to \(Exporter.self): \(exporter)") + } + + return castedExporter + } + + func anyEncode(into container: inout KeyedEncodingContainer, forKey key: Key) throws { + try exporter.anyEncode(into: &container, forKey: key) + } + + public static func == (lhs: AnyExporterConfiguration, rhs: AnyExporterConfiguration) -> Bool { + lhs.deltaIdentifier == rhs.deltaIdentifier + && lhs.exporter.compare(to: rhs.exporter) + } + + public func hash(into hasher: inout Hasher) { + exporter.anyHash(into: &hasher) + } +} + +extension AnyExporterConfiguration: Codable { + private enum CodingKeys: String, CodingKey { + case type + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + let type = try container.decode(ApodiniExporterType.self, forKey: .type) + + switch type { + case .rest: + try exporter = RESTExporterConfiguration(from: decoder) + } + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + + try container.encode(exporter.type, forKey: .type) + try exporter.encode(to: encoder) + } +} + +extension ExporterConfiguration { + /// Default type erased implementation for `Equatable`. + public func compare(to exporter: _ExporterConfiguration) -> Bool { + guard let casted = exporter as? Self else { + fatalError("\(Swift.type(of: exporter)) cannot be casted to \(Self.self).") + } + + return self == casted + } + + /// Default type erased implementation for `Hashable`. + public func anyHash(into hasher: inout Hasher) { + self.hash(into: &hasher) + } +} + +public struct RESTExporterConfiguration: ExporterConfiguration, Value { + public static var type: ApodiniExporterType { + .rest + } + + /// Encoder configuration + public var encoderConfiguration: EncoderConfiguration + /// Decoder configuration + public var decoderConfiguration: DecoderConfiguration + + public init(encoderConfiguration: EncoderConfiguration, decoderConfiguration: DecoderConfiguration) { + self.encoderConfiguration = encoderConfiguration + self.decoderConfiguration = decoderConfiguration + } +} diff --git a/Sources/ApodiniMigratorCore/WebService/ServiceInformation/HTTPInformation.swift b/Sources/ApodiniMigratorCore/WebService/ServiceInformation/HTTPInformation.swift new file mode 100644 index 00000000..efd04962 --- /dev/null +++ b/Sources/ApodiniMigratorCore/WebService/ServiceInformation/HTTPInformation.swift @@ -0,0 +1,37 @@ +// +// This source file is part of the Apodini open source project +// +// SPDX-FileCopyrightText: 2019-2021 Paul Schmiedmayer and the Apodini project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + +import Foundation + +public struct HTTPInformation: Value, LosslessStringConvertible { + public var description: String { + "\(hostname):\(port)" + } + + public let hostname: String + public let port: Int + + public init(hostname: String, port: Int = 80) { + self.hostname = hostname + self.port = port + } + + public init?(_ description: String) { + guard let colonIndex = description.lastIndex(of: ":") else { + return nil + } + + self.hostname = String(description[description.startIndex ... colonIndex]) + + let portString = String(description[description.index(after: colonIndex) ... description.endIndex]) + guard let port = Int(portString) else { + return nil + } + self.port = port + } +} diff --git a/Sources/ApodiniMigratorCore/WebService/ServiceInformation/ServiceInformation.swift b/Sources/ApodiniMigratorCore/WebService/ServiceInformation/ServiceInformation.swift new file mode 100644 index 00000000..dcafb713 --- /dev/null +++ b/Sources/ApodiniMigratorCore/WebService/ServiceInformation/ServiceInformation.swift @@ -0,0 +1,130 @@ +// +// This source file is part of the Apodini open source project +// +// SPDX-FileCopyrightText: 2019-2021 Paul Schmiedmayer and the Apodini project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + +import Foundation + +/// General Information about the web service. +public struct ServiceInformation: Value, Hashable { + /// Version information of the running web service. + public let version: Version + + /// Information about the exposed http endpoint + public let http: HTTPInformation + + public var exporters: [ApodiniExporterType: AnyExporterConfiguration] + + public var configuredExporters: Dictionary.Keys { + exporters.keys + } + + public init( + version: Version, + http: HTTPInformation, + exporters: [_ExporterConfiguration] + ) { + self.version = version + self.http = http + self.exporters = [:] + + for exporter in exporters { + self.exporters[exporter.type] = AnyExporterConfiguration(untyped: exporter) + } + } + + public init( + version: Version, + http: HTTPInformation, + exporters: _ExporterConfiguration... + ) { + self.init(version: version, http: http, exporters: exporters) + } + + @discardableResult + public mutating func add(exporter: Exporter) -> Self { + exporters[Exporter.type] = AnyExporterConfiguration(exporter) + return self + } + + public func exporter(for type: Exporter.Type = Exporter.self) -> Exporter { + guard let exporter = exporters[Exporter.type] else { + fatalError("Failed to retrieve exporter from ServiceInformation: \(type)") + } + + return exporter.typed() + } + + public func exporterIfPresent(for type: Exporter.Type = Exporter.self) -> Exporter? { + guard let exporter = exporters[Exporter.type] else { + return nil + } + + return exporter.typed() + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(version) + hasher.combine(http) + hasher.combine(exporters) + } + + public static func == (lhs: ServiceInformation, rhs: ServiceInformation) -> Bool { + lhs.version == rhs.version + && lhs.http == rhs.http + && lhs.exporters == rhs.exporters + } +} + +extension ServiceInformation: DeltaIdentifiable { + // there is only a single service information + public static var deltaIdentifier: DeltaIdentifier = "SERVICE_INFO_ID" + + public var deltaIdentifier: DeltaIdentifier { + Self.deltaIdentifier + } +} + +extension ServiceInformation: Codable { + private enum CodingKeys: String, CodingKey { + case version + case http + case exporters + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + try self.version = container.decode(Version.self, forKey: .version) + try self.http = container.decode(HTTPInformation.self, forKey: .http) + self.exporters = [:] + + let exporterContainer = try container.nestedContainer(keyedBy: ApodiniExporterType.self, forKey: .exporters) + for type in ApodiniExporterType.allCases { + var exporter: AnyExporterConfiguration? + do { + exporter = try type.anyDecode(from: exporterContainer, forKey: type) + } catch DecodingError.keyNotFound { + exporter = nil + } + + if let exporter = exporter { + self.exporters[type] = exporter + } + } + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(version, forKey: .version) + try container.encode(http, forKey: .http) + + var exporterContainer = container.nestedContainer(keyedBy: ApodiniExporterType.self, forKey: .exporters) + for (key, exporter) in exporters { + try exporter.anyEncode(into: &exporterContainer, forKey: key) + } + } +} diff --git a/Sources/ApodiniMigratorCore/WebService/Version.swift b/Sources/ApodiniMigratorCore/WebService/Version.swift index e3672f72..b0c9de74 100644 --- a/Sources/ApodiniMigratorCore/WebService/Version.swift +++ b/Sources/ApodiniMigratorCore/WebService/Version.swift @@ -60,9 +60,14 @@ public struct Version: Value { let container = try decoder.singleValueContainer() let string = try container.decode(String.self) - let components = string.split(string: "_") + let components = string + .split(separator: "_") + .map { String($0) } let prefix = components.first - let numbers = components.last?.split(string: ".") + let numbers = components + .last? + .split(separator: ".") + .map { String($0) } if let prefix = prefix, diff --git a/Sources/ApodiniMigratorShared/Extensions/Array+Extensions.swift b/Sources/ApodiniMigratorShared/Extensions/Array+Extensions.swift index 75d21e16..64085911 100644 --- a/Sources/ApodiniMigratorShared/Extensions/Array+Extensions.swift +++ b/Sources/ApodiniMigratorShared/Extensions/Array+Extensions.swift @@ -8,10 +8,13 @@ import Foundation -public extension Collection { - /// Indicates whether the collection is not empty - var isNotEmpty: Bool { - !isEmpty +public extension Array { + /// This method can be used to flatten an array of arrays. + /// - Returns: Returns the flattened array, where they are all appened to one big array. + func flatten() -> [InnerElement] where Element == [InnerElement] { + self.reduce(into: []) { result, element in + result.append(contentsOf: element) + } } } @@ -24,14 +27,8 @@ public extension Array where Element: Hashable { public extension Sequence { /// Returns a sorted version of self by a comparable element keypath - func sorted(by keyPath: KeyPath, increasingOrder: Bool = true) -> [Element] { - let sorted = self.sorted { $0[keyPath: keyPath] < $1[keyPath: keyPath] } - return increasingOrder ? sorted : sorted.reversed() - } - - /// Returns the first matched element, where the value of the property is equal to other - func firstMatch(on keyPath: KeyPath, with other: E) -> Element? { - first(where: { $0[keyPath: keyPath] == other }) + func sorted(by keyPath: KeyPath) -> [Element] { + self.sorted { $0[keyPath: keyPath] < $1[keyPath: keyPath] } } } diff --git a/Sources/ApodiniMigratorShared/Extensions/Encodable+Extensions.swift b/Sources/ApodiniMigratorShared/Extensions/Encodable+Extensions.swift index 61672e10..57ed0ddd 100644 --- a/Sources/ApodiniMigratorShared/Extensions/Encodable+Extensions.swift +++ b/Sources/ApodiniMigratorShared/Extensions/Encodable+Extensions.swift @@ -41,10 +41,20 @@ public extension Encodable { /// Writes self at the specified path with the defined format @discardableResult func write(at path: String, outputFormat: OutputFormat = .json, fileName: String? = nil) throws -> String { - let location = path.asPath + let location = Path(path) try location.mkpath() let filePath = location + "\(fileName ?? String(describing: Self.self)).\(outputFormat.rawValue)" try filePath.write(outputFormat.string(of: self)) return filePath.absolute().string } } + +// MARK: - KeyedEncodingContainerProtocol +extension KeyedEncodingContainerProtocol { + /// Only encodes the value if the collection is not empty + public mutating func encodeIfNotEmpty(_ value: T, forKey key: Key) throws where T: Collection, T.Element: Encodable { + if !value.isEmpty { + try encode(value, forKey: key) + } + } +} diff --git a/Sources/ApodiniMigratorShared/Extensions/String+Extensions.swift b/Sources/ApodiniMigratorShared/Extensions/String+Extensions.swift index c3efae08..824fe38d 100644 --- a/Sources/ApodiniMigratorShared/Extensions/String+Extensions.swift +++ b/Sources/ApodiniMigratorShared/Extensions/String+Extensions.swift @@ -10,21 +10,11 @@ import Foundation import PathKit public extension String { - /// Indicates whether self is not empty - var isNotEmpty: Bool { - !isEmpty - } - /// Line break static var lineBreak: String { "\n" } - /// Double line break - static var doubleLineBreak: String { - .lineBreak + .lineBreak - } - /// `self` wrapped with double quotes var doubleQuoted: String { "\"\(self)\"" @@ -59,67 +49,8 @@ public extension String { return self } - /// Path out of `self` - var asPath: Path { - Path(self) - } - - /// Splits the string by a character and returns the result as a String array - func split(character: Character) -> [String] { - split(separator: character).map { String($0) } - } - - /// Splits `self` by the passed string - /// - Parameters: - /// - string: separator - /// - ignoreEmptyComponents: flag whether empty components should be ignored, `false` by default - /// - Returns: the array of string components - func split(string: String, ignoreEmptyComponents: Bool = false) -> [String] { - components(separatedBy: string).filter { ignoreEmptyComponents ? $0.isNotEmpty : true } - } - - /// Returns the lines of a string - func lines() -> [String] { - split(string: .lineBreak) - } - - /// Returns lines of self separated by `\n`, and trimming whitespace characters - func sanitizedLines() -> [String] { - // splitting the string, empty lines are mapped into empty string array elements - split(string: .lineBreak).reduce(into: [String]()) { result, current in - let trimmed = current.trimmingCharacters(in: .whitespaces) - if !(result.last?.isEmpty == true && trimmed.isEmpty) { // not allowing double empty lines - result.append(trimmed) - } - } - } - - /// Replaces occurrencies of `string` with an empty string - func without(_ string: String) -> String { - with("", insteadOf: string) - } - - /// Replaces occurrencies of `strings` with an empty string - func without(strings: String...) -> String { - var result = self - strings.forEach { result = result.without($0) } - return result - } - - /// Replaces occurrencies of `target` with `replacement` - func with(_ replacement: String, insteadOf target: String) -> String { - replacingOccurrences(of: target, with: replacement) - } - /// Returns encoded data of `self` func data(_ encoding: Encoding = .utf8) -> Data { data(using: encoding) ?? .init() } } - -public extension Collection where Element == String { - /// Joins elements with a `\n` - var lineBreaked: String { - joined(separator: .lineBreak) - } -} diff --git a/Sources/ApodiniMigratorShared/FileExtension.swift b/Sources/ApodiniMigratorShared/FileExtension.swift index 3e1a5a9c..cf6794a8 100644 --- a/Sources/ApodiniMigratorShared/FileExtension.swift +++ b/Sources/ApodiniMigratorShared/FileExtension.swift @@ -11,27 +11,21 @@ import PathKit /// Represent different cases of file extensions public enum FileExtension: CustomStringConvertible { - /// Markdown - case markdown /// JSON case json /// YAML case yaml /// Swift case swift - /// Text - case text /// Other case other(String) /// String representation this extension public var description: String { switch self { - case .markdown: return "md" case .json: return "json" case .yaml: return "yaml" case .swift: return "swift" - case .text: return "txt" case let .other(value): return value } } @@ -57,13 +51,4 @@ public extension Path { } return try recursiveChildren().filter { $0.is(.swift) } } - - /// Returns all files in `self` and in subdirectories of `self` of `extensions` - func recursiveFiles(of extensions: FileExtension...) throws -> [Path] { - guard isDirectory else { - return [] - } - let fileExtensions = extensions.map { $0.description } - return try recursiveChildren().filter { fileExtensions.contains($0.extension ?? "") } - } } diff --git a/Sources/RESTMigrator/CoderConfiguration+Description.swift b/Sources/RESTMigrator/CoderConfiguration+Description.swift new file mode 100644 index 00000000..7332d466 --- /dev/null +++ b/Sources/RESTMigrator/CoderConfiguration+Description.swift @@ -0,0 +1,28 @@ +// +// This source file is part of the Apodini open source project +// +// SPDX-FileCopyrightText: 2019-2021 Paul Schmiedmayer and the Apodini project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + +import Foundation +import ApodiniMigratorShared + +extension DecoderConfiguration { + var networkingDescription: String { + """ + dateDecodingStrategy: .\(dateDecodingStrategy.rawValue), + dataDecodingStrategy: .\(dataDecodingStrategy.rawValue) + """ + } +} + +extension EncoderConfiguration { + var networkingDescription: String { + """ + dateEncodingStrategy: .\(dateEncodingStrategy.rawValue), + dataEncodingStrategy: .\(dataEncodingStrategy.rawValue) + """ + } +} diff --git a/Sources/RESTMigrator/DeltaIdentifier+Sanitize.swift b/Sources/RESTMigrator/DeltaIdentifier+Sanitize.swift new file mode 100644 index 00000000..9a850443 --- /dev/null +++ b/Sources/RESTMigrator/DeltaIdentifier+Sanitize.swift @@ -0,0 +1,18 @@ +// +// This source file is part of the Apodini open source project +// +// SPDX-FileCopyrightText: 2019-2021 Paul Schmiedmayer and the Apodini project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + +import Foundation + +extension DeltaIdentifier { + var swiftSanitizedName: String { + // incomplete list of special characters + rawValue + .replacingOccurrences(of: ".", with: "_") + .replacingOccurrences(of: ":", with: "_") + } +} diff --git a/Sources/RESTMigrator/Endpoint/APIFile.swift b/Sources/RESTMigrator/Endpoint/APIFile.swift new file mode 100644 index 00000000..1584729e --- /dev/null +++ b/Sources/RESTMigrator/Endpoint/APIFile.swift @@ -0,0 +1,65 @@ +// +// This source file is part of the Apodini open source project +// +// SPDX-FileCopyrightText: 2019-2021 Paul Schmiedmayer and the Apodini project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + +import Foundation +import ApodiniMigrator + +/// Represents the `API.swift` file of the client library +struct APIFile: GeneratedFile { + var fileName: Name = "API.swift" + + private let typeName = "API" + + /// All migrated endpoints of the library + @SharedNodeReference + var endpoints: [MigratedEndpoint] + + /// Initializes a new instance out all the migrated endpoints of the library + init(_ migratedEndpointsReference: SharedNodeReference<[MigratedEndpoint]>) { + self._endpoints = migratedEndpointsReference + endpoints.sort() + } + + var renderableContent: String { + FileHeaderComment() + + Import(.foundation) + "" + + MARKComment(typeName) + "\(Kind.enum.signature) \(typeName) {}" + "" + + MARKComment(.endpoints) + "\(Kind.extension.signature) \(typeName) {" + + Indent { + for migratedEndpoint in endpoints { + let endpoint = migratedEndpoint.endpoint + let nestedType = endpoint.response.nestedTypeString + var bodyInput = migratedEndpoint.parameters.map { "\($0.oldName): \($0.oldName)" } + bodyInput.append(contentsOf: DefaultEndpointInput.allCases.map { $0.keyValue }) + + migratedEndpoint.signature + + Indent { + "\(nestedType).\(endpoint.deltaIdentifier.swiftSanitizedName.lowerFirst)(" + Indent { + Joined(by: ",") { + bodyInput + } + } + ")" + } + "}" + } + } + + "}" + } +} diff --git a/Sources/ApodiniMigrator/Migrator/Endpoint/DefaultEndpointInput.swift b/Sources/RESTMigrator/Endpoint/DefaultEndpointInput.swift similarity index 100% rename from Sources/ApodiniMigrator/Migrator/Endpoint/DefaultEndpointInput.swift rename to Sources/RESTMigrator/Endpoint/DefaultEndpointInput.swift diff --git a/Sources/RESTMigrator/Endpoint/EndpointFile.swift b/Sources/RESTMigrator/Endpoint/EndpointFile.swift new file mode 100644 index 00000000..4dee16d6 --- /dev/null +++ b/Sources/RESTMigrator/Endpoint/EndpointFile.swift @@ -0,0 +1,89 @@ +// +// This source file is part of the Apodini open source project +// +// SPDX-FileCopyrightText: 2019-2021 Paul Schmiedmayer and the Apodini project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + +import Foundation +import ApodiniMigrator + +/// An object that represents an `Type+Endpoint.swift` file in the client library +class EndpointFile: GeneratedFile { + /// Suffix of endpoint files, e.g. `User+Endpoint.swift` + static let fileSuffix = "+Endpoint" + .swift + + let fileName: Name + + @SharedNodeReference + var migratedEndpoints: [MigratedEndpoint] + + /// Nested response type of endpoints that are grouped in the file, e.g `User` and `[User]` -> `User` + let typeInformation: TypeInformation + /// Kind of the file, always extension + let kind: Kind = .extension + /// Endpoints that are rendered in the file (same nested response type) + private let endpoints: [Endpoint] + /// All changes of the migration guide that belong to the `endpoints` + private let changes: [EndpointChange] + /// Imports of the file + private var imports = Import(.foundation) + + /// Initializes a new instance out of the same nested response type of `endpoints` and the `changes` that belong to those endpoints + init( + migratedEndpointsReference: SharedNodeReference<[MigratedEndpoint]>, + typeInformation: TypeInformation, + endpoints: [Endpoint], + changes: [EndpointChange] + ) { + _migratedEndpoints = migratedEndpointsReference + self.fileName = "\(typeInformation.unsafeTypeString)\(EndpointFile.fileSuffix)" + self.typeInformation = typeInformation + + self.endpoints = endpoints.sorted { lhs, rhs in + if lhs.response.unsafeTypeString == rhs.response.unsafeTypeString { + return lhs.deltaIdentifier < rhs.deltaIdentifier + } + return lhs.response.unsafeTypeString < rhs.response.unsafeTypeString + } + self.changes = changes + + if changes.contains(where: { $0.type == .removal }) { + imports.insert(.combine) + } + } + + var renderableContent: String { + FileHeaderComment() + + imports + "" + + MARKComment(.endpoints) + "\(kind.signature) \(typeInformation.unsafeFileNaming) {" + + Indent { + var first = true + for endpoint in endpoints { + if !first { + "" + "" + } else { + first = false + } + + let endpointMigrator = EndpointMethodMigrator( + endpoint, + changes: changes.of(base: endpoint) + ) + migratedEndpoints.append(endpointMigrator.migratedEndpoint) + + // rendering the method body + endpointMigrator + } + } + + "}" + } +} diff --git a/Sources/RESTMigrator/Endpoint/EndpointMethodMigrator.swift b/Sources/RESTMigrator/Endpoint/EndpointMethodMigrator.swift new file mode 100644 index 00000000..3bc3f594 --- /dev/null +++ b/Sources/RESTMigrator/Endpoint/EndpointMethodMigrator.swift @@ -0,0 +1,203 @@ +// +// This source file is part of the Apodini open source project +// +// SPDX-FileCopyrightText: 2019-2021 Paul Schmiedmayer and the Apodini project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + +import Foundation +import ApodiniMigrator + +class EndpointMethodMigrator: SourceCodeRenderable { + /// Endpoint of old version that will be migrated + private let endpoint: Endpoint + /// A flag that indicates whether the endpoint has been deleted in the new version + private let unavailable: Bool + + private var path: EndpointPath + private var operation: ApodiniMigratorCore.Operation + + private var responseString: String + /// An optional property that holds the id of the javascript convert function in case that response changed to some other type. Property set in `responseString()` + private var responseConvertID: Int? + + /// Lazy migrated endpoint property. + let migratedEndpoint: MigratedEndpoint + + /// Initializes a new instance out of an endpoint of old version and the changes that belong to `endpoint` + init(_ endpoint: Endpoint, changes: [EndpointChange]) { // swiftlint:disable:this function_body_length cyclomatic_complexity + self.endpoint = endpoint + self.unavailable = changes.contains(where: { $0.type == .removal }) + + self.path = endpoint.identifier() + self.operation = endpoint.identifier() + + self.responseString = endpoint.response.unsafeTypeString + + var parameters: [MigratedParameter] = [] + + var removedParameters: Set = [] + // index parameter changes by the parameter identifier + var updatedParameters: [DeltaIdentifier: [ParameterChange.UpdateChange]] = [:] + var renamedParameters: [DeltaIdentifier: ParameterChange.IdentifierChange] = [:] + + for endpointUpdate in changes.compactMap({ $0.modeledUpdateChange }) { + switch endpointUpdate.updated { + case let .identifier(identifierChange): + guard identifierChange.id.rawValue == Operation.identifierType + || identifierChange.id.rawValue == EndpointPath.identifierType else { + continue + } + + guard let updateChange = identifierChange.modeledUpdateChange else { + fatalError("Encountered unsupported change type for required endpoint identifier: \(identifierChange)") + } + + switch updateChange.id.rawValue { + case Operation.identifierType: + self.operation = updateChange.updated.to.typed() + case EndpointPath.identifierType: + self.path = updateChange.updated.to.typed() + default: + break + } + case let .response(from, to, migration, warning): + self.responseString = to.unsafeTypeString + self.responseConvertID = migration + case let .parameter(parameter): + if let parameterAddition = parameter.modeledAdditionChange { + parameters.append(.addedParameter(parameterAddition.added, defaultValue: parameterAddition.defaultValue)) + } else if let parameterRemoval = parameter.modeledRemovalChange { + guard let parameter = endpoint.parameters.first(where: { $0.deltaIdentifier == parameterRemoval.id }) else { + fatalError("Failed to match removal change with parameter in API document!") + } + parameters.append(.deletedParameter(parameter)) + removedParameters.insert(parameterRemoval.id) + } else if let parameterUpdate = parameter.modeledUpdateChange { + updatedParameters[parameterUpdate.id, default: []] + .append(parameterUpdate) + } else if let parameterIdentifierChange = parameter.modeledIdentifierChange { + renamedParameters[parameterIdentifierChange.from] = parameterIdentifierChange + } + default: + break + } + } + + for parameter in endpoint.parameters where !removedParameters.contains(parameter.deltaIdentifier) { + var newName: String? + var newType: TypeInformation? + var parameterType: ParameterType? + var necessityValueJSONId: Int? + var convertFromToJSONId: Int? + + if let identifierChange = renamedParameters[parameter.deltaIdentifier] { + newName = identifierChange.to.rawValue + } + + for change in updatedParameters[parameter.deltaIdentifier, default: []] { + switch change.updated { + case let .parameterType(from, to): + parameterType = to + case let .necessity(from, to, migration): + necessityValueJSONId = migration + precondition(convertFromToJSONId == nil, "Provided necessity value for a parameter that already has a convert method") + case let .type(from, to, forwardMigration, warning): + convertFromToJSONId = forwardMigration + newType = to + precondition(necessityValueJSONId == nil, "Provided a convert method for a parameter that already has a necessity value") + } + } + + parameters.append(MigratedParameter( + oldName: parameter.name, + newName: newName ?? parameter.name, + kind: parameterType ?? parameter.parameterType, + necessity: parameter.necessity, + oldType: parameter.typeInformation, + newType: newType ?? parameter.typeInformation, + convertFromTo: convertFromToJSONId, + defaultValue: nil, + necessityValueJSONId: necessityValueJSONId, + deleted: false + )) + } + + self.migratedEndpoint = MigratedEndpoint(endpoint: endpoint, unavailable: unavailable, parameters: parameters, path: path) + } + + @SourceCodeBuilder + private var returnValueString: String { + "return NetworkingService.trigger(handler)" + + if let convertID = responseConvertID { + Indent { + """ + .tryMap { try \(endpoint.response.unsafeTypeString).from($0, script: \(convertID)) } + .eraseToAnyPublisher() + """ + } + } + } + + /// Renders the body of the migrated endpoint + var renderableContent: String { + migratedEndpoint.signature + + Indent { + if unavailable { + "Future { $0(.failure(ApodiniError.deletedEndpoint())) }.eraseToAnyPublisher()" + } else { + let queryParametersString = migratedEndpoint.queryParametersString() + + if !queryParametersString.isEmpty { + queryParametersString + } + + """ + var headers = httpHeaders + headers.setContentType(to: "application/json") + + var errors: [ApodiniError] = [] + """ + for error in endpoint.errors { + "errors.addError(\(error.code), message: \"\(error.message)\")" + } + + """ + + let handler = Handler<\(responseString)>( + """ + Indent { + """ + path: "\(migratedEndpoint.resourcePath())", + httpMethod: .\(operation.asHTTPMethodString), + parameters: \(queryParametersString.isEmpty ? "[:]" : "parameters"), + headers: headers, + content: \(migratedEndpoint.contentParameterString()), + authorization: authorization, + errors: errors + """ + } + ")" + + "" + returnValueString + } + } + "}" + } +} + +// MARK: - Operation +fileprivate extension ApodiniMigratorCore.Operation { + var asHTTPMethodString: String { + switch self { + case .create: return "post" + case .read: return "get" + case .update: return "put" + case .delete: return "delete" + } + } +} diff --git a/Sources/RESTMigrator/Endpoint/EndpointsMigrator.swift b/Sources/RESTMigrator/Endpoint/EndpointsMigrator.swift new file mode 100644 index 00000000..50fe577e --- /dev/null +++ b/Sources/RESTMigrator/Endpoint/EndpointsMigrator.swift @@ -0,0 +1,61 @@ +// +// This source file is part of the Apodini open source project +// +// SPDX-FileCopyrightText: 2019-2021 Paul Schmiedmayer and the Apodini project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + +import Foundation +import ApodiniMigrator + +/// An object that handles / triggers the migrated rendering of all endpoints of the client library +struct EndpointsMigrator: LibraryComposite { + @SharedNodeReference + var migratedEndpoints: [MigratedEndpoint] + + var endpointFiles: [EndpointFile] = [] + + init( + migratedEndpointsReference: SharedNodeReference<[MigratedEndpoint]>, + document baseDocument: APIDocument, + migrationGuide: MigrationGuide + ) { + self._migratedEndpoints = migratedEndpointsReference + self.migratedEndpoints = [] + + let baseEndpoints = baseDocument.endpoints + let addedModels = migrationGuide.endpointChanges + .compactMap { $0.modeledAdditionChange } + .map { $0.added } + + let allEndpoints = baseEndpoints + addedModels + + // Grouping the endpoints based on their nested response type + let groupedEndpoints = allEndpoints.reduce(into: [String: [Endpoint]]()) { result, current in + result[current.response.nestedTypeString, default: []] + .append(current) + } + + // Iterating through all endpoint groups, and rendering one migrated Endpoint file per group + for (key, endpoints) in groupedEndpoints { + let endpointIds = endpoints.identifiers() + let changes = migrationGuide.endpointChanges + .filter { endpointIds.contains($0.id) } + + let endpointFile = EndpointFile( + migratedEndpointsReference: _migratedEndpoints, + typeInformation: .reference(key), + endpoints: endpoints, + changes: changes + ) + endpointFiles.append(endpointFile) + } + } + + var content: [LibraryComponent] { + for file in endpointFiles { + file + } + } +} diff --git a/Sources/ApodiniMigrator/Migrator/Endpoint/MigratedEndpoint.swift b/Sources/RESTMigrator/Endpoint/MigratedEndpoint.swift similarity index 70% rename from Sources/ApodiniMigrator/Migrator/Endpoint/MigratedEndpoint.swift rename to Sources/RESTMigrator/Endpoint/MigratedEndpoint.swift index 33b5976f..564eaf00 100644 --- a/Sources/ApodiniMigrator/Migrator/Endpoint/MigratedEndpoint.swift +++ b/Sources/RESTMigrator/Endpoint/MigratedEndpoint.swift @@ -7,6 +7,7 @@ // import Foundation +import ApodiniMigrator /// Represents a migrated endpoint of the client library class MigratedEndpoint { @@ -16,7 +17,7 @@ class MigratedEndpoint { private let path: EndpointPath /// A flag that indicates whether the endpoint has been deleted in the new version let unavailable: Bool - /// Migrated parameters of the client libray (all added, deleted, renamed or updated parameters) + /// Migrated parameters of the client library (all added, deleted, renamed or updated parameters) let parameters: [MigratedParameter] /// Only `parameters` that have not been deleted, that should be considered in the method body @@ -26,7 +27,7 @@ class MigratedEndpoint { /// Response string of the endpoint in the old version private var responseString: String { - endpoint.response.typeString + endpoint.response.unsafeTypeString } /// Initializes a new instance out of an endpoint of the old version, unavailable flag, migrated parameters and the path of the endpoint in the new version @@ -40,24 +41,19 @@ class MigratedEndpoint { /// Returns the input string of the endpoint method considering added parameters and providing default values for those private func methodInput() -> String { var input = parameters.map { parameter -> String in - let typeString = parameter.oldType.typeString + (parameter.necessity == .optional ? "?" : "") + let typeString = parameter.oldType.unsafeTypeString + (parameter.necessity == .optional ? "?" : "") var parameterSignature = "\(parameter.oldName): \(typeString)" if let defaultValue = parameter.defaultValue { - let defaultValueString: String - if case let .json(id) = defaultValue { - defaultValueString = "try! \(typeString).instance(from: \(id))" - } else { - defaultValueString = "nil" - } - parameterSignature += " = \(defaultValueString)" + parameterSignature += " = try! \(typeString).instance(from: \(defaultValue))" } return parameterSignature } input.append(contentsOf: DefaultEndpointInput.allCases.map { $0.signature }) - - return .lineBreak + input.joined(separator: ",\(String.lineBreak)") + .lineBreak + + return input + .joined(separator: ",\(String.lineBreak)") } /// Returns the `@available(*, deprecated, message:)` annotation in case that the endpoint has been deleted in the new version @@ -72,24 +68,30 @@ class MigratedEndpoint { private func setValue(for parameter: MigratedParameter) -> String { let setValue: String if let necessityValueID = parameter.necessityValueJSONId { - setValue = "\(parameter.oldName) ?? (try! \(parameter.oldType.typeString).instance(from: \(necessityValueID)))" + setValue = "\(parameter.oldName) ?? (try! \(parameter.oldType.unsafeTypeString).instance(from: \(necessityValueID)))" } else if let convertID = parameter.convertFromTo { - setValue = "try! \(parameter.newType.typeString).from(\(parameter.oldName), script: \(convertID))" + setValue = "try! \(parameter.newType.unsafeTypeString).from(\(parameter.oldName), script: \(convertID))" } else { setValue = "\(parameter.oldName)" } return setValue } - + /// Returns the signature of the endpoint method - func signature() -> String { - let methodName = endpoint.deltaIdentifier.rawValue.lowerFirst - let signature = - """ - \(EndpointComment(endpoint.handlerName, path: resourcePath(replaceBrackets: false))) - \(unavailableComment())static func \(methodName)(\(methodInput())) -> ApodiniPublisher<\(responseString)> { - """ - return signature + @SourceCodeBuilder + var signature: String { + let methodName = endpoint.deltaIdentifier.swiftSanitizedName.lowerFirst + + EndpointComment( + endpoint.handlerName.buildName(componentSeparator: ".", genericsStart: "<", genericsSeparator: ",", genericsDelimiter: ">"), + path: resourcePath(replaceBrackets: false) + ) + + "\(unavailableComment())static func \(methodName)(" + Indent { + methodInput() + } + ") -> ApodiniPublisher<\(responseString)> {" } /// Returns the adjusted resource path by considering potential renamings of the parameters in the new version and replacing them accordingly @@ -97,16 +99,14 @@ class MigratedEndpoint { var resourcePath = path.resourcePath for pathParameter in activeParameters.filter({ $0.kind == .path }) { - resourcePath = resourcePath.with("{\(pathParameter.oldName)}", insteadOf: "{\(pathParameter.newName)}") + resourcePath = resourcePath + .replacingOccurrences(of: "{\(pathParameter.newName)}", with: "{\(pathParameter.oldName)}") } - return replaceBrackets ? resourcePath.with("\\(", insteadOf: "{").with(")", insteadOf: "}") : resourcePath - } - - /// Returns the endpoint method with unavailable comment and a fatalError body - func unavailableBody() -> String { - var body = signature() - body += .lineBreak + "Future { $0(.failure(ApodiniError.deletedEndpoint())) }.eraseToAnyPublisher()" + .lineBreak + "}" - return body + return replaceBrackets + ? resourcePath + .replacingOccurrences(of: "{", with: "\\(") + .replacingOccurrences(of: "}", with: ")") + : resourcePath } /// Returns the query parameters string that should be rendered inside of the method body, considering only non-deleted query parameters @@ -122,12 +122,12 @@ class MigratedEndpoint { body += "parameters.set(\(setValue(for: parameter)), forKey: \(parameter.newName.doubleQuoted))" + .lineBreak } - return body + .lineBreak + return body } /// Returns the string that should be used in the `content` field of the handler initializer inside of the endpoint method, by only considering active content parameter func contentParameterString() -> String { - guard let contentParameter = activeParameters.firstMatch(on: \.kind, with: .content) else { + guard let contentParameter = activeParameters.first(where: { $0.kind == .content }) else { return "nil" } @@ -150,9 +150,9 @@ extension MigratedEndpoint: Comparable { static func < (lhs: MigratedEndpoint, rhs: MigratedEndpoint) -> Bool { let lhsEndpoint = lhs.endpoint let rhsEndpoint = rhs.endpoint - if lhsEndpoint.response.typeString == rhsEndpoint.response.typeString { + if lhsEndpoint.response.unsafeTypeString == rhsEndpoint.response.unsafeTypeString { return lhsEndpoint.deltaIdentifier < rhsEndpoint.deltaIdentifier } - return lhsEndpoint.response.typeString < rhsEndpoint.response.typeString + return lhsEndpoint.response.unsafeTypeString < rhsEndpoint.response.unsafeTypeString } } diff --git a/Sources/ApodiniMigrator/Migrator/Endpoint/MigratedParameter.swift b/Sources/RESTMigrator/Endpoint/MigratedParameter.swift similarity index 93% rename from Sources/ApodiniMigrator/Migrator/Endpoint/MigratedParameter.swift rename to Sources/RESTMigrator/Endpoint/MigratedParameter.swift index f266c9a3..9aa608ac 100644 --- a/Sources/ApodiniMigrator/Migrator/Endpoint/MigratedParameter.swift +++ b/Sources/RESTMigrator/Endpoint/MigratedParameter.swift @@ -26,8 +26,8 @@ struct MigratedParameter: Hashable { /// - Note: only one of the properties `convertFromTo` and `necessityValueJSONId` can be non-nil at the same type let convertFromTo: Int? /// Id of the default json value for if the parameter has been added in the new version, - /// If the migration guide did not provide any defaul value for the added parameter due to optional necessity, this value is equal to `-1` - let defaultValue: ChangeValue? + /// If the migration guide did not provide any default value for the added parameter due to optional necessity, this value is equal to `-1` + let defaultValue: Int? /// Id of the necessity value json id, if the necessity of the parameter changed from optional to required /// - Note: only one of the properties `convertFromTo` and `necessityValueJSONId` can be non-nil at the same type let necessityValueJSONId: Int? @@ -35,7 +35,7 @@ struct MigratedParameter: Hashable { let deleted: Bool /// A convenience static function that returns an added `MigratedParameter` out of an `Parameter` of new version and a `jsonValueID` - static func addedParameter(_ parameter: Parameter, defaultValue: ChangeValue) -> MigratedParameter { + static func addedParameter(_ parameter: Parameter, defaultValue: Int?) -> MigratedParameter { .init( oldName: parameter.name, newName: parameter.name, diff --git a/Sources/ApodiniMigratorCompare/MigrationGuide/ServiceType.swift b/Sources/RESTMigrator/Exports.swift similarity index 68% rename from Sources/ApodiniMigratorCompare/MigrationGuide/ServiceType.swift rename to Sources/RESTMigrator/Exports.swift index 818b0c2e..6fe0f1c8 100644 --- a/Sources/ApodiniMigratorCompare/MigrationGuide/ServiceType.swift +++ b/Sources/RESTMigrator/Exports.swift @@ -8,8 +8,6 @@ import Foundation -public enum ServiceType: String, Value { - case rest = "REST" - case graphQL = "GraphQL" - case gRPC -} +@_exported import ApodiniMigratorCore +@_exported import ApodiniMigratorCompare +@_exported import ApodiniMigratorShared diff --git a/Sources/ApodiniMigrator/Migrator/Models/Enum/DefaultEnumFile.swift b/Sources/RESTMigrator/Models/Enum/DefaultEnumFile.swift similarity index 73% rename from Sources/ApodiniMigrator/Migrator/Models/Enum/DefaultEnumFile.swift rename to Sources/RESTMigrator/Models/Enum/DefaultEnumFile.swift index f28674a7..99619111 100644 --- a/Sources/ApodiniMigrator/Migrator/Models/Enum/DefaultEnumFile.swift +++ b/Sources/RESTMigrator/Models/Enum/DefaultEnumFile.swift @@ -8,9 +8,14 @@ import Foundation import ApodiniTypeInformation +import ApodiniMigrator /// Represents an `enum` file that did not got affected by any change -struct DefaultEnumFile: SwiftFile { +struct DefaultEnumFile: GeneratedFile { + var fileName: Name { + "\(typeInformation.unsafeFileNaming).swift" + } + /// The `.enum` `typeInformation` to be rendered in this file let typeInformation: TypeInformation @@ -28,12 +33,12 @@ struct DefaultEnumFile: SwiftFile { return "" } - /// Raw value type of the enum - private let rawValueType: TypeInformation - /// Enum cases of the `typeInformation` private let enumCases: [EnumCase] - + + /// Raw value type of the enum + private let rawValueType: TypeInformation + /// Deprecated cases private let deprecatedCases = EnumDeprecatedCases() @@ -62,33 +67,41 @@ struct DefaultEnumFile: SwiftFile { self.enumCases = typeInformation.enumCases.sorted(by: \.name) self.rawValueType = rawValueType } - - /// Renders and formats the `typeInformation` in an enum swift file compliant way - func render() -> String { - """ - \(fileComment) - - \(Import(.foundation).render()) - - \(MARKComment(.model)) - \(annotationComment)\(kind.signature) \(typeNameString): \(rawValueType.nestedTypeString), Codable, CaseIterable { - \(enumCases.map { "case \($0.name)" }.lineBreaked) - \(MARKComment(.deprecated)) - \(deprecatedCases.render()) + var renderableContent: String { + FileHeaderComment() + + Import(.foundation) + "" + + MARKComment(.model) + "\(annotationComment)\(kind.signature) \(typeInformation.unsafeFileNaming): \(rawValueType.nestedTypeString), Codable, CaseIterable {" + Indent { + for enumCase in enumCases { + "case \(enumCase.name)" + } + "" - \(MARKComment(.encodable)) - \(enumEncodingMethod.render()) + MARKComment(.deprecated) + deprecatedCases + "" - \(MARKComment(.decodable)) - \(enumDecoderInitializer.render()) + MARKComment(.encodable) + enumEncodingMethod + "" - \(MARKComment(.utils)) - \(encodeValueMethod.render()) + MARKComment(.decodable) + enumDecoderInitializer + "" + + MARKComment(.utils) + encodeValueMethod } - - \(EnumExtensions(typeInformation, rawValueType: rawValueType).render()) """ + } + + """ + EnumExtensions(typeInformation, rawValueType: rawValueType).render() } } diff --git a/Sources/ApodiniMigrator/Migrator/Models/Enum/EnumDecoderInitializer.swift b/Sources/RESTMigrator/Models/Enum/EnumDecoderInitializer.swift similarity index 71% rename from Sources/ApodiniMigrator/Migrator/Models/Enum/EnumDecoderInitializer.swift rename to Sources/RESTMigrator/Models/Enum/EnumDecoderInitializer.swift index 7f3644e0..06de00bf 100644 --- a/Sources/ApodiniMigrator/Migrator/Models/Enum/EnumDecoderInitializer.swift +++ b/Sources/RESTMigrator/Models/Enum/EnumDecoderInitializer.swift @@ -7,9 +7,10 @@ // import Foundation +import ApodiniMigrator /// Represents the `init(from:)` initializer of an `enum` -struct EnumDecoderInitializer: Renderable { +struct EnumDecoderInitializer: SourceCodeRenderable { /// The default enum case to be set in the initializer let defaultCase: EnumCase @@ -22,11 +23,11 @@ struct EnumDecoderInitializer: Renderable { } /// Renders the content of the initializer in a non-formatted way - func render() -> String { - """ - public init(from decoder: Decoder) throws { - self = Self(rawValue: try decoder.singleValueContainer().decode(RawValue.self)) ?? .\(defaultCase.name) + var renderableContent: String { + "public init(from decoder: Decoder) throws {" + Indent { + "self = Self(rawValue: try decoder.singleValueContainer().decode(RawValue.self)) ?? .\(defaultCase.name)" } - """ + "}" } } diff --git a/Sources/ApodiniMigrator/Migrator/Models/Enum/EnumDeprecatedCases.swift b/Sources/RESTMigrator/Models/Enum/EnumDeprecatedCases.swift similarity index 76% rename from Sources/ApodiniMigrator/Migrator/Models/Enum/EnumDeprecatedCases.swift rename to Sources/RESTMigrator/Models/Enum/EnumDeprecatedCases.swift index 90657f3d..70815bc8 100644 --- a/Sources/ApodiniMigrator/Migrator/Models/Enum/EnumDeprecatedCases.swift +++ b/Sources/RESTMigrator/Models/Enum/EnumDeprecatedCases.swift @@ -7,9 +7,10 @@ // import Foundation +import ApodiniMigrator -/// Represents the deprecetad cases static property on an `enum` declaration -struct EnumDeprecatedCases: Renderable { +/// Represents the deprecated cases static property on an `enum` declaration +struct EnumDeprecatedCases: SourceCodeRenderable { /// Name of the deprecated cases variable static let variableName = "deprecatedCases" /// String description of the static variable @@ -22,11 +23,9 @@ struct EnumDeprecatedCases: Renderable { init(deprecated: [EnumCase] = []) { self.deprecatedCaseNames = deprecated.map { $0.name } } - + /// Renders the deprecated cases static variable - func render() -> String { - """ - \(Self.base)[\(deprecatedCaseNames.map { ".\($0)" }.joined(separator: ", "))] - """ + var renderableContent: String { + "\(Self.base)[\(deprecatedCaseNames.map { ".\($0)" }.joined(separator: ", "))]" } } diff --git a/Sources/RESTMigrator/Models/Enum/EnumEncodeValueMethod.swift b/Sources/RESTMigrator/Models/Enum/EnumEncodeValueMethod.swift new file mode 100644 index 00000000..1a9c4a51 --- /dev/null +++ b/Sources/RESTMigrator/Models/Enum/EnumEncodeValueMethod.swift @@ -0,0 +1,29 @@ +// +// This source file is part of the Apodini open source project +// +// SPDX-FileCopyrightText: 2019-2021 Paul Schmiedmayer and the Apodini project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + +import Foundation +import ApodiniMigrator + +/// Represents the `encodableValue()` util method in an enum +struct EnumEncodeValueMethod: SourceCodeRenderable { + /// Renders the content of the initializer in a non-formatted way + var renderableContent: String { + "private func encodableValue() throws -> Self {" + Indent { + "let deprecated = Self.\(EnumDeprecatedCases.variableName)" + "guard deprecated.contains(self) else {" + Indent { + "return self" + } + "}" + + "throw ApodiniError(code: 404, message: \"The web service does not support the cases of this enum anymore\")" + } + "}" + } +} diff --git a/Sources/ApodiniMigrator/Migrator/Models/Enum/EnumEncodingMethod.swift b/Sources/RESTMigrator/Models/Enum/EnumEncodingMethod.swift similarity index 53% rename from Sources/ApodiniMigrator/Migrator/Models/Enum/EnumEncodingMethod.swift rename to Sources/RESTMigrator/Models/Enum/EnumEncodingMethod.swift index 46f5a79f..32545dc6 100644 --- a/Sources/ApodiniMigrator/Migrator/Models/Enum/EnumEncodingMethod.swift +++ b/Sources/RESTMigrator/Models/Enum/EnumEncodingMethod.swift @@ -7,17 +7,20 @@ // import Foundation +import ApodiniMigrator /// Represents `encode(to:)` method of an Enum object -struct EnumEncodingMethod: Renderable { +struct EnumEncodingMethod: SourceCodeRenderable { /// Renders the content of the method in a non-formatted way - func render() -> String { - """ - public func encode(to encoder: Encoder) throws { - var container = encoder.singleValueContainer() - - try container.encode(try encodableValue().rawValue) + var renderableContent: String { + "public func encode(to encoder: Encoder) throws {" + Indent { + """ + var container = encoder.singleValueContainer() + + try container.encode(try encodableValue().rawValue) + """ } - """ + "}" } } diff --git a/Sources/RESTMigrator/Models/Enum/EnumExtensions.swift b/Sources/RESTMigrator/Models/Enum/EnumExtensions.swift new file mode 100644 index 00000000..68aa5805 --- /dev/null +++ b/Sources/RESTMigrator/Models/Enum/EnumExtensions.swift @@ -0,0 +1,64 @@ +// +// This source file is part of the Apodini open source project +// +// SPDX-FileCopyrightText: 2019-2021 Paul Schmiedmayer and the Apodini project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + +import Foundation +import ApodiniTypeInformation +import ApodiniMigrator + +struct EnumExtensions: SourceCodeRenderable { + let `enum`: TypeInformation + let rawValueType: TypeInformation + var typeName: String { + `enum`.unsafeTypeString + } + + init(_ enum: TypeInformation, rawValueType: TypeInformation) { + self.enum = `enum` + self.rawValueType = rawValueType + } + + var renderableContent: String { + MARKComment("CustomStringConvertible") + "\(Kind.extension.rawValue) \(typeName): CustomStringConvertible {" + Indent { + "\(GenericComment(comment: "/// Textual representation"))" + "public var description: String {" + Indent { + "rawValue.description" + } + "}" + } + "}" + + "" + + MARKComment("LosslessStringConvertible") + "\(Kind.extension.rawValue) \(typeName): LosslessStringConvertible {" + Indent { + "\(GenericComment(comment: "/// Instantiates an instance of the conforming type from a string representation."))" + "public init?(_ description: String) {" + Indent { + if rawValueType == .scalar(.string) { + "self.init(rawValue: description)" + } else { + "if let rawValue = RawValue(description) {" + Indent { + "self.init(rawValue: rawValue)" + } + "} else {" + Indent { + "return nil" + } + "}" + } + } + "}" + } + "}" + } +} diff --git a/Sources/RESTMigrator/Models/Enum/EnumMigrator.swift b/Sources/RESTMigrator/Models/Enum/EnumMigrator.swift new file mode 100644 index 00000000..e0b41d0b --- /dev/null +++ b/Sources/RESTMigrator/Models/Enum/EnumMigrator.swift @@ -0,0 +1,153 @@ +// +// This source file is part of the Apodini open source project +// +// SPDX-FileCopyrightText: 2019-2021 Paul Schmiedmayer and the Apodini project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + +import Foundation +import ApodiniMigratorCompare +import ApodiniMigrator + +/// An object that handles the migration of an enum declaration and renders the output accordingly +struct EnumMigrator: GeneratedFile { + var fileName: Name { + "\(typeInformation.unsafeFileNaming).swift" + } + + /// Type information enum that will be rendered + private let typeInformation: TypeInformation + /// RawValue type of the enum, either int or string + private let rawValueType: TypeInformation + + + private var addedCases: [EnumCase] = [] + private var removedCases: [EnumCase] = [] + /// A dictionary holding updates of the raw values of the enum. + /// Mapping case identifier/name to case rawValue! + private var rawValueUpdates: [DeltaIdentifier: String] = [:] + + /// A flag that indicates whether enum has been deleted in the new version + private let notPresentInNewVersion: Bool + /// An unsupported change related to the enum from the migration guide, + private var unsupportedChanges: [UnsupportedChange] = [] + + /// Initializes a new instance out of an `enum` type information and its corresponding changes + init(_ typeInformation: TypeInformation, changes: [ModelChange]) { + precondition(!changes.contains(where: { $0.id != typeInformation.deltaIdentifier }), "Found unrelated changes for \(typeInformation)") + + guard typeInformation.isEnum, let rawValueType = typeInformation.sanitizedRawValueType else { + fatalError("Attempted to initialize EnumMigrator with a non enum TypeInformation \(typeInformation.rootType)") + } + + self.typeInformation = typeInformation + self.rawValueType = rawValueType + + notPresentInNewVersion = changes.contains(where: { $0.type == .removal }) + + for change in changes.compactMap({ $0.modeledUpdateChange }) { + // first step is to check for unsupported changes and mark them as such + if case .rootType = change.updated { + let unsupportedChange = Change(from: change) + .classifyUnsupported(description: """ + ApodiniMigrator is not able to handle the migration of \(change.id). \ + Change from enum to object or vice versa is currently not supported. + """) + unsupportedChanges.append(unsupportedChange) + continue + } else if case let .rawValueType(_, to) = change.updated { + let unsupportedChange = Change(from: change) + .classifyUnsupported(description: """ + The raw value type of this enum has changed to \(to.nestedTypeString). \ + ApodiniMigrator is not able to migrate this change. + """) + unsupportedChanges.append(unsupportedChange) + continue + } + + // now we analyze for case changes (additions, removal and updates) + guard case let .`case`(caseChange) = change.updated else { + continue + } + + if let caseAddition = caseChange.modeledAdditionChange { + self.addedCases.append(caseAddition.added) + } else if let caseRemoval = caseChange.modeledRemovalChange { + if let deletedCase = typeInformation.enumCases.first(where: { $0.deltaIdentifier == caseRemoval.id }) { + removedCases.append(deletedCase) + } + } else if let caseUpdate = caseChange.modeledUpdateChange, + case let .rawValue(from, to) = caseUpdate.updated { + self.rawValueUpdates[caseUpdate.id] = to + } + } + } + + /// Returns the corresponding raw value of the case, considering potential updates + private func rawValue(for case: EnumCase) -> String { + if let updated = rawValueUpdates[`case`.deltaIdentifier] { + return " = \(updated.doubleQuoted)" + } + return "" + } + + var renderableContent: String { + // some weirdness of result builder that this thingy requires an explicit initializer + var annotation: Annotation? = nil // swiftlint:disable:this redundant_optional_initialization + + if !unsupportedChanges.isEmpty { + annotation = GenericComment( + comment: "@available(*, deprecated, message: \"\(unsupportedChanges.map { $0.description }.joined(separator: "; "))\")" + ) + } else if notPresentInNewVersion { + annotation = GenericComment( + comment: "@available(*, message: \("This enum is not used in the new version anymore!".doubleQuoted))" + ) + } + + + if let annotation = annotation { + DefaultEnumFile(typeInformation, annotation: annotation) + } else { + let allCases = (typeInformation.enumCases + addedCases).sorted(by: \.name) + + var addedCasesAnnotation = "" + + if !addedCases.isEmpty { + addedCasesAnnotation = "@available(*, message: \("This enum has been migrated with new cases. The client developer should ensure to adjust potential switch blocks of this enum".doubleQuoted))" + .lineBreak + } + + FileHeaderComment() + + Import(.foundation) + "" + + MARKComment(.model) + "\(addedCasesAnnotation)\(Kind.enum.signature) \(typeInformation.unsafeFileNaming): \(rawValueType.nestedTypeString), Codable, CaseIterable {" + Indent { + for enumCase in allCases { + precondition(enumCase.name == enumCase.rawValue, "Assumption about the TypeInformation framework changed!") + "case \(enumCase.name)\(rawValue(for: enumCase))" + } + "" + + MARKComment(.deprecated) + EnumDeprecatedCases(deprecated: self.removedCases) + "" + MARKComment(.encodable) + EnumEncodingMethod() + "" + MARKComment(.decodable) + EnumDecoderInitializer(allCases) + "" + MARKComment(.utils) + EnumEncodeValueMethod() + } + "}" + + "" + EnumExtensions(typeInformation, rawValueType: rawValueType) + } + } +} diff --git a/Sources/RESTMigrator/Models/ModelsMigrator.swift b/Sources/RESTMigrator/Models/ModelsMigrator.swift new file mode 100644 index 00000000..89f8ec22 --- /dev/null +++ b/Sources/RESTMigrator/Models/ModelsMigrator.swift @@ -0,0 +1,73 @@ +// +// This source file is part of the Apodini open source project +// +// SPDX-FileCopyrightText: 2019-2021 Paul Schmiedmayer and the Apodini project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + +import Foundation +import ApodiniMigrator + +/// An object that handles / triggers the migrated rendering of enums and objects of the client library +struct ModelsMigrator: LibraryComposite { + /// Unchanged models of the client library. We can directly generate those. + private let unchangedModels: [TypeInformation] + + /// Changed models of the client library. Change descriptions reside in ``modelChanges``. + /// Models might be updated, removed or renamed. + private let changedModels: [TypeInformation] + /// All changes of the migration guide, describing changes of models saved in ``changedModels``. + private let modelChanges: [ModelChange] + + init(document baseDocument: APIDocument, migrationGuide: MigrationGuide) { + let changesIds = migrationGuide.modelChanges.map { $0.id } + + var unchangedModels: [TypeInformation] = [] + var changedModels: [TypeInformation] = [] + + // new models are per definition unchanged + let addedModels: [TypeInformation] = migrationGuide.modelChanges + .compactMap { $0.modeledAdditionChange } + .map { $0.added } + changedModels.append(contentsOf: addedModels) + + let allModels = baseDocument.models + .fileRenderableTypes() + .sorted(by: \.unsafeTypeString) + // check if the models from the base document were changed or not + for model in allModels { + if changesIds.contains(model.deltaIdentifier) { + changedModels.append(model) + } else { + unchangedModels.append(model) + } + } + + self.unchangedModels = unchangedModels + + self.changedModels = changedModels + self.modelChanges = migrationGuide.modelChanges + } + + var content: [LibraryComponent] { + // generate non-modified types without any special migration + for unchangedModelInformation in unchangedModels { + if unchangedModelInformation.isEnum { + DefaultEnumFile(unchangedModelInformation) + } else { + DefaultObjectFile(unchangedModelInformation) + } + } + + for changedModel in changedModels { + let changes = modelChanges.of(base: changedModel) + + if changedModel.isEnum { + EnumMigrator(changedModel, changes: changes) + } else if changedModel.isObject { + ObjectMigrator(changedModel, changes: changes) + } + } + } +} diff --git a/Sources/RESTMigrator/Models/Object/DecoderInitializer.swift b/Sources/RESTMigrator/Models/Object/DecoderInitializer.swift new file mode 100644 index 00000000..691d44ec --- /dev/null +++ b/Sources/RESTMigrator/Models/Object/DecoderInitializer.swift @@ -0,0 +1,92 @@ +// +// This source file is part of the Apodini open source project +// +// SPDX-FileCopyrightText: 2019-2021 Paul Schmiedmayer and the Apodini project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + +import Foundation +import ApodiniMigrator + +/// Represents `init(from decoder: Decoder)` initializer of a Decodable object +struct DecoderInitializer: SourceCodeRenderable { + /// All properties of the object that this initializer belongs to + private let properties: [TypeProperty] + /// Deleted properties of the object if any + private let removed: [PropertyChange.RemovalChange] + private let updateChanges: [PropertyChange.UpdateChange] + + /// Initializer + init( + properties: [TypeProperty], + removed: [PropertyChange.RemovalChange] = [], + changes: [PropertyChange.UpdateChange] = [] + ) { + self.properties = properties + self.removed = removed + self.updateChanges = changes + } + + /// Returns the corresponding line of `property` inside of the initializer by considering potential changes of the property + private func decodingLine(for property: TypeProperty) -> String { + if let removalChange = removed.first(where: { $0.id == property.deltaIdentifier }) { + if let fallbackValue = removalChange.fallbackValue { + return "\(property.name) = try \(property.type.unsafeTypeString).instance(from: \(fallbackValue))" + } else { + return "\(property.name) = nil" + } + } + + guard let change = updateChanges.first(where: { $0.id == property.deltaIdentifier }) else { + return property.decoderInitLine + } + + // I'm honestly not sure why we don't support both changes at the same time (and only one change per property) + // I'm just rewriting the thing and don't really have the time to fix things. + + if case let .necessity(from, to, migration) = change.updated { + if to != .optional { + return property.decoderInitLine + } + + return """ + \(property.name) = try container.decodeIfPresent\ + (\(property.type.unsafeTypeString).self, forKey: .\(property.name)) \ + ?? (try \(property.type.unsafeTypeString).instance(from: \(migration))) + """ + } else if case let .type(from, to, forwardMigration, backwardMigration, hint) = change.updated { + let decodeMethod = "decode\(to.isOptional ? "IfPresent" : "")" + return """ + \(property.name) = try \(property.type.unsafeTypeString).from(\ + try container.\(decodeMethod)(\(to.unsafeTypeString.dropQuestionMark).self, forKey: .\(property.name)), script: \(backwardMigration)\ + ) + """ + } + + return property.decoderInitLine + } + + /// Renders the content of the initializer in a non-formatted way + var renderableContent: String { + "public init(from decoder: Decoder) throws {" + Indent { + "let container = try decoder.container(keyedBy: CodingKeys.self)" + "" + + for property in properties { + decodingLine(for: property) + } + } + "}" + } +} + +/// TypeProperty extension +private extension TypeProperty { + /// The corresponding line of the property to be rendered inside `init(from decoder: Decoder)` if no change affected the property + var decoderInitLine: String { + let decodeMethodString = "decode\(type.isOptional ? "IfPresent" : "")" + return "\(name) = try container.\(decodeMethodString)(\(type.unsafeTypeString.dropQuestionMark).self, forKey: .\(name))" + } +} diff --git a/Sources/ApodiniMigrator/Migrator/Models/Object/DefaultObjectFile.swift b/Sources/RESTMigrator/Models/Object/DefaultObjectFile.swift similarity index 63% rename from Sources/ApodiniMigrator/Migrator/Models/Object/DefaultObjectFile.swift rename to Sources/RESTMigrator/Models/Object/DefaultObjectFile.swift index 112b64a9..99dfc772 100644 --- a/Sources/ApodiniMigrator/Migrator/Models/Object/DefaultObjectFile.swift +++ b/Sources/RESTMigrator/Models/Object/DefaultObjectFile.swift @@ -7,9 +7,14 @@ // import Foundation +import ApodiniMigrator /// Represents an `object` file that was not affected by any change -struct DefaultObjectFile: ObjectSwiftFile { +struct DefaultObjectFile: GeneratedFile { + var fileName: Name { + "\(typeInformation.unsafeFileNaming).swift" + } + /// `TypeInformation` to be rendered in this file let typeInformation: TypeInformation @@ -41,12 +46,12 @@ struct DefaultObjectFile: ObjectSwiftFile { /// Encoding method of the object private var encodingMethod: EncodingMethod { - .init(properties) + .init(properties: properties) } /// Decoder initializer of the object private var decoderInitializer: DecoderInitializer { - .init(properties) + .init(properties: properties) } /// Initializer @@ -55,46 +60,48 @@ struct DefaultObjectFile: ObjectSwiftFile { /// - kind: the kind of the file, if other than .struct or .class is passed, .struct is chosen by default /// - annotation: an annotation on the object, e.g. if the model is not present in the new version anymore init(_ typeInformation: TypeInformation, kind: Kind = .struct, annotation: Annotation? = nil) { - precondition([.struct, .class].contains(kind) && typeInformation.isObject, "Can't initialize an ObjectFile with a non object type information or file other than struct or class") + precondition([.struct, .class].contains(kind) && typeInformation.isObject, + "Can't initialize an ObjectFile with a non object type information or file other than struct or class") self.typeInformation = typeInformation self.kind = kind self.properties = typeInformation.objectProperties.sorted(by: \.name) self.annotation = annotation } - - /// Renders and formats the `typeInformation` in a swift file compliant way - func render() -> String { - if properties.isEmpty { - let content = - """ - \(fileHeader(annotation: annotationComment)) - \(MARKComment(.initializer)) - public init() {} - } - """ - return content - } else { - let content = - """ - \(fileHeader(annotation: annotationComment)) - \(MARKComment(.codingKeys)) - \(codingKeysEnum.render()) - - \(MARKComment(.properties)) - \(properties.map { $0.propertyLine }.lineBreaked) - \(MARKComment(.initializer)) - \(objectInitializer.render()) - - \(MARKComment(.encodable)) - \(encodingMethod.render()) - - \(MARKComment(.decodable)) - \(decoderInitializer.render()) + var renderableContent: String { + FileHeaderComment() + + Import(.foundation) + "" + + MARKComment(.model) + "\(annotationComment)\(kind.signature) \(typeInformation.unsafeFileNaming): Codable {" + + Indent { + if properties.isEmpty { + MARKComment(.initializer) + "public init() {}" + } else { + MARKComment(.codingKeys) + codingKeysEnum + "" + MARKComment(.properties) + for property in properties { + property.propertyLine } - """ - return content + "" + MARKComment(.initializer) + objectInitializer + "" + MARKComment(.encodable) + encodingMethod + "" + MARKComment(.decodable) + decoderInitializer + } } + + "}" } } @@ -102,6 +109,6 @@ struct DefaultObjectFile: ObjectSwiftFile { extension TypeProperty { /// The corresponding line of the property to be rendered under the list of properties of the object var propertyLine: String { - "public var \(name): \(type.typeString)" + "public var \(name): \(type.unsafeTypeString)" } } diff --git a/Sources/RESTMigrator/Models/Object/EncodingMethod.swift b/Sources/RESTMigrator/Models/Object/EncodingMethod.swift new file mode 100644 index 00000000..e5396553 --- /dev/null +++ b/Sources/RESTMigrator/Models/Object/EncodingMethod.swift @@ -0,0 +1,76 @@ +// +// This source file is part of the Apodini open source project +// +// SPDX-FileCopyrightText: 2019-2021 Paul Schmiedmayer and the Apodini project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + +import Foundation +import ApodiniMigrator + +/// Represents `encode(to:)` method of an Encodable object +struct EncodingMethod: SourceCodeRenderable { + /// The properties of the object that this method belongs to (not including deleted ones) + private let properties: [TypeProperty] + private let updateChanges: [PropertyChange.UpdateChange] + + /// Initializer for a new instance with non-deleted properties of the object, necessity changes and convert changes + init(properties: [TypeProperty], changes: [PropertyChange.UpdateChange] = []) { + self.properties = properties + self.updateChanges = changes + } + + /// Returns the corresponding line of the property inside of the method by considering all changes related with the property + private func encodingLine(for property: TypeProperty) -> String { + guard let change = updateChanges.first(where: { $0.id == property.deltaIdentifier }) else { + return property.encodingMethodLine + } + + // I'm honestly not sure why we don't support both changes at the same time (and only one change per property) + // I'm just rewriting the thing and don't really have the time to fix things. + + if case let .necessity(from, to, migration) = change.updated { + if to != .required { + return property.encodingMethodLine + } + + return """ + try container.encode(\(property.name) \ + ?? (try \(property.type.unwrapped.unsafeTypeString)\ + .instance(from: \(migration))), forKey: .\(property.name)) + """ + } else if case let .type(from, to, forwardMigration, backwardMigration, hint) = change.updated { + let encodeMethod = "encode\(to.isOptional ? "IfPresent" : "")" + return """ + try container.\(encodeMethod)(try \(to.unsafeTypeString)\ + .from(\(property.name), script: \(forwardMigration)), forKey: .\(property.name)) + """ + } + + return property.encodingMethodLine + } + + /// Renders the content of the method in a non-formatted way + var renderableContent: String { + "public func encode(to encoder: Encoder) throws {" + Indent { + "var container = encoder.container(keyedBy: CodingKeys.self)" + "" + + for property in properties { + encodingLine(for: property) + } + } + "}" + } +} + +/// TypeProperty extension +private extension TypeProperty { + /// The corresponding line of the property to be rendered inside `encode(to:)` method if the property is not affected by any change + var encodingMethodLine: String { + let encodeMethodString = "encode\(type.isOptional ? "IfPresent" : "")" + return "try container.\(encodeMethodString)(\(name), forKey: .\(name))" + } +} diff --git a/Sources/ApodiniMigrator/Migrator/Models/Object/ObjectCodingKeys.swift b/Sources/RESTMigrator/Models/Object/ObjectCodingKeys.swift similarity index 54% rename from Sources/ApodiniMigrator/Migrator/Models/Object/ObjectCodingKeys.swift rename to Sources/RESTMigrator/Models/Object/ObjectCodingKeys.swift index 12607d4b..d1a3bf58 100644 --- a/Sources/ApodiniMigrator/Migrator/Models/Object/ObjectCodingKeys.swift +++ b/Sources/RESTMigrator/Models/Object/ObjectCodingKeys.swift @@ -7,9 +7,10 @@ // import Foundation +import ApodiniMigrator /// Represents the CodingKeys enum defined inside an object file -struct ObjectCodingKeys: Renderable { +struct ObjectCodingKeys: SourceCodeRenderable { /// An enumeration `typeInformation` of the object, name is always `CodingKeys` private let codingKeysEnum: TypeInformation @@ -19,27 +20,27 @@ struct ObjectCodingKeys: Renderable { } /// Initializer of the coding keys from all properties of the object and potential renaming changes - init(_ properties: [TypeProperty], renameChanges: [UpdateChange] = []) { - let renames = renameChanges.reduce(into: [String: String]()) { result, current in - if case let .stringValue(oldName) = current.from, case let .stringValue(newName) = current.to { - result[oldName] = newName - } + init(_ properties: [TypeProperty], renameChanges: [PropertyChange.IdentifierChange] = []) { + let renameMap = renameChanges.reduce(into: [:]) { result, value in + result[value.from.rawValue] = value.to.rawValue } let allCases: [EnumCase] = properties.map { property in - let rawValue = renames[property.name] ?? property.name + let rawValue = renameMap[property.name] ?? property.name return .init(property.name, rawValue: rawValue) } - codingKeysEnum = .enum(name: .init(name: "CodingKeys"), rawValueType: .scalar(.string), cases: allCases.sorted(by: \.name)) + codingKeysEnum = .enum(name: .init(rawValue: "CodingKeys"), rawValueType: .scalar(.string), cases: allCases.sorted(by: \.name)) } /// Renders the content of the enum, in a non-formatted way - func render() -> String { - """ - private enum \(codingKeysEnum.typeName.name): String, CodingKey { - \(enumCases.map { "case \($0.name)\($0.rawValue == $0.name ? "" : " = \($0.rawValue.doubleQuoted)")" }.lineBreaked) + var renderableContent: String { + "private enum \(codingKeysEnum.typeName.mangledName): String, CodingKey {" + Indent { + for enumCase in enumCases { + "case \(enumCase.name)\(enumCase.rawValue == enumCase.name ? "" : " = \"\(enumCase.rawValue)\"")" + } } - """ + "}" } } diff --git a/Sources/ApodiniMigrator/Migrator/Models/Object/ObjectInitializer.swift b/Sources/RESTMigrator/Models/Object/ObjectInitializer.swift similarity index 63% rename from Sources/ApodiniMigrator/Migrator/Models/Object/ObjectInitializer.swift rename to Sources/RESTMigrator/Models/Object/ObjectInitializer.swift index f2843354..40b8ab23 100644 --- a/Sources/ApodiniMigrator/Migrator/Models/Object/ObjectInitializer.swift +++ b/Sources/RESTMigrator/Models/Object/ObjectInitializer.swift @@ -7,43 +7,50 @@ // import Foundation +import ApodiniMigrator /// Represents initializer of an object -struct ObjectInitializer: Renderable { +struct ObjectInitializer: SourceCodeRenderable { /// All properties of the object that this initializer belongs to (including added and deleted properties private let properties: [TypeProperty] /// Dictionary of default values of the added properties of the object - private var defaultValues: [DeltaIdentifier: ChangeValue] + private var defaultValues: [DeltaIdentifier: Int?] /// Initializes a new instance out of old properties of the object and the added properties - init(_ properties: [TypeProperty], addedProperties: [AddedProperty] = []) { + init(_ properties: [TypeProperty], addedProperties: [PropertyChange.AdditionChange] = []) { var allProperties = properties defaultValues = [:] for added in addedProperties { - defaultValues[added.typeProperty.deltaIdentifier] = added.defaultValue - allProperties.append(added.typeProperty) + defaultValues[added.id] = added.defaultValue + allProperties.append(added.added) } self.properties = allProperties.sorted(by: \.name) } /// Renders the content of the initializer in a non-formatted way - func render() -> String { - """ - public init( - \(properties.map { "\($0.name): \(defaultValue(for: $0))" }.joined(separator: ",\(String.lineBreak)")) - ) { - \(properties.map { "\($0.initLine)" }.lineBreaked) + var renderableContent: String { + "public init(" + Indent { + properties + .map { "\($0.name): \(defaultValue(for: $0))" } + .joined(separator: ",\n") } - """ + ") {" + Indent { + for property in properties { + property.initLine + } + } + "}" } /// Returns the string of the type of the property appending a corresponding default value for added properties as provided in the migration guide private func defaultValue(for property: TypeProperty) -> String { - var typeString = property.type.typeString - if let defaultValue = defaultValues[property.deltaIdentifier] { + var typeString = property.type.unsafeTypeString + if let defaultValueEntry: Int? = defaultValues[property.deltaIdentifier] { let defaultValueString: String - if case let .json(id) = defaultValue { - defaultValueString = "try! \(typeString).instance(from: \(id))" + if let defaultValue = defaultValueEntry { + defaultValueString = "try! \(typeString).instance(from: \(defaultValue))" } else { defaultValueString = "nil" } @@ -54,7 +61,7 @@ struct ObjectInitializer: Renderable { } /// TypeProperty extension -extension TypeProperty { +private extension TypeProperty { /// The corresponding line of the property to be rendered inside `init` var initLine: String { "self.\(name) = \(name)" diff --git a/Sources/RESTMigrator/Models/Object/ObjectMigrator.swift b/Sources/RESTMigrator/Models/Object/ObjectMigrator.swift new file mode 100644 index 00000000..046047d1 --- /dev/null +++ b/Sources/RESTMigrator/Models/Object/ObjectMigrator.swift @@ -0,0 +1,149 @@ +// +// This source file is part of the Apodini open source project +// +// SPDX-FileCopyrightText: 2019-2021 Paul Schmiedmayer and the Apodini project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + +import Foundation +import ApodiniMigrator + +/// An object that handles the migration of an object in the client library +struct ObjectMigrator: GeneratedFile { + var fileName: Name { + "\(typeInformation.unsafeFileNaming).swift" + } + + /// Type information of the object that will be migrated + var typeInformation: TypeInformation + /// Kind of the file, either object or struct + var kind: Kind + + /// An unsupported change related to this object if any contained in the migration guide + private var unsupportedChanges: [UnsupportedChange] = [] + /// A flag that indicates whether the object is present in the new version or not + private let notPresentInNewVersion: Bool + + /// All old properties of the object + private let basedProperties: [TypeProperty] + + /// Holds all renamed property changes. It maps the old property identifier to the new identifier. + private var renamedProperties: [PropertyChange.IdentifierChange] = [] + /// All properties that have been added in the new version + private var addedProperties: [PropertyChange.AdditionChange] = [] + /// All properties that have been deleted in the new version + private var removedProperties: [PropertyChange.RemovalChange] = [] + /// All properties that have been updated in the new version (necessity or type). + private var updatedProperties: [PropertyChange.UpdateChange] = [] + + /// Initializes a new instance out of an object type information, kind of the file and the changes related to the object + init(_ typeInformation: TypeInformation, kind: Kind = .struct, changes: [ModelChange]) { + precondition([.struct, .class].contains(kind) && typeInformation.isObject, + "Can't initialize an ObjectMigrator with a non object type information or file other than struct or class") + precondition(!changes.contains(where: { $0.id != typeInformation.deltaIdentifier }), "Found unrelated changes for \(typeInformation)") + + self.typeInformation = typeInformation + self.kind = kind + self.basedProperties = typeInformation.objectProperties + + self.notPresentInNewVersion = changes.contains(where: { $0.type == .removal }) + + for change in changes.compactMap({ $0.modeledUpdateChange }) { + // first step is to check for unsupported changes and mark them as such + if case .rootType = change.updated { + let unsupportedChange = Change(from: change) + .classifyUnsupported(description: """ + ApodiniMigrator is not able to handle the migration of \(change.id). \ + Change from enum to object or vice versa is currently not supported. + """) + unsupportedChanges.append(unsupportedChange) + continue + } + + // now we analyze for property changes (additions, removal and updates) + guard case let .property(propertyChange) = change.updated else { + continue + } + + if let idChange = propertyChange.modeledIdentifierChange { + self.renamedProperties.append(idChange) + } else if let propertyAddition = propertyChange.modeledAdditionChange { + self.addedProperties.append(propertyAddition) + } else if let propertyRemoval = propertyChange.modeledRemovalChange { + self.removedProperties.append(propertyRemoval) + } else if let propertyUpdate = propertyChange.modeledUpdateChange { + self.updatedProperties.append(propertyUpdate) + } + } + } + + var renderableContent: String { + // some weirdness of result builder that this thingy requires an explicit initializer + var annotation: Annotation? = nil // swiftlint:disable:this redundant_optional_initialization + if !unsupportedChanges.isEmpty { + annotation = GenericComment( + comment: "@available(*, deprecated, message: \"\(unsupportedChanges.map { $0.description }.joined(separator: "; "))\")" + ) + } else if notPresentInNewVersion { + annotation = GenericComment( + comment: "@available(*, deprecated, message: \"This model is not used in the new version anymore!\")" + ) + } + + + if (basedProperties.isEmpty && addedProperties.isEmpty) || annotation != nil { + DefaultObjectFile(typeInformation, annotation: annotation) + } else { + let allProperties = (basedProperties + addedProperties.map(\.added)) + .sorted(by: \.name) + + let objectInitializer = ObjectInitializer(basedProperties, addedProperties: addedProperties) + + let encodingMethod = EncodingMethod( + properties: allProperties.filter { property in + !removedProperties.contains(where: { $0.id == property.deltaIdentifier }) + }, + changes: updatedProperties + ) + + let decoderInitializer = DecoderInitializer( + properties: allProperties, + removed: removedProperties, + changes: updatedProperties + ) + + + FileHeaderComment() + + Import(.foundation) + "" + + MARKComment(.model) + "\(annotation?.comment ?? "")\(kind.signature) \(typeInformation.unsafeFileNaming): Codable {" + Indent { + MARKComment(.codingKeys) + ObjectCodingKeys(allProperties, renameChanges: renamedProperties) + "" + + MARKComment(.properties) + for property in allProperties { + property.propertyLine + } + "" + + MARKComment(.initializer) + objectInitializer + "" + + MARKComment(.encodable) + encodingMethod + "" + + MARKComment(.decodable) + decoderInitializer + } + "}" + } + } +} diff --git a/Sources/RESTMigrator/Networking/NetworkingMigrator.swift b/Sources/RESTMigrator/Networking/NetworkingMigrator.swift new file mode 100644 index 00000000..161ee485 --- /dev/null +++ b/Sources/RESTMigrator/Networking/NetworkingMigrator.swift @@ -0,0 +1,68 @@ +// +// This source file is part of the Apodini open source project +// +// SPDX-FileCopyrightText: 2019-2021 Paul Schmiedmayer and the Apodini project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + +import Foundation +import ApodiniMigratorCompare + +internal extension HTTPInformation { + var urlFormatted: String { + "http://\(description)" + } +} + +struct NetworkingMigrator { + let baseServiceInformation: ServiceInformation + let serviceChanges: [ServiceInformationChange] + + private var exporterConfiguration: RESTExporterConfiguration { + baseServiceInformation.exporter() + } + + func serverPath() -> String { + var serverPath = baseServiceInformation.http.urlFormatted + + for change in serviceChanges { + if let update = change.modeledUpdateChange, + case let .http(from, to) = update.updated { + serverPath = to.urlFormatted + } + } + + return serverPath + } + + func encoderConfiguration() -> EncoderConfiguration { + var encoderConfiguration = exporterConfiguration.encoderConfiguration + + for change in serviceChanges { + if let update = change.modeledUpdateChange, + case let .exporter(exporter) = update.updated, + let exporterUpdate = exporter.modeledUpdateChange, + let exporter = exporterUpdate.updated.to.tryTyped(of: RESTExporterConfiguration.self) { + encoderConfiguration = exporter.encoderConfiguration + } + } + + return encoderConfiguration + } + + func decoderConfiguration() -> DecoderConfiguration { + var decoderConfiguration = exporterConfiguration.decoderConfiguration + + for change in serviceChanges { + if let update = change.modeledUpdateChange, + case let .exporter(exporter) = update.updated, + let exporterUpdate = exporter.modeledUpdateChange, + let exporter = exporterUpdate.updated.to.tryTyped(of: RESTExporterConfiguration.self) { + decoderConfiguration = exporter.decoderConfiguration + } + } + + return decoderConfiguration + } +} diff --git a/Sources/RESTMigrator/RESTMigrator.swift b/Sources/RESTMigrator/RESTMigrator.swift new file mode 100644 index 00000000..7ff8bd3c --- /dev/null +++ b/Sources/RESTMigrator/RESTMigrator.swift @@ -0,0 +1,141 @@ +// +// This source file is part of the Apodini open source project +// +// SPDX-FileCopyrightText: 2019-2021 Paul Schmiedmayer and the Apodini project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + +import Foundation +import Logging +import ApodiniMigrator + +public struct RESTMigrator: ApodiniMigrator.Migrator { + enum MigratorError: Error { + case incompatible(message: String) + } + + public var bundle = Bundle.module + + public static let logger: Logger = { + .init(label: "org.apodini.migrator.rest") + }() + + private let document: APIDocument + private let migrationGuide: MigrationGuide + + /// Networking migrator + private let networkingMigrator: NetworkingMigrator + + @SharedNodeStorage + var apiFileMigratedEndpoints: [MigratedEndpoint] + + public init(documentPath: String, migrationGuidePath: String? = nil) throws { + try self.document = APIDocument.decode(from: Path(documentPath)) + + if let path = migrationGuidePath { + try self.migrationGuide = MigrationGuide.decode(from: Path(path)) + } else { + self.migrationGuide = .empty(id: document.id) + } + + guard migrationGuide.id == document.id else { + throw MigratorError.incompatible( + message: """ + Migration guide is not compatible with the provided document. Apparently another old document version, \ + has been used to generate the migration guide! + """ + ) + } + + if document.serviceInformation.exporterIfPresent(for: RESTExporterConfiguration.self, migrationGuide: migrationGuide) == nil { + throw MigratorError.incompatible( + message: """ + RESTMigrator is not compatible with the provided documents. The web service either \ + hasn't a REST interface configured, or it was removed in the latest version! + """ + ) + } + + networkingMigrator = NetworkingMigrator( + baseServiceInformation: document.serviceInformation, + serviceChanges: migrationGuide.serviceChanges + ) + } + + public var library: RootDirectory { + let encoderConfiguration = networkingMigrator.encoderConfiguration() + let decoderConfiguration = networkingMigrator.decoderConfiguration() + + Sources { + Target(.packageName) { + Directory("Endpoints") { + EndpointsMigrator( + migratedEndpointsReference: $apiFileMigratedEndpoints, + document: document, + migrationGuide: migrationGuide + ) + } + + Directory("HTTP") { + ResourceFile(copy: "ApodiniError.swift", filePrefix: { FileHeaderComment() }) + ResourceFile(copy: "HTTPAuthorization.swift", filePrefix: { FileHeaderComment() }) + ResourceFile(copy: "HTTPHeaders.swift", filePrefix: { FileHeaderComment() }) + ResourceFile(copy: "HTTPMethod.swift", filePrefix: { FileHeaderComment() }) + ResourceFile(copy: "Parameters.swift", filePrefix: { FileHeaderComment() }) + } + + Directory("Models") { + ModelsMigrator( + document: document, + migrationGuide: migrationGuide + ) + } + + Directory("Networking") { + ResourceFile(copy: "Handler.swift", filePrefix: { FileHeaderComment() }) + ResourceFile(copy: "NetworkingService.swift", filePrefix: { FileHeaderComment() }) + .replacing(Placeholder("serverpath"), with: networkingMigrator.serverPath()) + .replacing(Placeholder("encoder___configuration"), with: encoderConfiguration.networkingDescription) + .replacing(Placeholder("decoder___configuration"), with: decoderConfiguration.networkingDescription) + } + + Directory("Resources") { + StringFile(name: "js-convert-scripts.json", content: migrationGuide.scripts.json) + StringFile(name: "json-values.json", content: migrationGuide.jsonValues.json) + } + + Directory("Utils") { + ResourceFile(copy: "Utils.swift", filePrefix: { FileHeaderComment() }) + } + + APIFile($apiFileMigratedEndpoints) + } + .dependency(product: "ApodiniMigratorClientSupport", of: "ApodiniMigrator") + .resource(type: .process, path: "Resources") + } + + Tests { + TestTarget("\(.packageName)Tests") { + ModelTestsFile( + name: "\(.packageName)Tests.swift", + models: document.models.fileRenderableTypes(), + objectJSONs: migrationGuide.objectJSONs, + encoderConfiguration: encoderConfiguration + ) + + ResourceFile(copy: "XCTestManifests.swift", filePrefix: { FileHeaderComment() }) + } + .dependency(target: .packageName) + + StubLinuxMainFile(prefix: { FileHeaderComment() }) + } + + SwiftPackageFile(swiftTools: "5.5") + .platform(".macOS(.v12)", ".iOS(.v14)") + .dependency(url: "https://github.com/Apodini/ApodiniMigrator.git", ".upToNextMinor(from: \"0.1.0\")") + .product(library: .packageName, targets: .packageName) + + ReadMeFile("Readme.md") + } +} diff --git a/Sources/ApodiniMigrator/Templates/HTTP/ApodiniError.md b/Sources/RESTMigrator/Resources/HTTP/ApodiniError.swift similarity index 100% rename from Sources/ApodiniMigrator/Templates/HTTP/ApodiniError.md rename to Sources/RESTMigrator/Resources/HTTP/ApodiniError.swift diff --git a/Sources/ApodiniMigrator/Templates/HTTP/ApodiniError.md.license b/Sources/RESTMigrator/Resources/HTTP/ApodiniError.swift.license similarity index 100% rename from Sources/ApodiniMigrator/Templates/HTTP/ApodiniError.md.license rename to Sources/RESTMigrator/Resources/HTTP/ApodiniError.swift.license diff --git a/Sources/ApodiniMigrator/Templates/HTTP/HTTPAuthorization.md b/Sources/RESTMigrator/Resources/HTTP/HTTPAuthorization.swift similarity index 100% rename from Sources/ApodiniMigrator/Templates/HTTP/HTTPAuthorization.md rename to Sources/RESTMigrator/Resources/HTTP/HTTPAuthorization.swift diff --git a/Sources/ApodiniMigrator/Templates/HTTP/HTTPAuthorization.md.license b/Sources/RESTMigrator/Resources/HTTP/HTTPAuthorization.swift.license similarity index 100% rename from Sources/ApodiniMigrator/Templates/HTTP/HTTPAuthorization.md.license rename to Sources/RESTMigrator/Resources/HTTP/HTTPAuthorization.swift.license diff --git a/Sources/ApodiniMigrator/Templates/HTTP/HTTPHeaders.md b/Sources/RESTMigrator/Resources/HTTP/HTTPHeaders.swift similarity index 100% rename from Sources/ApodiniMigrator/Templates/HTTP/HTTPHeaders.md rename to Sources/RESTMigrator/Resources/HTTP/HTTPHeaders.swift diff --git a/Sources/ApodiniMigrator/Templates/HTTP/HTTPHeaders.md.license b/Sources/RESTMigrator/Resources/HTTP/HTTPHeaders.swift.license similarity index 100% rename from Sources/ApodiniMigrator/Templates/HTTP/HTTPHeaders.md.license rename to Sources/RESTMigrator/Resources/HTTP/HTTPHeaders.swift.license diff --git a/Sources/ApodiniMigrator/Templates/HTTP/HTTPMethod.md b/Sources/RESTMigrator/Resources/HTTP/HTTPMethod.swift similarity index 100% rename from Sources/ApodiniMigrator/Templates/HTTP/HTTPMethod.md rename to Sources/RESTMigrator/Resources/HTTP/HTTPMethod.swift diff --git a/Sources/ApodiniMigrator/Templates/HTTP/HTTPMethod.md.license b/Sources/RESTMigrator/Resources/HTTP/HTTPMethod.swift.license similarity index 100% rename from Sources/ApodiniMigrator/Templates/HTTP/HTTPMethod.md.license rename to Sources/RESTMigrator/Resources/HTTP/HTTPMethod.swift.license diff --git a/Sources/ApodiniMigrator/Templates/HTTP/Parameters.md b/Sources/RESTMigrator/Resources/HTTP/Parameters.swift similarity index 100% rename from Sources/ApodiniMigrator/Templates/HTTP/Parameters.md rename to Sources/RESTMigrator/Resources/HTTP/Parameters.swift diff --git a/Sources/ApodiniMigrator/Templates/HTTP/Parameters.md.license b/Sources/RESTMigrator/Resources/HTTP/Parameters.swift.license similarity index 100% rename from Sources/ApodiniMigrator/Templates/HTTP/Parameters.md.license rename to Sources/RESTMigrator/Resources/HTTP/Parameters.swift.license diff --git a/Sources/ApodiniMigrator/Templates/Networking/Handler.md b/Sources/RESTMigrator/Resources/Networking/Handler.swift similarity index 97% rename from Sources/ApodiniMigrator/Templates/Networking/Handler.md rename to Sources/RESTMigrator/Resources/Networking/Handler.swift index e96c6824..046e9bbe 100644 --- a/Sources/ApodiniMigrator/Templates/Networking/Handler.md +++ b/Sources/RESTMigrator/Resources/Networking/Handler.swift @@ -74,9 +74,9 @@ extension URLRequest { if let authorization = handler.authorization { switch authorization.location { - ____SKIP____case .cookie, .header: + case .cookie, .header: authorization.inject(into: &headers) - ____SKIP____case .query: + case .query: let query = "\(url?.query != nil ? "&" : "?")\(authorization.query)" url?.appendPathComponent(query) } diff --git a/Sources/ApodiniMigrator/Templates/Networking/Handler.md.license b/Sources/RESTMigrator/Resources/Networking/Handler.swift.license similarity index 100% rename from Sources/ApodiniMigrator/Templates/Networking/Handler.md.license rename to Sources/RESTMigrator/Resources/Networking/Handler.swift.license diff --git a/Sources/ApodiniMigrator/Templates/Networking/NetworkingService.md b/Sources/RESTMigrator/Resources/Networking/NetworkingService.swift similarity index 70% rename from Sources/ApodiniMigrator/Templates/Networking/NetworkingService.md rename to Sources/RESTMigrator/Resources/Networking/NetworkingService.swift index b7a1da40..f1a6da01 100644 --- a/Sources/ApodiniMigrator/Templates/Networking/NetworkingService.md +++ b/Sources/RESTMigrator/Resources/Networking/NetworkingService.swift @@ -41,28 +41,28 @@ public enum NetworkingService { /// - baseURL: baseURL of the web service, where the handler is located, default value `NetworkingService.baseURL` public static func trigger(_ handler: Handler, at baseURL: URL = baseURL) -> ApodiniPublisher { URLSession.shared.dataTaskPublisher(for: URLRequest(for: handler, with: baseURL)) - ____INDENTATION____.tryMap { data, response in - ____INDENTATION____let sanitizedData = !data.isEmpty ? data : "{}".data(using: .utf8) ?? .init() - ____INDENTATION____guard let response = response as? HTTPURLResponse else { - ____INDENTATION____ return sanitizedData - ____INDENTATION____} - ____INDENTATION____ - ____INDENTATION____let statusCode = response.statusCode - ____INDENTATION____ - ____INDENTATION____if 200 ... 299 ~= statusCode { - ____INDENTATION____ return sanitizedData - ____INDENTATION____} - ____INDENTATION____ - ____INDENTATION____if let handlerError = handler.error(with: statusCode) { - ____INDENTATION____ throw handlerError - ____INDENTATION____} - ____INDENTATION____ - ____INDENTATION____throw URLError(.init(rawValue: statusCode)) - ____INDENTATION____} - ____INDENTATION____.decode(type: DataWrapper.self, decoder: decoder) - ____INDENTATION____.map(\.data) - ____INDENTATION____.receive(on: DispatchQueue.main) - ____INDENTATION____.eraseToAnyPublisher() + .tryMap { data, response in + let sanitizedData = !data.isEmpty ? data : "{}".data(using: .utf8) ?? .init() + guard let response = response as? HTTPURLResponse else { + return sanitizedData + } + + let statusCode = response.statusCode + + if 200 ... 299 ~= statusCode { + return sanitizedData + } + + if let handlerError = handler.error(with: statusCode) { + throw handlerError + } + + throw URLError(.init(rawValue: statusCode)) + } + .decode(type: DataWrapper.self, decoder: decoder) + .map(\.data) + .receive(on: DispatchQueue.main) + .eraseToAnyPublisher() } /// Encodes an instance of the indicated type with `NetworkingService.encoder` diff --git a/Sources/ApodiniMigrator/Templates/Networking/NetworkingService.md.license b/Sources/RESTMigrator/Resources/Networking/NetworkingService.swift.license similarity index 100% rename from Sources/ApodiniMigrator/Templates/Networking/NetworkingService.md.license rename to Sources/RESTMigrator/Resources/Networking/NetworkingService.swift.license diff --git a/Sources/ApodiniMigrator/Templates/Package.md b/Sources/RESTMigrator/Resources/Package.swift similarity index 100% rename from Sources/ApodiniMigrator/Templates/Package.md rename to Sources/RESTMigrator/Resources/Package.swift diff --git a/Sources/ApodiniMigrator/Templates/Package.md.license b/Sources/RESTMigrator/Resources/Package.swift.license similarity index 100% rename from Sources/ApodiniMigrator/Templates/Package.md.license rename to Sources/RESTMigrator/Resources/Package.swift.license diff --git a/Sources/RESTMigrator/Resources/Readme.md b/Sources/RESTMigrator/Resources/Readme.md new file mode 100644 index 00000000..fe7319d3 --- /dev/null +++ b/Sources/RESTMigrator/Resources/Readme.md @@ -0,0 +1,2 @@ +# ___PACKAGE_NAME___ + diff --git a/Sources/ApodiniMigrator/Templates/Readme.md.license b/Sources/RESTMigrator/Resources/Readme.md.license similarity index 100% rename from Sources/ApodiniMigrator/Templates/Readme.md.license rename to Sources/RESTMigrator/Resources/Readme.md.license diff --git a/Sources/ApodiniMigrator/Templates/Tests/LinuxMain.md b/Sources/RESTMigrator/Resources/Tests/LinuxMain.swift similarity index 100% rename from Sources/ApodiniMigrator/Templates/Tests/LinuxMain.md rename to Sources/RESTMigrator/Resources/Tests/LinuxMain.swift diff --git a/Sources/ApodiniMigrator/Templates/Tests/LinuxMain.md.license b/Sources/RESTMigrator/Resources/Tests/LinuxMain.swift.license similarity index 100% rename from Sources/ApodiniMigrator/Templates/Tests/LinuxMain.md.license rename to Sources/RESTMigrator/Resources/Tests/LinuxMain.swift.license diff --git a/Sources/ApodiniMigrator/Templates/Tests/XCTestManifests.md b/Sources/RESTMigrator/Resources/Tests/XCTestManifests.swift similarity index 100% rename from Sources/ApodiniMigrator/Templates/Tests/XCTestManifests.md rename to Sources/RESTMigrator/Resources/Tests/XCTestManifests.swift diff --git a/Sources/ApodiniMigrator/Templates/Tests/XCTestManifests.md.license b/Sources/RESTMigrator/Resources/Tests/XCTestManifests.swift.license similarity index 100% rename from Sources/ApodiniMigrator/Templates/Tests/XCTestManifests.md.license rename to Sources/RESTMigrator/Resources/Tests/XCTestManifests.swift.license diff --git a/Sources/ApodiniMigrator/Templates/Utils/Utils.md b/Sources/RESTMigrator/Resources/Utils/Utils.swift similarity index 93% rename from Sources/ApodiniMigrator/Templates/Utils/Utils.md rename to Sources/RESTMigrator/Resources/Utils/Utils.swift index e9a090fb..69f56505 100644 --- a/Sources/ApodiniMigrator/Templates/Utils/Utils.md +++ b/Sources/RESTMigrator/Resources/Utils/Utils.swift @@ -51,9 +51,10 @@ fileprivate extension Bundle { /// Returns the typed instance at `resource` func resource(_ resource: Resource) -> D { guard - ____INDENTATION____let path = path(forResource: resource.rawValue, ofType: "json"), - ____INDENTATION____let instance = try? D.decode(from: path.asPath) - else { fatalError("Resource \(resource.rawValue) is malformed") } + let path = path(forResource: resource.rawValue, ofType: "json"), + let instance = try? D.decode(from: path.asPath) else { + fatalError("Resource \(resource.rawValue) is malformed") + } return instance } } diff --git a/Sources/ApodiniMigrator/Templates/Utils/Utils.md.license b/Sources/RESTMigrator/Resources/Utils/Utils.swift.license similarity index 100% rename from Sources/ApodiniMigrator/Templates/Utils/Utils.md.license rename to Sources/RESTMigrator/Resources/Utils/Utils.swift.license diff --git a/Sources/RESTMigrator/TestTarget/ModelTestsFile.swift b/Sources/RESTMigrator/TestTarget/ModelTestsFile.swift new file mode 100644 index 00000000..ba86f9b4 --- /dev/null +++ b/Sources/RESTMigrator/TestTarget/ModelTestsFile.swift @@ -0,0 +1,110 @@ +// +// This source file is part of the Apodini open source project +// +// SPDX-FileCopyrightText: 2019-2021 Paul Schmiedmayer and the Apodini project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + +import Foundation +import ApodiniMigratorClientSupport +import ApodiniMigratorShared +import ApodiniMigrator + +struct ModelTestsFile: GeneratedFile { + let fileName: Name + let models: [TypeInformation] + let objectJSONs: [String: JSONValue] + let encoderConfiguration: EncoderConfiguration + + init( + name: Name, + models: [TypeInformation], + objectJSONs: [String: JSONValue] = [:], + encoderConfiguration: EncoderConfiguration = .default + ) { + self.fileName = name + self.models = models.sorted(by: \.unsafeTypeString) + self.objectJSONs = objectJSONs + self.encoderConfiguration = encoderConfiguration + } + + private func dereference(_ model: TypeInformation) -> TypeInformation { + switch model { + case .scalar, .enum: return model + case let .repeated(element): return .repeated(element: dereference(element)) + case let .dictionary(key, value): return .dictionary(key: key, value: dereference(value)) + case let .optional(wrappedValue): return .optional(wrappedValue: dereference(wrappedValue)) + case let .object(name, properties, _): + return .object(name: name, properties: properties.map { .init(name: $0.name, type: dereference($0.type), annotation: $0.annotation) }) + case let .reference(key): + if let type = models.first(where: { $0.typeName.buildName() == key.rawValue }) { + return dereference(type) + } + fatalError("Something went fundamentally wrong. Did not find the corresponding model of the reference with key: \(key.rawValue)") + } + } + + private func method(for model: TypeInformation) -> String { + let jsonString: String + if let jsonValue = objectJSONs[model.typeName.rawValue] { + jsonString = jsonValue.rawValue + } else { + jsonString = JSONStringBuilder.jsonString(dereference(model), with: encoderConfiguration) + } + + let typeName = model.typeName.mangledName + + @SourceCodeBuilder + var method: String { + "func test\(typeName)() throws {" + Indent { + """ + let json: JSONValue = + \""" + \(jsonString) + \""" + + let instance = XCTAssertNoThrowWithResult(try \(typeName).instance(from: json)) + XCTAssertNoThrow(try \(typeName).encoder.encode(instance)) + """ + } + "}" + } + + return method + } + + var renderableContent: String { + FileHeaderComment() + + Import(.xCTest) + Import(.packageName, testable: true) + Import(.apodiniMigratorClientSupport, testable: true) + "" + + "final class \(Placeholder.packageName)Tests: XCTestCase {" + Indent { + for model in models { + method(for: model) + "" + } + + "func XCTAssertNoThrowWithResult(_ expression: @autoclosure () throws -> T) -> T {" + Indent { + "XCTAssertNoThrow(try expression())" + "do {" + Indent { + "return try expression()" + } + "} catch {" + Indent { + "preconditionFailure(\"Expression threw an error: \\(error.localizedDescription)\")" + } + "}" + } + "}" + } + "}" + } +} diff --git a/Sources/RESTMigrator/TypeInformation+Unsafe.swift b/Sources/RESTMigrator/TypeInformation+Unsafe.swift new file mode 100644 index 00000000..16c92add --- /dev/null +++ b/Sources/RESTMigrator/TypeInformation+Unsafe.swift @@ -0,0 +1,41 @@ +// +// This source file is part of the Apodini open source project +// +// SPDX-FileCopyrightText: 2019-2021 Paul Schmiedmayer and the Apodini project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + +import Foundation + +extension TypeInformation { + /// Retrieves the unsafe `mangledName` which is used by the ``RESTMigrator`` for file naming. + /// This is considered unsafe as we cannot guarantee that we avoid name collisions (e.g. generics or nested types). + /// Further, we normally cannot reconstruct the `TypeName` from `.reference` key. + var unsafeFileNaming: String { + switch self { + case let .reference(key): + return TypeName(rawValue: key.rawValue).mangledName + default: + return typeName.mangledName + } + } + + /// Retrieves the unsafe `typeString` which is used by the ``RESTMigrator` for type naming. + /// This is considered unsafe as we cannot guarantee that we avoid name collisions (e.g. nested types). + /// Further, we normally cannot reconstruct the `TypeName` from `.reference` key. + var unsafeTypeString: String { + switch self { + case let .repeated(element): + return "[\(element.unsafeTypeString)]" + case let .dictionary(key, value): + return "[\(key.description): \(value.unsafeTypeString)]" + case let .optional(wrappedValue): + return wrappedValue.unsafeTypeString + "?" + case .enum, .object, .scalar: + return typeString + case let .reference(key): + return TypeName(rawValue: key.rawValue).buildName() + } + } +} diff --git a/Tests/ApodiniMigratorTests/ApodiniMigratorCompareTests/AnyCodableAndRelaxedIdentifiableTests.swift b/Tests/ApodiniMigratorTests/ApodiniMigratorCompareTests/AnyCodableAndRelaxedIdentifiableTests.swift index f4093799..c556cce3 100644 --- a/Tests/ApodiniMigratorTests/ApodiniMigratorCompareTests/AnyCodableAndRelaxedIdentifiableTests.swift +++ b/Tests/ApodiniMigratorTests/ApodiniMigratorCompareTests/AnyCodableAndRelaxedIdentifiableTests.swift @@ -14,8 +14,8 @@ final class AnyCodableAndRelaxedIdentifiableTests: ApodiniMigratorXCTestCase { func testAnyCodable() throws { let encoder = JSONEncoder() let decoder = JSONDecoder() - - let document = try Documents.v1.instance() as Document + + let document = try Documents.v1.decodedContent() as APIDocument let documentAsAnyCodable = document.asAnyCodableElement let documentData = XCTAssertNoThrowWithResult(try encoder.encode(documentAsAnyCodable)) XCTAssertNoThrow(try decoder.decode(AnyCodableElement.self, from: documentData)) @@ -28,7 +28,7 @@ final class AnyCodableAndRelaxedIdentifiableTests: ApodiniMigratorXCTestCase { let endpointData = XCTAssertNoThrowWithResult(try encoder.encode(endpoint.asAnyCodableElement)) XCTAssertNoThrow(try decoder.decode(AnyCodableElement.self, from: endpointData)) - let path = endpoint.path + let path: EndpointPath = endpoint.identifier() let pathData = XCTAssertNoThrowWithResult(try encoder.encode(path.asAnyCodableElement)) XCTAssertNoThrow(try decoder.decode(AnyCodableElement.self, from: pathData)) @@ -39,16 +39,20 @@ final class AnyCodableAndRelaxedIdentifiableTests: ApodiniMigratorXCTestCase { let typeInformation = parameter.typeInformation let typeInformationData = XCTAssertNoThrowWithResult(try encoder.encode(typeInformation.asAnyCodableElement)) XCTAssertNoThrow(try decoder.decode(AnyCodableElement.self, from: typeInformationData)) - - let encoderConfig = document.metaData.encoderConfiguration + + + let exporterConfiguration: RESTExporterConfiguration = document.serviceInformation.exporter() + + let encoderConfig = exporterConfiguration.encoderConfiguration let encoderConfigData = XCTAssertNoThrowWithResult(try encoder.encode(encoderConfig.asAnyCodableElement)) XCTAssertNoThrow(try decoder.decode(AnyCodableElement.self, from: encoderConfigData)) - let decoderConfig = document.metaData.decoderConfiguration + let decoderConfig = exporterConfiguration.decoderConfiguration let decoderConfigData = XCTAssertNoThrowWithResult(try encoder.encode(decoderConfig.asAnyCodableElement)) XCTAssertNoThrow(try decoder.decode(AnyCodableElement.self, from: decoderConfigData)) - - let operation = endpoint.operation + + + let operation: ApodiniMigratorCore.Operation = endpoint.identifier() let operationData = XCTAssertNoThrowWithResult(try encoder.encode(operation.asAnyCodableElement)) XCTAssertNoThrow(try decoder.decode(AnyCodableElement.self, from: operationData)) @@ -77,14 +81,10 @@ final class AnyCodableAndRelaxedIdentifiableTests: ApodiniMigratorXCTestCase { func testRelaxedDeltaIdentifiable() { let int = TypeName(Int.self) let string = TypeName(String.self) - let customInt = TypeName(name: "Int") + let customInt = TypeName(rawValue: "Int") XCTAssertEqual(int ?= string, false) XCTAssertEqual(int ?= customInt, false) XCTAssert(int ?= int) - - let reference = TypeInformation.reference("User") - XCTAssert(reference.deltaIdentifier.rawValue == "User") - XCTAssert(reference ?= .object(name: .init(name: "User"), properties: [])) } } diff --git a/Tests/ApodiniMigratorTests/ApodiniMigratorCompareTests/EndpointComparatorTests.swift b/Tests/ApodiniMigratorTests/ApodiniMigratorCompareTests/EndpointComparatorTests.swift index fb117cfb..fdd116ce 100644 --- a/Tests/ApodiniMigratorTests/ApodiniMigratorCompareTests/EndpointComparatorTests.swift +++ b/Tests/ApodiniMigratorTests/ApodiniMigratorCompareTests/EndpointComparatorTests.swift @@ -10,7 +10,15 @@ import XCTest @testable import ApodiniMigratorCore @testable import ApodiniMigratorCompare +// swiftlint:disable:next type_body_length final class EndpointComparatorTests: ApodiniMigratorXCTestCase { + var endpointChanges = [EndpointChange]() + + override func tearDownWithError() throws { + try super.tearDownWithError() + endpointChanges.removeAll() + } + struct LHSResponse: ApodiniMigratorCodable { static var encoder: JSONEncoder = .init() static var decoder: JSONDecoder = .init() @@ -32,21 +40,24 @@ final class EndpointComparatorTests: ApodiniMigratorXCTestCase { handlerName: "handlerName", deltaIdentifier: "test", operation: .read, + communicationalPattern: .requestResponse, absolutePath: "/v1/tests/{second}", parameters: [ .init(name: "isRunning", typeInformation: .scalar(.string), parameterType: .lightweight, isRequired: false), .init(name: "first", typeInformation: .scalar(.string), parameterType: .lightweight, isRequired: true), .init(name: "second", typeInformation: .scalar(.uuid), parameterType: .path, isRequired: true), + // swiftlint:disable:next force_try .init(name: "third", typeInformation: try! TypeInformation(type: TestTypes.Car.self), parameterType: .content, isRequired: true) ], + // swiftlint:disable:next force_try response: try! TypeInformation(type: LHSResponse.self), errors: [] ) func testNoEndpointChange() throws { - let endpointComparator = EndpointComparator(lhs: lhs, rhs: lhs, changes: node, configuration: .default) - endpointComparator.compare() - XCTAssert(node.isEmpty) + let comparator = EndpointComparator(lhs: lhs, rhs: lhs) + comparator.compare(comparisonContext, &endpointChanges) + XCTAssert(endpointChanges.isEmpty) } func testOperationChanged() throws { @@ -54,24 +65,35 @@ final class EndpointComparatorTests: ApodiniMigratorXCTestCase { handlerName: lhs.handlerName, deltaIdentifier: lhs.deltaIdentifier.description, operation: .create, - absolutePath: lhs.path.description, + communicationalPattern: .requestResponse, + absolutePath: lhs.identifier(for: EndpointPath.self).description, parameters: lhs.parameters, response: lhs.response, errors: lhs.errors ) - - let endpointComparator = EndpointComparator(lhs: lhs, rhs: rhs, changes: node, configuration: .default) - endpointComparator.compare() - XCTAssert(node.changes.count == 1) - let change = try XCTUnwrap(node.changes.first as? UpdateChange) - XCTAssert(change.element == .endpoint("test", target: .operation)) - XCTAssert(change.breaking) - XCTAssert(change.solvable) - if case let .element(codable) = change.to { - XCTAssert(codable.typed(ApodiniMigratorCore.Operation.self) == .create) - } else { + + let comparator = EndpointComparator(lhs: lhs, rhs: rhs) + comparator.compare(comparisonContext, &endpointChanges) + + XCTAssertEqual(endpointChanges.count, 1) + let change = try XCTUnwrap(endpointChanges.first) + XCTAssertEqual(change.id, lhs.deltaIdentifier) + XCTAssertEqual(change.type, .update) + XCTAssertEqual(change.breaking, true) + XCTAssertEqual(change.solvable, true) + let updateChange = try XCTUnwrap(change.modeledUpdateChange) + + guard case let .identifier(identifierChange) = updateChange.updated else { XCTFail("Change did not store the updated operation") + return } + + XCTAssertEqual(identifierChange.type, .update) + XCTAssertEqual(change.breaking, identifierChange.breaking) + XCTAssertEqual(change.solvable, identifierChange.solvable) + XCTAssertEqual(identifierChange.id.rawValue, Operation.identifierType) + let operationUpdate = try XCTUnwrap(identifierChange.modeledUpdateChange) + XCTAssertEqual(operationUpdate.updated.to.typed(), Operation.create) } func testResourcePathChange() throws { @@ -79,24 +101,66 @@ final class EndpointComparatorTests: ApodiniMigratorXCTestCase { handlerName: lhs.handlerName, deltaIdentifier: lhs.deltaIdentifier.description, operation: .read, + communicationalPattern: lhs.communicationalPattern, absolutePath: "/v1/newTests/{second}", parameters: lhs.parameters, response: lhs.response, errors: lhs.errors ) - - let endpointComparator = EndpointComparator(lhs: lhs, rhs: rhs, changes: node, configuration: .default) - endpointComparator.compare() - XCTAssert(node.changes.count == 1) - let change = try XCTUnwrap(node.changes.first as? UpdateChange) - XCTAssert(change.element == .endpoint("test", target: .resourcePath)) - XCTAssert(change.breaking) - XCTAssert(change.solvable) - if case let .element(codable) = change.to { - XCTAssert(codable.typed(EndpointPath.self) == rhs.path) - } else { + + let comparator = EndpointComparator(lhs: lhs, rhs: rhs) + comparator.compare(comparisonContext, &endpointChanges) + + XCTAssertEqual(endpointChanges.count, 1) + let change = try XCTUnwrap(endpointChanges.first) + XCTAssertEqual(change.id, lhs.deltaIdentifier) + XCTAssertEqual(change.type, .update) + XCTAssertEqual(change.breaking, true) + XCTAssertEqual(change.solvable, true) + let updateChange = try XCTUnwrap(change.modeledUpdateChange) + + guard case let .identifier(identifierChange) = updateChange.updated else { XCTFail("Change did not store the updated resource path") + return } + XCTAssertEqual(identifierChange.type, .update) + XCTAssertEqual(change.breaking, identifierChange.breaking) + XCTAssertEqual(change.solvable, identifierChange.solvable) + XCTAssertEqual(identifierChange.id.rawValue, EndpointPath.identifierType) + let pathChange = try XCTUnwrap(identifierChange.modeledUpdateChange) + XCTAssertEqual(pathChange.updated.to.typed(of: EndpointPath.self), rhs.identifier()) + } + + func testCommunicationPatternChange() throws { + let rhs = Endpoint( + handlerName: lhs.handlerName, + deltaIdentifier: lhs.deltaIdentifier.description, + operation: lhs.operation, + communicationalPattern: .bidirectionalStream, + absolutePath: lhs.path.description, + parameters: lhs.parameters, + response: lhs.response, + errors: lhs.errors + ) + + let comparator = EndpointComparator(lhs: lhs, rhs: rhs) + comparator.compare(comparisonContext, &endpointChanges) + + XCTAssertEqual(endpointChanges.count, 1) + let change = try XCTUnwrap(endpointChanges.first) + XCTAssertEqual(change.id, lhs.deltaIdentifier) + XCTAssertEqual(change.type, .update) + XCTAssertEqual(change.breaking, true) + XCTAssertEqual(change.solvable, true) + let updateChange = try XCTUnwrap(change.modeledUpdateChange) + + guard case let .communicationalPattern(from, to) = updateChange.updated else { + XCTFail("Change did not store the updated communicational pattern") + return + } + + XCTAssertEqual(from, .requestResponse) + XCTAssertEqual(to, .bidirectionalStream) } func testAddNewEndpointParameter() throws { @@ -104,29 +168,41 @@ final class EndpointComparatorTests: ApodiniMigratorXCTestCase { let rhs = Endpoint( handlerName: lhs.handlerName, deltaIdentifier: lhs.deltaIdentifier.description, - operation: lhs.operation, - absolutePath: lhs.path.description, + operation: lhs.identifier(), + communicationalPattern: .requestResponse, + absolutePath: lhs.identifier(for: EndpointPath.self).description, parameters: lhs.parameters + newParameter, response: lhs.response, errors: lhs.errors ) - - let endpointComparator = EndpointComparator(lhs: lhs, rhs: rhs, changes: node, configuration: .default) - endpointComparator.compare() - XCTAssert(node.changes.count == 1) - let change = try XCTUnwrap(node.changes.first as? AddChange) - XCTAssert(change.element == .endpoint("test", target: .queryParameter)) - XCTAssert(change.breaking) - XCTAssert(change.solvable) - if case let .element(codable) = change.added { - XCTAssert(codable.typed(Parameter.self) == newParameter) - } else { - XCTFail("Change did not store the updated resource path") + + let comparator = EndpointComparator(lhs: lhs, rhs: rhs) + comparator.compare(comparisonContext, &endpointChanges) + + XCTAssertEqual(endpointChanges.count, 1) + let change = try XCTUnwrap(endpointChanges.first) + XCTAssertEqual(change.id, lhs.deltaIdentifier) + XCTAssertEqual(change.type, .update) + XCTAssertEqual(change.breaking, true) + XCTAssertEqual(change.solvable, true) + let updateChange = try XCTUnwrap(change.modeledUpdateChange) + + guard case let .parameter(parameterChange) = updateChange.updated else { + XCTFail("Change did not store the updated property") + return } - - if case let .json(id) = change.defaultValue, let json = node.jsonValues[id] { - let defaultValue = XCTAssertNoThrowWithResult(try Int64.instance(from: json)) - XCTAssert(defaultValue == 0) + + XCTAssertEqual(parameterChange.type, .addition) + XCTAssertEqual(change.breaking, parameterChange.breaking) + XCTAssertEqual(change.solvable, parameterChange.solvable) + XCTAssertEqual(parameterChange.id, newParameter.deltaIdentifier) + let parameterAddition = try XCTUnwrap(parameterChange.modeledAdditionChange) + XCTAssertEqual(parameterAddition.added, newParameter) + + if let jsonId = parameterAddition.defaultValue, + let json = comparisonContext.jsonValues[jsonId] { + let instance = XCTAssertNoThrowWithResult(try Int64.instance(from: json)) + XCTAssertEqual(instance, 0) } else { XCTFail("No default value provided for added required parameter") } @@ -136,34 +212,47 @@ final class EndpointComparatorTests: ApodiniMigratorXCTestCase { let rhs = Endpoint( handlerName: lhs.handlerName, deltaIdentifier: lhs.deltaIdentifier.description, - operation: lhs.operation, - absolutePath: lhs.path.description, + operation: lhs.identifier(), + communicationalPattern: .requestResponse, + absolutePath: lhs.identifier(for: EndpointPath.self).description, parameters: lhs.parameters.filter { $0.name != "first" }, response: lhs.response, errors: lhs.errors ) - - let endpointComparator = EndpointComparator(lhs: lhs, rhs: rhs, changes: node, configuration: .default) - endpointComparator.compare() - XCTAssert(node.changes.count == 1) - let change = try XCTUnwrap(node.changes.first as? DeleteChange) - XCTAssert(change.element == .endpoint("test", target: .queryParameter)) - XCTAssert(!change.breaking) - XCTAssert(change.solvable) - if case let .elementID(id) = change.deleted { - XCTAssert(id == "first") - } else { - XCTFail("Change did not provide the id of the deleted parameter") + + let comparator = EndpointComparator(lhs: lhs, rhs: rhs) + comparator.compare(comparisonContext, &endpointChanges) + + XCTAssertEqual(endpointChanges.count, 1) + let change = try XCTUnwrap(endpointChanges.first) + XCTAssertEqual(change.id, lhs.deltaIdentifier) + XCTAssertEqual(change.type, .update) + XCTAssertEqual(change.breaking, false) + XCTAssertEqual(change.solvable, true) + let updateChange = try XCTUnwrap(change.modeledUpdateChange) + + guard case let .parameter(parameterChange) = updateChange.updated else { + XCTFail("Change did not store the updated property") + return } - XCTAssert(change.fallbackValue == .none, "Provided a non necessary fallback value for a deleted endpoint parameter") + + XCTAssertEqual(parameterChange.type, .removal) + XCTAssertEqual(change.breaking, parameterChange.breaking) + XCTAssertEqual(change.solvable, parameterChange.solvable) + XCTAssertEqual(parameterChange.id, "first") + + let parameterRemoval = try XCTUnwrap(parameterChange.modeledRemovalChange) + XCTAssertEqual(parameterRemoval.removed, nil) + XCTAssertEqual(parameterRemoval.fallbackValue, nil, "Provided a non necessary fallback value for a deleted endpoint parameter") } func testRenamedEndpointParameter() throws { let rhs = Endpoint( handlerName: lhs.handlerName, deltaIdentifier: lhs.deltaIdentifier.description, - operation: lhs.operation, - absolutePath: lhs.path.description, + operation: lhs.identifier(), + communicationalPattern: .requestResponse, + absolutePath: lhs.identifier(for: EndpointPath.self).description, parameters: [ .init(name: "isRunning", typeInformation: .scalar(.string), parameterType: .lightweight, isRequired: false), .init(name: "firstParam", typeInformation: .scalar(.string), parameterType: .lightweight, isRequired: true), @@ -173,29 +262,41 @@ final class EndpointComparatorTests: ApodiniMigratorXCTestCase { response: lhs.response, errors: lhs.errors ) - - let endpointComparator = EndpointComparator(lhs: lhs, rhs: rhs, changes: node, configuration: .default) - endpointComparator.compare() - XCTAssert(node.changes.count == 1) - let change = try XCTUnwrap(node.changes.first as? UpdateChange) - XCTAssert(change.element == .endpoint("test", target: .queryParameter)) - XCTAssert(change.type == .rename) - XCTAssert(change.breaking) - XCTAssert(change.solvable) - if case let .stringValue(value) = change.to, let similarity = change.similarity { - XCTAssert(value == "firstParam") - XCTAssert(similarity > 0.5) - } else { - XCTFail("Change did not provide the updated name of the parameter") + + let comparator = EndpointComparator(lhs: lhs, rhs: rhs) + comparator.compare(comparisonContext, &endpointChanges) + + XCTAssertEqual(endpointChanges.count, 1) + let change = try XCTUnwrap(endpointChanges.first) + XCTAssertEqual(change.id, lhs.deltaIdentifier) + XCTAssertEqual(change.type, .update) + XCTAssertEqual(change.breaking, true) + XCTAssertEqual(change.solvable, true) + let updateChange = try XCTUnwrap(change.modeledUpdateChange) + + guard case let .parameter(parameterChange) = updateChange.updated else { + XCTFail("Change did not store the updated property") + return } + + XCTAssertEqual(parameterChange.type, .idChange) + XCTAssertEqual(change.breaking, parameterChange.breaking) + XCTAssertEqual(change.solvable, parameterChange.solvable) + XCTAssertEqual(parameterChange.id, "first") + + let parameterRename = try XCTUnwrap(parameterChange.modeledIdentifierChange) + XCTAssertEqual(parameterChange.id, parameterRename.from) + XCTAssertEqual(parameterRename.to, "firstParam") + XCTAssert(try XCTUnwrap(parameterRename.similarity) > 0.5) } func testEndpointParameterNecessityChange() throws { let rhs = Endpoint( handlerName: lhs.handlerName, deltaIdentifier: lhs.deltaIdentifier.description, - operation: lhs.operation, - absolutePath: lhs.path.description, + operation: lhs.identifier(), + communicationalPattern: .requestResponse, + absolutePath: lhs.identifier(for: EndpointPath.self).description, parameters: [ .init(name: "isRunning", typeInformation: .scalar(.string), parameterType: .lightweight, isRequired: true), .init(name: "first", typeInformation: .scalar(.string), parameterType: .lightweight, isRequired: true), @@ -205,20 +306,40 @@ final class EndpointComparatorTests: ApodiniMigratorXCTestCase { response: lhs.response, errors: lhs.errors ) - - let endpointComparator = EndpointComparator(lhs: lhs, rhs: rhs, changes: node, configuration: .default) - endpointComparator.compare() - XCTAssert(node.changes.count == 1) - let change = try XCTUnwrap(node.changes.first as? UpdateChange) - XCTAssert(change.element == .endpoint("test", target: .queryParameter)) - XCTAssert(change.parameterTarget == .necessity) - XCTAssert(change.breaking) - XCTAssert(change.solvable) - if case let .json(id) = change.necessityValue, let json = node.jsonValues[id] { - let necessityValue = XCTAssertNoThrowWithResult(try String.instance(from: json)) - XCTAssert(necessityValue == "") - } else { - XCTFail("Change did not provide a necessity value for the required parameter") + + let comparator = EndpointComparator(lhs: lhs, rhs: rhs) + comparator.compare(comparisonContext, &endpointChanges) + + XCTAssertEqual(endpointChanges.count, 1) + let change = try XCTUnwrap(endpointChanges.first) + XCTAssertEqual(change.id, lhs.deltaIdentifier) + XCTAssertEqual(change.type, .update) + XCTAssertEqual(change.breaking, true) + XCTAssertEqual(change.solvable, true) + let updateChange = try XCTUnwrap(change.modeledUpdateChange) + + guard case let .parameter(parameterChange) = updateChange.updated else { + XCTFail("Change did not store the updated parameter") + return + } + + XCTAssertEqual(parameterChange.type, .update) + XCTAssertEqual(change.breaking, parameterChange.breaking) + XCTAssertEqual(change.solvable, parameterChange.solvable) + XCTAssertEqual(parameterChange.id, "isRunning") + + let parameterUpdate = try XCTUnwrap(parameterChange.modeledUpdateChange) + guard case let .necessity(from, to, necessityMigration) = parameterUpdate.updated else { + XCTFail("Unexpected parameter update change: \(parameterUpdate.updated)") + return + } + XCTAssertEqual(from, .optional) + XCTAssertEqual(to, .required) + + if let id = necessityMigration, + let json = comparisonContext.jsonValues[id] { + let instance = XCTAssertNoThrowWithResult(try String.instance(from: json)) + XCTAssertEqual(instance, "") } } @@ -226,8 +347,10 @@ final class EndpointComparatorTests: ApodiniMigratorXCTestCase { let rhs = Endpoint( handlerName: lhs.handlerName, deltaIdentifier: lhs.deltaIdentifier.description, - operation: lhs.operation, - absolutePath: lhs.path.description.without("{second}"), // removing from path as well + operation: lhs.identifier(), + communicationalPattern: .requestResponse, + // removing from path as well + absolutePath: lhs.identifier(for: EndpointPath.self).description.replacingOccurrences(of: "{second}", with: ""), parameters: [ .init(name: "isRunning", typeInformation: .scalar(.string), parameterType: .lightweight, isRequired: false), .init(name: "first", typeInformation: .scalar(.string), parameterType: .lightweight, isRequired: true), @@ -237,21 +360,35 @@ final class EndpointComparatorTests: ApodiniMigratorXCTestCase { response: lhs.response, errors: lhs.errors ) - - let endpointComparator = EndpointComparator(lhs: lhs, rhs: rhs, changes: node, configuration: .default) - endpointComparator.compare() - XCTAssert(node.changes.count == 2) // registered two changes, one for the path as well - let change = try XCTUnwrap(node.changes.first(where: { $0.element.target == EndpointTarget.pathParameter.rawValue }) as? UpdateChange) - XCTAssert(change.element == .endpoint("test", target: .pathParameter)) - XCTAssert(change.parameterTarget == .kind) - XCTAssert(change.targetID == "second") - XCTAssert(change.breaking) - XCTAssert(change.solvable) - if case let .element(codable) = change.to { - XCTAssert(codable.typed(ParameterType.self) == .lightweight) - } else { - XCTFail("Change did not provide the updated kind of the parameter") + + let comparator = EndpointComparator(lhs: lhs, rhs: rhs) + comparator.compare(comparisonContext, &endpointChanges) + + XCTAssertEqual(endpointChanges.count, 2) // registered two changes, one for the path as well + let change = try XCTUnwrap(endpointChanges.last) + XCTAssertEqual(change.id, lhs.deltaIdentifier) + XCTAssertEqual(change.type, .update) + XCTAssertEqual(change.breaking, true) + XCTAssertEqual(change.solvable, true) + let updateChange = try XCTUnwrap(change.modeledUpdateChange) + + guard case let .parameter(parameterChange) = updateChange.updated else { + XCTFail("Change did not store the updated parameter: \(updateChange.updated)") + return } + + XCTAssertEqual(parameterChange.type, .update) + XCTAssertEqual(change.breaking, parameterChange.breaking) + XCTAssertEqual(change.solvable, parameterChange.solvable) + XCTAssertEqual(parameterChange.id, "second") + + let parameterUpdate = try XCTUnwrap(parameterChange.modeledUpdateChange) + guard case let .parameterType(from, to) = parameterUpdate.updated else { + XCTFail("Unexpected parameter update change: \(parameterUpdate.updated)") + return + } + XCTAssertEqual(from, .path) + XCTAssertEqual(to, .lightweight) } func testEndpointParameterTypeChange() throws { @@ -261,8 +398,9 @@ final class EndpointComparatorTests: ApodiniMigratorXCTestCase { let rhs = Endpoint( handlerName: lhs.handlerName, deltaIdentifier: lhs.deltaIdentifier.description, - operation: lhs.operation, - absolutePath: lhs.path.description, + operation: lhs.identifier(), + communicationalPattern: .requestResponse, + absolutePath: lhs.identifier(for: EndpointPath.self).description, parameters: [ .init(name: "isRunning", typeInformation: .scalar(.string), parameterType: .lightweight, isRequired: false), .init(name: "first", typeInformation: .scalar(.bool), parameterType: .lightweight, isRequired: true), @@ -272,22 +410,39 @@ final class EndpointComparatorTests: ApodiniMigratorXCTestCase { response: lhs.response, errors: lhs.errors ) - - let endpointComparator = EndpointComparator(lhs: lhs, rhs: rhs, changes: node, configuration: .default) - endpointComparator.compare() - XCTAssert(node.changes.count == 1) - let change = try XCTUnwrap(node.changes.first as? UpdateChange) - XCTAssert(change.element == .endpoint("test", target: .queryParameter)) - XCTAssert(change.parameterTarget == .typeInformation) - XCTAssert(change.targetID == "first") - XCTAssert(change.convertionWarning == nil) - XCTAssert(change.breaking) - XCTAssert(change.solvable) - if case let .element(codable) = change.to, let scriptID = change.convertFromTo, let script = node.scripts[scriptID] { - XCTAssert(codable.typed(TypeInformation.self) == .scalar(.bool)) + + let comparator = EndpointComparator(lhs: lhs, rhs: rhs) + comparator.compare(comparisonContext, &endpointChanges) + + XCTAssertEqual(endpointChanges.count, 1) + let change = try XCTUnwrap(endpointChanges.first) + XCTAssertEqual(change.id, lhs.deltaIdentifier) + XCTAssertEqual(change.type, .update) + XCTAssertEqual(change.breaking, true) + XCTAssertEqual(change.solvable, true) + let updateChange = try XCTUnwrap(change.modeledUpdateChange) + + guard case let .parameter(parameterChange) = updateChange.updated else { + XCTFail("Change did not store the updated parameter") + return + } + + XCTAssertEqual(parameterChange.type, .update) + XCTAssertEqual(change.breaking, parameterChange.breaking) + XCTAssertEqual(change.solvable, parameterChange.solvable) + XCTAssertEqual(parameterChange.id, "first") + + let parameterUpdate = try XCTUnwrap(parameterChange.modeledUpdateChange) + guard case let .type(from, to, forwardMigration, conversionWarning) = parameterUpdate.updated else { + XCTFail("Unexpected parameter update change: \(parameterUpdate.updated)") + return + } + XCTAssertEqual(from, .scalar(.string)) + XCTAssertEqual(to, .scalar(.bool)) + XCTAssertEqual(conversionWarning, nil) + + if let script = comparisonContext.scripts[forwardMigration] { XCTAssertNoThrow(try Bool.from("", script: script), "Invalid script to convert string to bool") - } else { - XCTFail("Change did not provide the required script to convert the updated parameter type") } } @@ -299,29 +454,38 @@ final class EndpointComparatorTests: ApodiniMigratorXCTestCase { handlerName: lhs.handlerName, deltaIdentifier: lhs.deltaIdentifier.description, operation: .read, - absolutePath: lhs.path.description, + communicationalPattern: .requestResponse, + absolutePath: lhs.identifier(for: EndpointPath.self).description, parameters: lhs.parameters, response: try TypeInformation(type: RHSResponse.self), errors: lhs.errors ) - - let endpointComparator = EndpointComparator(lhs: lhs, rhs: rhs, changes: node, configuration: .default) - endpointComparator.compare() - XCTAssert(node.changes.count == 1) - let change = try XCTUnwrap(node.changes.first as? UpdateChange) - XCTAssert(change.element == .endpoint("test", target: .response)) - XCTAssert(change.convertionWarning == nil) - XCTAssert(change.breaking) - XCTAssert(change.solvable) - if case let .element(codable) = change.to, let scriptID = change.convertToFrom, let script = node.scripts[scriptID] { - XCTAssert(codable.typed(TypeInformation.self) == .reference("EndpointComparatorTestsRHSResponse")) + + let comparator = EndpointComparator(lhs: lhs, rhs: rhs) + comparator.compare(comparisonContext, &endpointChanges) + + XCTAssertEqual(endpointChanges.count, 1) + let change = try XCTUnwrap(endpointChanges.first) + XCTAssertEqual(change.id, lhs.deltaIdentifier) + XCTAssertEqual(change.type, .update) + XCTAssertEqual(change.breaking, true) + XCTAssertEqual(change.solvable, true) + let updateChange = try XCTUnwrap(change.modeledUpdateChange) + + guard case let .response(from, to, backwardsConversion, conversionWarning) = updateChange.updated else { + XCTFail("Change did not store the updated parameter") + return + } + XCTAssertEqual(from, .reference("EndpointComparatorTests.LHSResponse")) + XCTAssertEqual(to, .reference("EndpointComparatorTests.RHSResponse")) + XCTAssertEqual(conversionWarning, nil) + + if let script = comparisonContext.scripts[backwardsConversion] { let id = UUID() let instance = XCTAssertNoThrowWithResult(try LHSResponse.from(RHSResponse(identifier: id, name: "someResponse"), script: script)) - XCTAssert(instance.id == id) - XCTAssert(instance.name == "someResponse") - XCTAssert(instance.age == 0) - } else { - XCTFail("Change did not provide the required script to convert the updated response type") + XCTAssertEqual(instance.id, id) + XCTAssertEqual(instance.name, "someResponse") + XCTAssertEqual(instance.age, 0) } } } diff --git a/Tests/ApodiniMigratorTests/ApodiniMigratorCompareTests/EndpointsComparatorTests.swift b/Tests/ApodiniMigratorTests/ApodiniMigratorCompareTests/EndpointsComparatorTests.swift index 38a84535..cb250a02 100644 --- a/Tests/ApodiniMigratorTests/ApodiniMigratorCompareTests/EndpointsComparatorTests.swift +++ b/Tests/ApodiniMigratorTests/ApodiniMigratorCompareTests/EndpointsComparatorTests.swift @@ -11,10 +11,18 @@ import XCTest @testable import ApodiniMigratorCompare final class EndpointsComparatorTests: ApodiniMigratorXCTestCase { + var endpointChanges = [EndpointChange]() + + override func tearDownWithError() throws { + try super.tearDownWithError() + endpointChanges.removeAll() + } + private let lhs = Endpoint( handlerName: "handlerName", deltaIdentifier: "runTests", operation: .read, + communicationalPattern: .requestResponse, absolutePath: "/v1/tests", parameters: [], response: .scalar(.bool), @@ -25,6 +33,7 @@ final class EndpointsComparatorTests: ApodiniMigratorXCTestCase { handlerName: "handlerName", deltaIdentifier: "runningTests", operation: .read, + communicationalPattern: .requestResponse, absolutePath: "/v1/tests", parameters: [], response: .scalar(.bool), @@ -33,65 +42,62 @@ final class EndpointsComparatorTests: ApodiniMigratorXCTestCase { override func setUp() { super.setUp() - - node = ChangeContextNode(compareConfiguration: .active) + + comparisonContext = ChangeComparisonContext(configuration: .active) } func testNoEndpointsChange() throws { - let endpointsComparator = EndpointsComparator(lhs: [lhs], rhs: [lhs], changes: node, configuration: .default) - endpointsComparator.compare() - XCTAssert(node.isEmpty) + let comparator = EndpointsComparator(lhs: [lhs], rhs: [lhs]) + comparator.compare(comparisonContext, &endpointChanges) + XCTAssertEqual(endpointChanges.isEmpty, true) } func testEndpointDeleted() throws { - let endpointsComparator = EndpointsComparator(lhs: [lhs], rhs: [], changes: node, configuration: .default) - endpointsComparator.compare() - XCTAssert(node.changes.count == 1) - let deleteChange = try XCTUnwrap(node.changes.first as? DeleteChange) - - XCTAssert(deleteChange.element == .endpoint(lhs.deltaIdentifier, target: .`self`)) - XCTAssert(deleteChange.breaking) - XCTAssert(!deleteChange.solvable) - XCTAssert(deleteChange.fallbackValue == .none) - XCTAssert(deleteChange.providerSupport == .renameHint(DeleteChange.self)) + let comparator = EndpointsComparator(lhs: [lhs], rhs: []) + comparator.compare(comparisonContext, &endpointChanges) + + XCTAssertEqual(endpointChanges.count, 1) + let change = try XCTUnwrap(endpointChanges.first) + XCTAssertEqual(change.id, lhs.deltaIdentifier) + XCTAssertEqual(change.type, .removal) + XCTAssertEqual(change.breaking, true) + XCTAssertEqual(change.solvable, false) + + let removalChange = try XCTUnwrap(change.modeledRemovalChange) + XCTAssertEqual(removalChange.removed, nil) + XCTAssertEqual(removalChange.fallbackValue, nil) } func testEndpointAdded() throws { - let endpointsComparator = EndpointsComparator(lhs: [], rhs: [lhs], changes: node, configuration: .default) - endpointsComparator.compare() - XCTAssert(node.changes.count == 1) - let addChange = try XCTUnwrap(node.changes.first as? AddChange) - - XCTAssert(addChange.element == .endpoint(lhs.deltaIdentifier, target: .`self`)) - XCTAssert(!addChange.breaking) - XCTAssert(addChange.providerSupport == .renameHint(AddChange.self)) - XCTAssert(addChange.solvable) - - if case let .element(codable) = addChange.added { - XCTAssert(codable.typed(Endpoint.self) == lhs) - } else { - XCTFail("Added endpoint was not stored in the change object") - } + let comparator = EndpointsComparator(lhs: [], rhs: [lhs]) + comparator.compare(comparisonContext, &endpointChanges) + + XCTAssertEqual(endpointChanges.count, 1) + let change = try XCTUnwrap(endpointChanges.first) + XCTAssertEqual(change.id, lhs.deltaIdentifier) + XCTAssertEqual(change.type, .addition) + XCTAssertEqual(change.breaking, false) + XCTAssertEqual(change.solvable, true) + + let additionChange = try XCTUnwrap(change.modeledAdditionChange) + XCTAssertEqual(additionChange.added, lhs) + XCTAssertEqual(additionChange.defaultValue, nil) } func testEndpointRenamed() throws { - let endpointsComparator = EndpointsComparator(lhs: [lhs], rhs: [rhs], changes: node, configuration: .default) - endpointsComparator.compare() - XCTAssert(node.changes.count == 1) - let renameChange = try XCTUnwrap(node.changes.first as? UpdateChange) - - let providerSupport = try XCTUnwrap(renameChange.providerSupport) - XCTAssert(renameChange.element == .endpoint(lhs.deltaIdentifier, target: .deltaIdentifier)) - XCTAssert(renameChange.type == .rename) - XCTAssert(!renameChange.breaking) - XCTAssert(renameChange.solvable) - XCTAssert(providerSupport == .renameValidationHint) - - if case let .stringValue(value) = renameChange.to, let similarity = renameChange.similarity { - XCTAssert(value == "runningTests") - XCTAssert(similarity > 0.5) - } else { - XCTFail("Rename change did not store the updated string value of the endpoint identifier") - } + let comparator = EndpointsComparator(lhs: [lhs], rhs: [rhs]) + comparator.compare(comparisonContext, &endpointChanges) + + XCTAssertEqual(endpointChanges.count, 1) + let change = try XCTUnwrap(endpointChanges.first) + XCTAssertEqual(change.id, lhs.deltaIdentifier) + XCTAssertEqual(change.type, .idChange) + XCTAssertEqual(change.breaking, false) + XCTAssertEqual(change.solvable, true) + + let idChange = try XCTUnwrap(change.modeledIdentifierChange) + XCTAssertEqual(idChange.from, change.id) + XCTAssertEqual(idChange.to, rhs.deltaIdentifier) + XCTAssert(try XCTUnwrap(idChange.similarity) > 0.5) } } diff --git a/Tests/ApodiniMigratorTests/ApodiniMigratorCompareTests/EnumComparatorTests.swift b/Tests/ApodiniMigratorTests/ApodiniMigratorCompareTests/EnumComparatorTests.swift index 0bf0566d..0dd23b48 100644 --- a/Tests/ApodiniMigratorTests/ApodiniMigratorCompareTests/EnumComparatorTests.swift +++ b/Tests/ApodiniMigratorTests/ApodiniMigratorCompareTests/EnumComparatorTests.swift @@ -11,8 +11,15 @@ import XCTest @testable import ApodiniMigratorCompare final class EnumComparatorTests: ApodiniMigratorXCTestCase { + var modelChanges = [ModelChange]() + + override func tearDownWithError() throws { + try super.tearDownWithError() + modelChanges.removeAll() + } + let enumeration: TypeInformation = .enum( - name: .init(name: "ProgLang"), + name: .init(rawValue: "ProgLang"), rawValueType: .scalar(.string), cases: [ .init("swift"), @@ -24,92 +31,138 @@ final class EnumComparatorTests: ApodiniMigratorXCTestCase { override func setUp() { super.setUp() - - node = ChangeContextNode(compareConfiguration: .active) + + comparisonContext = ChangeComparisonContext(configuration: .active) } func testNoEnumChange() { - let enumComparator = EnumComparator(lhs: enumeration, rhs: enumeration, changes: node, configuration: .default) - enumComparator.compare() - XCTAssert(node.isEmpty) + let comparator = EnumComparator(lhs: enumeration, rhs: enumeration) + comparator.compare(comparisonContext, &modelChanges) + XCTAssertEqual(modelChanges.isEmpty, true) } func testDeletedEnumCase() throws { - let updated: TypeInformation = .enum(name: enumeration.typeName, rawValueType: .scalar(.string), cases: enumeration.enumCases.filter { $0.name != "other" }) - let enumComparator = EnumComparator(lhs: enumeration, rhs: updated, changes: node, configuration: .default) - enumComparator.compare() - - XCTAssert(node.changes.count == 1) - let deleteChange = try XCTUnwrap(node.changes.first as? DeleteChange) - - XCTAssert(deleteChange.element == .enum(enumeration.deltaIdentifier, target: .case)) - XCTAssert(deleteChange.breaking) - XCTAssert(deleteChange.solvable) - XCTAssert(deleteChange.fallbackValue == .none) - XCTAssert(deleteChange.providerSupport == .renameHint(DeleteChange.self)) - if case let .elementID(id) = deleteChange.deleted { - XCTAssert(id == "other") - } else { - XCTFail("Did not provide the id of the deleted enum case") + let updated: TypeInformation = .enum( + name: enumeration.typeName, + rawValueType: .scalar(.string), + cases: enumeration.enumCases.filter { $0.name != "other" } + ) + + let comparator = EnumComparator(lhs: enumeration, rhs: updated) + comparator.compare(comparisonContext, &modelChanges) + + XCTAssertEqual(modelChanges.count, 1) + let change = try XCTUnwrap(modelChanges.first) + XCTAssertEqual(change.id, enumeration.deltaIdentifier) + XCTAssertEqual(change.type, .update) + XCTAssertEqual(change.breaking, true) + XCTAssertEqual(change.solvable, true) + + let updateChange = try XCTUnwrap(change.modeledUpdateChange) + guard case let .case(caseChange) = updateChange.updated else { + XCTFail("Change did not store the updated enum case") + return } + + XCTAssertEqual(caseChange.id, "other") + XCTAssertEqual(caseChange.type, .removal) + XCTAssertEqual(change.breaking, caseChange.breaking) + XCTAssertEqual(change.solvable, caseChange.solvable) + + let caseRemoval = try XCTUnwrap(caseChange.modeledRemovalChange) + XCTAssertEqual(caseRemoval.removed, nil) + XCTAssertEqual(caseRemoval.fallbackValue, nil) } func testRenamedEnumCases() throws { let cases = enumeration.enumCases.filter { $0.name != "swift" } + .init("swiftLang") let updated: TypeInformation = .enum(name: enumeration.typeName, rawValueType: .scalar(.string), cases: cases) - let enumComparator = EnumComparator(lhs: enumeration, rhs: updated, changes: node, configuration: .default) - enumComparator.compare() - - XCTAssert(node.changes.count == 2) // update of the raw value as well - let change = try XCTUnwrap(node.changes.first(where: { $0.element.target == EnumTarget.case.rawValue }) as? UpdateChange) - XCTAssert(change.element == .enum(enumeration.deltaIdentifier, target: .case)) - XCTAssert(change.type == .rename) - XCTAssert(change.breaking) - XCTAssert(change.solvable) - if case let .stringValue(value) = change.to, let similarity = change.similarity { - XCTAssert(value == "swiftLang") - XCTAssert(similarity > 0.5) - } else { - XCTFail("Change did not provide the updated name of the enum case") + + let comparator = EnumComparator(lhs: enumeration, rhs: updated) + comparator.compare(comparisonContext, &modelChanges) + + XCTAssertEqual(modelChanges.count, 2) // update of the raw value as well + let change = try XCTUnwrap(modelChanges.first) + XCTAssertEqual(change.id, enumeration.deltaIdentifier) + XCTAssertEqual(change.type, .update) + XCTAssertEqual(change.breaking, true) + XCTAssertEqual(change.solvable, true) + + let updateChange = try XCTUnwrap(change.modeledUpdateChange) + guard case let .case(caseChange) = updateChange.updated else { + XCTFail("Change did not store the updated enum case") + return } + + XCTAssertEqual(caseChange.id, "swift") + XCTAssertEqual(caseChange.type, .idChange) + XCTAssertEqual(change.breaking, caseChange.breaking) + XCTAssertEqual(change.solvable, caseChange.solvable) + + let caseRename = try XCTUnwrap(caseChange.modeledIdentifierChange) + XCTAssertEqual(caseRename.from, caseChange.id) + XCTAssertEqual(caseRename.to, "swiftLang") + XCTAssert(try XCTUnwrap(caseRename.similarity) > 0.5) } func testAddedEnumCase() throws { - let updated: TypeInformation = .enum(name: enumeration.typeName, rawValueType: .scalar(.string), cases: enumeration.enumCases + .init("newCase")) - let enumComparator = EnumComparator(lhs: enumeration, rhs: updated, changes: node, configuration: .default) - enumComparator.compare() - - XCTAssert(node.changes.count == 1) - let change = try XCTUnwrap(node.changes.first as? AddChange) - - XCTAssert(change.element == .enum(enumeration.deltaIdentifier, target: .case)) - XCTAssert(!change.breaking) - XCTAssert(change.solvable) - XCTAssert(change.providerSupport == .renameHint(AddChange.self)) - if case let .element(codable) = change.added { - XCTAssert(codable.typed(EnumCase.self) == .init("newCase")) - } else { - XCTFail("Did not provide the added enum case") + let updated: TypeInformation = .enum( + name: enumeration.typeName, + rawValueType: .scalar(.string), + cases: enumeration.enumCases + .init("newCase") + ) + + let comparator = EnumComparator(lhs: enumeration, rhs: updated) + comparator.compare(comparisonContext, &modelChanges) + + XCTAssertEqual(modelChanges.count, 1) + let change = try XCTUnwrap(modelChanges.first) + XCTAssertEqual(change.id, enumeration.deltaIdentifier) + XCTAssertEqual(change.type, .update) + XCTAssertEqual(change.breaking, false) + XCTAssertEqual(change.solvable, true) + + let updateChange = try XCTUnwrap(change.modeledUpdateChange) + guard case let .case(caseChange) = updateChange.updated else { + XCTFail("Change did not store the updated enum case") + return } + + XCTAssertEqual(caseChange.id, "newCase") + XCTAssertEqual(caseChange.type, .addition) + XCTAssertEqual(change.breaking, caseChange.breaking) + XCTAssertEqual(change.solvable, caseChange.solvable) + + let caseAddition = try XCTUnwrap(caseChange.modeledAdditionChange) + XCTAssertEqual(caseAddition.added, EnumCase("newCase")) + XCTAssertEqual(caseAddition.defaultValue, nil) } func testUnsupportedRawValueTypeChange() throws { let updated: TypeInformation = .enum(name: enumeration.typeName, rawValueType: .scalar(.int), cases: enumeration.enumCases) - let enumComparator = EnumComparator(lhs: enumeration, rhs: updated, changes: node, configuration: .default) - enumComparator.compare() - - XCTAssert(node.changes.count == 1) - - let change = try XCTUnwrap(node.changes.first as? UnsupportedChange) - XCTAssert(change.element == .enum(enumeration.deltaIdentifier, target: .`self`)) - XCTAssertEqual(change.type, .unsupported) - XCTAssert(change.breaking) - XCTAssert(!change.solvable) + + let comparator = EnumComparator(lhs: enumeration, rhs: updated) + comparator.compare(comparisonContext, &modelChanges) + + XCTAssertEqual(modelChanges.count, 1) + let change = try XCTUnwrap(modelChanges.first) + XCTAssertEqual(change.id, enumeration.deltaIdentifier) + XCTAssertEqual(change.type, .update) + XCTAssertEqual(change.breaking, true) + XCTAssertEqual(change.solvable, false) + + let updateChange = try XCTUnwrap(change.modeledUpdateChange) + guard case let .rawValueType(from, to) = updateChange.updated else { + XCTFail("Change did not store the updated raw value type") + return + } + + XCTAssertEqual(from, .scalar(.string)) + XCTAssertEqual(to, .scalar(.int)) } func testIgnoreCompareWithNonEnum() { - let enumComparator = EnumComparator(lhs: enumeration, rhs: .scalar(.bool), changes: node, configuration: .default) - enumComparator.compare() - XCTAssert(node.isEmpty) + let comparator = EnumComparator(lhs: self.enumeration, rhs: .scalar(.bool)) + XCTAssertRuntimeFailure(comparator.compare(self.comparisonContext, &self.modelChanges)) } } diff --git a/Tests/ApodiniMigratorTests/ApodiniMigratorCompareTests/JavaScriptConvertTests.swift b/Tests/ApodiniMigratorTests/ApodiniMigratorCompareTests/JavaScriptConvertTests.swift index b7c31542..91f48be3 100644 --- a/Tests/ApodiniMigratorTests/ApodiniMigratorCompareTests/JavaScriptConvertTests.swift +++ b/Tests/ApodiniMigratorTests/ApodiniMigratorCompareTests/JavaScriptConvertTests.swift @@ -9,7 +9,7 @@ import XCTest @testable import ApodiniMigratorCore @testable import ApodiniMigratorClientSupport -@testable import ApodiniMigrator +@testable import RESTMigrator @testable import ApodiniMigratorCompare fileprivate extension ApodiniMigratorCodable { @@ -42,6 +42,7 @@ extension Data: Codable { private typealias Codable = ApodiniMigratorCodable +// swiftlint:disable:next type_body_length final class JavaScriptConvertTests: ApodiniMigratorXCTestCase { func testComplexTypeScriptConvert() throws { guard canImportJavaScriptCore() else { @@ -166,8 +167,8 @@ final class JavaScriptConvertTests: ApodiniMigratorXCTestCase { """ let student = XCTAssertNoThrowWithResult(try Student.from(0, script: script)) - XCTAssert(student.dog.name == "") - XCTAssert(student.name == "") + XCTAssert(student.dog.name.isEmpty) + XCTAssert(student.name.isEmpty) XCTAssert(student.number == 0) let invalidConvert: JSScript = @@ -181,8 +182,8 @@ final class JavaScriptConvertTests: ApodiniMigratorXCTestCase { """ let secondStudent = try Student.fromValues("John", UUID(), Dog(name: "Dog"), script: invalidConvert) - XCTAssert(secondStudent.dog.name == "") - XCTAssert(secondStudent.name == "") + XCTAssert(secondStudent.dog.name.isEmpty) + XCTAssert(secondStudent.name.isEmpty) XCTAssert(secondStudent.number == 0) } @@ -232,7 +233,7 @@ final class JavaScriptConvertTests: ApodiniMigratorXCTestCase { guard canImportJavaScriptCore() else { return } - let scriptBuilder = JSScriptBuilder(from: .optional(wrappedValue: .scalar(.string)), to: .scalar(.date)) + let scriptBuilder = JSScriptBuilder(from: .optional(wrappedValue: .scalar(.string)), to: .scalar(.date), context: comparisonContext) XCTAssertNoThrow(try String?.from(Date(), script: scriptBuilder.convertToFrom)) } @@ -387,9 +388,9 @@ final class JavaScriptConvertTests: ApodiniMigratorXCTestCase { guard canImportJavaScriptCore() else { return } - let js = JSPrimitiveScript.identity(for: .bool) + let script = JSPrimitiveScript.identity(for: .bool) - XCTAssertNoThrow(try Double.from(Date(Default()), script: js.convertToFrom)) + XCTAssertNoThrow(try Double.from(Date(Default()), script: script.convertToFrom)) } func testIgonoreInput() throws { @@ -439,7 +440,11 @@ final class JavaScriptConvertTests: ApodiniMigratorXCTestCase { let name: String } - let jsBuilder = JSScriptBuilder(from: try TypeInformation(type: User.self), to: try TypeInformation(type: UserNew.self)) + let jsBuilder = JSScriptBuilder( + from: try TypeInformation(type: User.self), + to: try TypeInformation(type: UserNew.self), + context: comparisonContext + ) let newUser = UserNew(ident: .init(), name: "I am new user") let user = try User.from(newUser, script: jsBuilder.convertToFrom) diff --git a/Tests/ApodiniMigratorTests/ApodiniMigratorCompareTests/MetaDataComparatorTests.swift b/Tests/ApodiniMigratorTests/ApodiniMigratorCompareTests/MetaDataComparatorTests.swift deleted file mode 100644 index f6b07c88..00000000 --- a/Tests/ApodiniMigratorTests/ApodiniMigratorCompareTests/MetaDataComparatorTests.swift +++ /dev/null @@ -1,95 +0,0 @@ -// -// This source file is part of the Apodini open source project -// -// SPDX-FileCopyrightText: 2019-2021 Paul Schmiedmayer and the Apodini project authors (see CONTRIBUTORS.md) -// -// SPDX-License-Identifier: MIT -// - -import XCTest -@testable import ApodiniMigratorCore -@testable import ApodiniMigratorCompare - -final class MetaDataComparatorTests: ApodiniMigratorXCTestCase { - func testServerPathChanged() throws { - let lhs = MetaData(serverPath: "www.test.com", version: .default, encoderConfiguration: .default, decoderConfiguration: .default) - let rhs = MetaData(serverPath: "www.updated.com", version: .default, encoderConfiguration: .default, decoderConfiguration: .default) - - let comparator = MetaDataComparator(lhs: lhs, rhs: rhs, changes: node, configuration: .default) - comparator.compare() - - XCTAssert(node.changes.count == 1) - let serverPathChange = try XCTUnwrap(node.changes.first as? UpdateChange) - XCTAssert(serverPathChange.breaking) - XCTAssert(serverPathChange.solvable) - XCTAssert(serverPathChange.type == .update) - XCTAssert(serverPathChange.element == .networking(target: .serverPath)) - if case let .stringValue(value) = serverPathChange.to { - XCTAssert(value == "www.updated.com/v1") - } else { - XCTFail("Change did not store the updated server path") - } - } - - func testVersionChanged() throws { - let lhs = MetaData(serverPath: "www.test.com", version: .default, encoderConfiguration: .default, decoderConfiguration: .default) - let rhs = MetaData(serverPath: "www.test.com", version: .init(prefix: "api", major: 2, minor: 0, patch: 0), encoderConfiguration: .default, decoderConfiguration: .default) - - let comparator = MetaDataComparator(lhs: lhs, rhs: rhs, changes: node, configuration: .default) - comparator.compare() - - XCTAssert(node.changes.count == 1) - let serverPathChange = try XCTUnwrap(node.changes.first as? UpdateChange) - XCTAssert(serverPathChange.type == .update) - XCTAssert(serverPathChange.breaking) - XCTAssert(serverPathChange.solvable) - XCTAssert(serverPathChange.element == .networking(target: .serverPath)) - if case let .stringValue(value) = serverPathChange.to { - XCTAssert(value == "www.test.com/api2") - } else { - XCTFail("Change did not store the updated server path") - } - } - - func testEncoderConfigurationChanged() throws { - let lhs = MetaData(serverPath: "", version: .default, encoderConfiguration: .default, decoderConfiguration: .default) - let updatedConfiguration = EncoderConfiguration(dateEncodingStrategy: .millisecondsSince1970, dataEncodingStrategy: .base64) - let rhs = MetaData(serverPath: "", version: .default, encoderConfiguration: updatedConfiguration, decoderConfiguration: .default) - - let comparator = MetaDataComparator(lhs: lhs, rhs: rhs, changes: node, configuration: .default) - comparator.compare() - - XCTAssert(node.changes.count == 1) - let change = try XCTUnwrap(node.changes.first as? UpdateChange) - XCTAssert(change.breaking) - XCTAssert(change.solvable) - XCTAssert(change.element == .networking(target: .encoderConfiguration)) - - if case let .element(codable) = change.to { - XCTAssert(codable.typed(EncoderConfiguration.self) == updatedConfiguration) - } else { - XCTFail("Change did not store the updated configuration") - } - } - - func testDecoderConfigurationChanged() throws { - let lhs = MetaData(serverPath: "", version: .default, encoderConfiguration: .default, decoderConfiguration: .default) - let updatedConfiguration = DecoderConfiguration(dateDecodingStrategy: .secondsSince1970, dataDecodingStrategy: .base64) - let rhs = MetaData(serverPath: "", version: .default, encoderConfiguration: .default, decoderConfiguration: updatedConfiguration) - - let comparator = MetaDataComparator(lhs: lhs, rhs: rhs, changes: node, configuration: .default) - comparator.compare() - - XCTAssert(node.changes.count == 1) - let change = try XCTUnwrap(node.changes.first as? UpdateChange) - XCTAssert(change.breaking) - XCTAssert(change.solvable) - XCTAssert(change.element == .networking(target: .decoderConfiguration)) - - if case let .element(codable) = change.to { - XCTAssert(codable.typed(DecoderConfiguration.self) == updatedConfiguration) - } else { - XCTFail("Change did not store the updated configuration") - } - } -} diff --git a/Tests/ApodiniMigratorTests/ApodiniMigratorCompareTests/ModelsComparatorTests.swift b/Tests/ApodiniMigratorTests/ApodiniMigratorCompareTests/ModelsComparatorTests.swift index 3674a728..4d9b3cf5 100644 --- a/Tests/ApodiniMigratorTests/ApodiniMigratorCompareTests/ModelsComparatorTests.swift +++ b/Tests/ApodiniMigratorTests/ApodiniMigratorCompareTests/ModelsComparatorTests.swift @@ -11,8 +11,15 @@ import XCTest @testable import ApodiniMigratorCompare final class ModelsComparatorTests: ApodiniMigratorXCTestCase { + var modelChanges = [ModelChange]() + + override func tearDownWithError() throws { + try super.tearDownWithError() + modelChanges.removeAll() + } + let user: TypeInformation = .object( - name: .init(name: "User"), + name: .init(rawValue: "User"), properties: [ .init(name: "id", type: .scalar(.uuid)), .init(name: "name", type: .scalar(.string)), @@ -22,11 +29,11 @@ final class ModelsComparatorTests: ApodiniMigratorXCTestCase { ) var renamedUser: TypeInformation { - .object(name: .init(name: "UserNew"), properties: user.objectProperties) + .object(name: .init(rawValue: "UserNew"), properties: user.objectProperties) } let programmingLanguages: TypeInformation = .enum( - name: .init(name: "ProgLang"), + name: .init(rawValue: "ProgLang"), rawValueType: .scalar(.string), cases: [ .init("swift"), @@ -38,98 +45,100 @@ final class ModelsComparatorTests: ApodiniMigratorXCTestCase { override func setUp() { super.setUp() - - node = ChangeContextNode(compareConfiguration: .active) + + comparisonContext = ChangeComparisonContext(configuration: .active) } - func testNoModelsChange() throws { - let modelsComparator = ModelsComparator(lhs: [user, programmingLanguages], rhs: [programmingLanguages, user], changes: node, configuration: .default) - modelsComparator.compare() - XCTAssert(node.isEmpty) + func testModelComparatorCommutativity() throws { + let comparator = ModelsComparator(lhs: [user, programmingLanguages], rhs: [programmingLanguages, user]) + comparator.compare(comparisonContext, &modelChanges) + XCTAssert(modelChanges.isEmpty) } func testModelDeleted() throws { - let modelsComparator = ModelsComparator(lhs: [user, programmingLanguages], rhs: [user], changes: node, configuration: .default) - modelsComparator.compare() - XCTAssert(node.changes.count == 1) - let deleteChange = try XCTUnwrap(node.changes.first as? DeleteChange) - - XCTAssert(deleteChange.element == .enum(programmingLanguages.deltaIdentifier, target: .`self`)) - XCTAssert(!deleteChange.breaking) - XCTAssert(!deleteChange.solvable) - XCTAssert(deleteChange.fallbackValue == .none) - XCTAssert(deleteChange.providerSupport == .renameHint(DeleteChange.self)) - if let providerSupport = deleteChange.providerSupport { - let decodedInstance = XCTAssertNoThrowWithResult(try ProviderSupport.decode(from: providerSupport.json)) - XCTAssert(decodedInstance == deleteChange.providerSupport) - XCTAssertNoThrow(try ProviderSupport.decode(from: "{}")) - } + let comparator = ModelsComparator(lhs: [user, programmingLanguages], rhs: [user]) + comparator.compare(comparisonContext, &modelChanges) + + XCTAssertEqual(modelChanges.count, 1) + let change = try XCTUnwrap(modelChanges.first) + XCTAssertEqual(change.id, programmingLanguages.deltaIdentifier) + XCTAssertEqual(change.type, .removal) + XCTAssertEqual(change.breaking, false) + XCTAssertEqual(change.solvable, false) + + let removalChange = try XCTUnwrap(change.modeledRemovalChange) + XCTAssertEqual(removalChange.removed, nil) + XCTAssertEqual(removalChange.fallbackValue, nil) } func testModelAdded() throws { - let modelsComparator = ModelsComparator(lhs: [user], rhs: [user, programmingLanguages], changes: node, configuration: .default) - modelsComparator.compare() - XCTAssert(node.changes.count == 1) - let addChange = try XCTUnwrap(node.changes.first as? AddChange) - - XCTAssert(addChange.element == .enum(programmingLanguages.deltaIdentifier, target: .`self`)) - XCTAssert(!addChange.breaking) - XCTAssert(addChange.providerSupport == .renameHint(AddChange.self)) - XCTAssert(addChange.solvable) - - if case let .element(codable) = addChange.added { - XCTAssert(codable.typed(TypeInformation.self) == programmingLanguages) - } else { - XCTFail("Added enumeration was not stored in the change object") - } + let comparator = ModelsComparator(lhs: [user], rhs: [user, programmingLanguages]) + comparator.compare(comparisonContext, &modelChanges) + + XCTAssertEqual(modelChanges.count, 1) + let change = try XCTUnwrap(modelChanges.first) + XCTAssertEqual(change.id, programmingLanguages.deltaIdentifier) + XCTAssertEqual(change.type, .addition) + XCTAssertEqual(change.breaking, false) + XCTAssertEqual(change.solvable, true) + + let additionChange = try XCTUnwrap(change.modeledAdditionChange) + XCTAssertEqual(additionChange.added, programmingLanguages) } func testModelRenamed() throws { - let endpointsComparator = ModelsComparator(lhs: [user], rhs: [renamedUser], changes: node, configuration: .default) - endpointsComparator.compare() - XCTAssert(node.changes.count == 1) - let renameChange = try XCTUnwrap(node.changes.first as? UpdateChange) - let providerSupport = try XCTUnwrap(renameChange.providerSupport) - - XCTAssert(renameChange.element == .object(user.deltaIdentifier, target: .typeName)) - XCTAssert(!renameChange.breaking) - XCTAssert(renameChange.solvable) - XCTAssert(renameChange.type == .rename) - XCTAssert(providerSupport == .renameValidationHint) - - if case let .stringValue(value) = renameChange.to, let similarity = renameChange.similarity { - XCTAssert(value == "UserNew") - XCTAssert(similarity > 0.5) - } else { - XCTFail("Rename change did not store the updated string value of the new type name") - } + let comparator = ModelsComparator(lhs: [user], rhs: [renamedUser]) + comparator.compare(comparisonContext, &modelChanges) + + XCTAssertEqual(modelChanges.count, 1) + let change = try XCTUnwrap(modelChanges.first) + XCTAssertEqual(change.id, user.deltaIdentifier) + XCTAssertEqual(change.type, .idChange) + XCTAssertEqual(change.breaking, false) + XCTAssertEqual(change.solvable, true) + + let idChange = try XCTUnwrap(change.modeledIdentifierChange) + XCTAssertEqual(idChange.to, "UserNew") + XCTAssert(try XCTUnwrap(idChange.similarity) > 0.5) } func testJSObjectScriptForRenamedType() { - let obj1: TypeInformation = .object(name: .init(name: "Test"), properties: [.init(name: "prop1", type: user)]) - let obj2: TypeInformation = .object(name: .init(name: "Test"), properties: [.init(name: "prop1", type: renamedUser)]) - let comp2 = ModelsComparator(lhs: [obj1, user], rhs: [obj2, renamedUser], changes: node, configuration: .default) - comp2.compare() - - let scriptBuilder = JSObjectScript(from: obj1, to: obj2, changes: node, encoderConfiguration: .default) + let obj1: TypeInformation = .object(name: .init(rawValue: "Test"), properties: [.init(name: "prop1", type: user)]) + let obj2: TypeInformation = .object(name: .init(rawValue: "Test"), properties: [.init(name: "prop1", type: renamedUser)]) + + let comparator = ModelsComparator(lhs: [obj1, user], rhs: [obj2, renamedUser]) + comparator.compare(comparisonContext, &modelChanges) + comparisonContext.modelChanges = modelChanges + + let scriptBuilder = JSObjectScript(from: obj1, to: obj2, context: comparisonContext) XCTAssert(scriptBuilder.convertFromTo.rawValue.contains("'prop1': parsedFrom.prop1")) XCTAssert(scriptBuilder.convertToFrom.rawValue.contains("'prop1': parsedTo.prop1")) } func testUnsupportedTypeChange() throws { let changedUser: TypeInformation = .enum( - name: .init(name: "User"), + name: .init(rawValue: "User"), rawValueType: .scalar(.string), cases: [] ) - let endpointsComparator = ModelsComparator(lhs: [user], rhs: [changedUser], changes: node, configuration: .default) - endpointsComparator.compare() - XCTAssert(node.changes.count == 1) - - let change = try XCTUnwrap(node.changes.first as? UnsupportedChange) - XCTAssert(change.element == .object(user.deltaIdentifier, target: .`self`)) - XCTAssertEqual(change.type, .unsupported) - XCTAssert(change.breaking) - XCTAssert(!change.solvable) + + let comparator = ModelsComparator(lhs: [user], rhs: [changedUser]) + comparator.compare(comparisonContext, &modelChanges) + + XCTAssertEqual(modelChanges.count, 1) + let change = try XCTUnwrap(modelChanges.first) + XCTAssertEqual(change.id, user.deltaIdentifier) + XCTAssertEqual(change.type, .update) + XCTAssertEqual(change.breaking, true) + XCTAssertEqual(change.solvable, false) + + let updateChange = try XCTUnwrap(change.modeledUpdateChange) + if case let .rootType(from, to, newModel) = updateChange.updated { + XCTAssertEqual(from, .object) + XCTAssertEqual(to, .enum) + XCTAssertEqual(newModel, changedUser) + } else { + XCTFail("Encountered unexpected update change: \(updateChange)") + } } } diff --git a/Tests/ApodiniMigratorTests/ApodiniMigratorCompareTests/ObjectComparatorTests.swift b/Tests/ApodiniMigratorTests/ApodiniMigratorCompareTests/ObjectComparatorTests.swift index 5172d032..b9b047e9 100644 --- a/Tests/ApodiniMigratorTests/ApodiniMigratorCompareTests/ObjectComparatorTests.swift +++ b/Tests/ApodiniMigratorTests/ApodiniMigratorCompareTests/ObjectComparatorTests.swift @@ -11,8 +11,15 @@ import XCTest @testable import ApodiniMigratorCompare final class ObjectComparatorTests: ApodiniMigratorXCTestCase { + var modelChanges = [ModelChange]() + + override func tearDownWithError() throws { + try super.tearDownWithError() + modelChanges.removeAll() + } + let user: TypeInformation = .object( - name: .init(name: "User"), + name: .init(rawValue: "User"), properties: [ .init(name: "id", type: .scalar(.uuid)), .init(name: "name", type: .scalar(.string)), @@ -25,35 +32,46 @@ final class ObjectComparatorTests: ApodiniMigratorXCTestCase { override func setUp() { super.setUp() - - node = ChangeContextNode(compareConfiguration: .active) + + comparisonContext = ChangeComparisonContext(configuration: .active) } func testNoObjectChange() { - let objectComparator = ObjectComparator(lhs: user, rhs: user, changes: node, configuration: .default) - objectComparator.compare() - XCTAssert(node.isEmpty) + let comparator = ObjectComparator(lhs: user, rhs: user) + comparator.compare(comparisonContext, &modelChanges) + XCTAssert(modelChanges.isEmpty) } func testAddedObjectProperty() throws { let newProperty: TypeProperty = .init(name: "birthday", type: .scalar(.date)) let updated: TypeInformation = .object(name: user.typeName, properties: user.objectProperties + newProperty) - let objectComparator = ObjectComparator(lhs: user, rhs: updated, changes: node, configuration: .default) - objectComparator.compare() - - XCTAssert(node.changes.count == 1) - let change = try XCTUnwrap(node.changes.first as? AddChange) - XCTAssert(change.element == .object(user.deltaIdentifier, target: .property)) - XCTAssert(change.breaking) - XCTAssert(change.solvable) - XCTAssert(change.providerSupport == .renameHint(AddChange.self)) - if case let .element(codable) = change.added { - XCTAssert(codable.typed(TypeProperty.self) == newProperty) - } else { - XCTFail("Did not provide the added property") + + let comparator = ObjectComparator(lhs: user, rhs: updated) + comparator.compare(comparisonContext, &modelChanges) + + XCTAssertEqual(modelChanges.count, 1) + let change = try XCTUnwrap(modelChanges.first) + XCTAssertEqual(change.id, user.deltaIdentifier) + XCTAssertEqual(change.type, .update) + XCTAssertEqual(change.breaking, true) + XCTAssertEqual(change.solvable, true) + + let updateChange = try XCTUnwrap(change.modeledUpdateChange) + guard case let .property(propertyChange) = updateChange.updated else { + XCTFail("Change did not store the updated property") + return } - - if case let .json(id) = change.defaultValue, let json = node.jsonValues[id] { + + XCTAssertEqual(propertyChange.type, .addition) + XCTAssertEqual(change.breaking, propertyChange.breaking) + XCTAssertEqual(change.solvable, propertyChange.solvable) + XCTAssertEqual(propertyChange.id, newProperty.deltaIdentifier) + + let propertyAddition = try XCTUnwrap(propertyChange.modeledAdditionChange) + XCTAssertEqual(propertyAddition.added, newProperty) + + if let defaultValue = propertyAddition.defaultValue, + let json = comparisonContext.jsonValues[defaultValue] { XCTAssertNoThrow(try Date.instance(from: json)) } else { XCTFail("Did not provide a default value for the added required property") @@ -62,23 +80,33 @@ final class ObjectComparatorTests: ApodiniMigratorXCTestCase { func testDeletedProperty() throws { let updated: TypeInformation = .object(name: user.typeName, properties: user.objectProperties.filter { $0.name != "githubProfile" }) - let objectComparator = ObjectComparator(lhs: user, rhs: updated, changes: node, configuration: .default) - objectComparator.compare() - - XCTAssert(node.changes.count == 1) - let deleteChange = try XCTUnwrap(node.changes.first as? DeleteChange) - - XCTAssert(deleteChange.element == .object(user.deltaIdentifier, target: .property)) - XCTAssert(deleteChange.breaking) - XCTAssert(deleteChange.solvable) - XCTAssert(deleteChange.providerSupport == .renameHint(DeleteChange.self)) - if case let .elementID(id) = deleteChange.deleted { - XCTAssert(id == "githubProfile") - } else { - XCTFail("Did not provide the id of the deleted property") + + let comparator = ObjectComparator(lhs: user, rhs: updated) + comparator.compare(comparisonContext, &modelChanges) + + XCTAssertEqual(modelChanges.count, 1) + let change = try XCTUnwrap(modelChanges.first) + XCTAssertEqual(change.id, user.deltaIdentifier) + XCTAssertEqual(change.type, .update) + XCTAssertEqual(change.breaking, true) + XCTAssertEqual(change.solvable, true) + + let updateChange = try XCTUnwrap(change.modeledUpdateChange) + guard case let .property(propertyChange) = updateChange.updated else { + XCTFail("Change did not store the updated property") + return } - - if case let .json(id) = deleteChange.fallbackValue, let json = node.jsonValues[id] { + + XCTAssertEqual(propertyChange.type, .removal) + XCTAssertEqual(change.breaking, propertyChange.breaking) + XCTAssertEqual(change.solvable, propertyChange.solvable) + XCTAssertEqual(propertyChange.id, "githubProfile") + + let propertyRemoval = try XCTUnwrap(propertyChange.modeledRemovalChange) + XCTAssertEqual(propertyRemoval.removed, nil) + + if let fallbackValue = propertyRemoval.fallbackValue, + let json = comparisonContext.jsonValues[fallbackValue] { XCTAssertNoThrow(try URL.instance(from: json)) } else { XCTFail("Did not provide a fallback value of deleted property") @@ -90,23 +118,32 @@ final class ObjectComparatorTests: ApodiniMigratorXCTestCase { name: user.typeName, properties: user.objectProperties.filter { $0.name != "githubProfile" } + .init(name: "github", type: .scalar(.url)) ) - - let objectComparator = ObjectComparator(lhs: user, rhs: updated, changes: node, configuration: .default) - objectComparator.compare() - - XCTAssert(node.changes.count == 1) - let change = try XCTUnwrap(node.changes.first as? UpdateChange) - XCTAssert(change.element == .object(user.deltaIdentifier, target: .property)) - XCTAssert(change.type == .rename) - XCTAssert(change.targetID == "githubProfile") - XCTAssert(change.breaking) - XCTAssert(change.solvable) - if case let .stringValue(value) = change.to, let similarity = change.similarity { - XCTAssert(value == "github") - XCTAssert(similarity > 0.5) - } else { - XCTFail("Change did not provide the updated name of the property") + + let comparator = ObjectComparator(lhs: user, rhs: updated) + comparator.compare(comparisonContext, &modelChanges) + + XCTAssertEqual(modelChanges.count, 1) + let change = try XCTUnwrap(modelChanges.first) + XCTAssertEqual(change.id, user.deltaIdentifier) + XCTAssertEqual(change.type, .update) + XCTAssertEqual(change.breaking, true) + XCTAssertEqual(change.solvable, true) + let updateChange = try XCTUnwrap(change.modeledUpdateChange) + + guard case let .property(propertyChange) = updateChange.updated else { + XCTFail("Change did not store the updated property") + return } + + XCTAssertEqual(propertyChange.type, .idChange) + XCTAssertEqual(change.breaking, propertyChange.breaking) + XCTAssertEqual(change.solvable, propertyChange.solvable) + XCTAssertEqual(propertyChange.id, "githubProfile") + + let propertyRename = try XCTUnwrap(propertyChange.modeledIdentifierChange) + XCTAssertEqual(propertyRename.from, propertyChange.id) + XCTAssertEqual(propertyRename.to, "github") + XCTAssert(try XCTUnwrap(propertyRename.similarity) > 0.5) } func testPropertyNecessityToRequiredChange() throws { @@ -114,24 +151,37 @@ final class ObjectComparatorTests: ApodiniMigratorXCTestCase { name: user.typeName, properties: user.objectProperties.filter { $0.name != "age" } + .init(name: "age", type: .scalar(.uint)) ) - - let objectComparator = ObjectComparator(lhs: user, rhs: updated, changes: node, configuration: .default) - objectComparator.compare() - - XCTAssert(node.changes.count == 1) - let change = try XCTUnwrap(node.changes.first as? UpdateChange) - XCTAssert(change.element == .object(user.deltaIdentifier, target: .necessity)) - XCTAssert(change.type == .update) - XCTAssert(change.targetID == "age") - XCTAssert(change.breaking) - XCTAssert(change.solvable) - if case let .element(codable) = change.to { - XCTAssert(codable.typed(Necessity.self) == .required) - } else { - XCTFail("Change did not provide the updated necessity of the property") + + let comparator = ObjectComparator(lhs: user, rhs: updated) + comparator.compare(comparisonContext, &modelChanges) + + XCTAssertEqual(modelChanges.count, 1) + let change = try XCTUnwrap(modelChanges.first) + XCTAssertEqual(change.id, user.deltaIdentifier) + XCTAssertEqual(change.type, .update) + XCTAssertEqual(change.breaking, true) + XCTAssertEqual(change.solvable, true) + let updateChange = try XCTUnwrap(change.modeledUpdateChange) + + guard case let .property(propertyChange) = updateChange.updated else { + XCTFail("Change did not store the updated property") + return } - - if case let .json(id) = change.necessityValue, let json = node.jsonValues[id] { + + XCTAssertEqual(propertyChange.type, .update) + XCTAssertEqual(change.breaking, propertyChange.breaking) + XCTAssertEqual(change.solvable, propertyChange.solvable) + XCTAssertEqual(propertyChange.id, "age") + + let propertyUpdate = try XCTUnwrap(propertyChange.modeledUpdateChange) + guard case let .necessity(from, to, necessityMigration) = propertyUpdate.updated else { + XCTFail("Unexpected property update change: \(propertyUpdate.updated)") + return + } + XCTAssertEqual(from, .optional) + XCTAssertEqual(to, .required) + + if let json = comparisonContext.jsonValues[necessityMigration] { XCTAssertNoThrow(try UInt.instance(from: json)) } else { XCTFail("Did not provide a necessity value for the updated property") @@ -143,23 +193,37 @@ final class ObjectComparatorTests: ApodiniMigratorXCTestCase { name: user.typeName, properties: user.objectProperties.filter { $0.name != "name" } + .init(name: "name", type: .optional(wrappedValue: .scalar(.string))) ) - - let objectComparator = ObjectComparator(lhs: user, rhs: updated, changes: node, configuration: .default) - objectComparator.compare() - - XCTAssert(node.changes.count == 1) - let change = try XCTUnwrap(node.changes.first as? UpdateChange) - XCTAssert(change.element == .object(user.deltaIdentifier, target: .necessity)) - XCTAssert(change.type == .update) - XCTAssert(change.breaking) - XCTAssert(change.solvable) - if case let .element(codable) = change.to { - XCTAssert(codable.typed(Necessity.self) == .optional) - } else { - XCTFail("Change did not provide the updated necessity of the property") + + let comparator = ObjectComparator(lhs: user, rhs: updated) + comparator.compare(comparisonContext, &modelChanges) + + XCTAssertEqual(modelChanges.count, 1) + let change = try XCTUnwrap(modelChanges.first) + XCTAssertEqual(change.id, user.deltaIdentifier) + XCTAssertEqual(change.type, .update) + XCTAssertEqual(change.breaking, true) + XCTAssertEqual(change.solvable, true) + let updateChange = try XCTUnwrap(change.modeledUpdateChange) + + guard case let .property(propertyChange) = updateChange.updated else { + XCTFail("Change did not store the updated property") + return } - - if case let .json(id) = change.necessityValue, let json = node.jsonValues[id] { + + XCTAssertEqual(propertyChange.type, .update) + XCTAssertEqual(change.breaking, propertyChange.breaking) + XCTAssertEqual(change.solvable, propertyChange.solvable) + XCTAssertEqual(propertyChange.id, "name") + + let propertyUpdate = try XCTUnwrap(propertyChange.modeledUpdateChange) + guard case let .necessity(from, to, necessityMigration) = propertyUpdate.updated else { + XCTFail("Unexpected property update change: \(propertyUpdate.updated)") + return + } + XCTAssertEqual(from, .required) + XCTAssertEqual(to, .optional) + + if let json = comparisonContext.jsonValues[necessityMigration] { XCTAssertNoThrow(try String.instance(from: json)) } else { XCTFail("Did not provide a necessity value for the updated property") @@ -170,33 +234,50 @@ final class ObjectComparatorTests: ApodiniMigratorXCTestCase { guard canImportJavaScriptCore() else { return } + let updated: TypeInformation = .object( name: user.typeName, properties: user.objectProperties.filter { $0.name != "isStudent" } + .init(name: "isStudent", type: .scalar(.bool)) ) - - let objectComparator = ObjectComparator(lhs: user, rhs: updated, changes: node, configuration: .default) - objectComparator.compare() - - XCTAssert(node.changes.count == 1) - let change = try XCTUnwrap(node.changes.first as? UpdateChange) - XCTAssert(change.element == .object(user.deltaIdentifier, target: .property)) - XCTAssert(change.type == .propertyChange) - XCTAssert(change.breaking) - XCTAssert(change.solvable) - if case let .element(codable) = change.to { - XCTAssert(codable.typed(TypeInformation.self) == .scalar(.bool)) - } else { - XCTFail("Change did not provide the updated type of the property") + + let comparator = ObjectComparator(lhs: user, rhs: updated) + comparator.compare(comparisonContext, &modelChanges) + + XCTAssertEqual(modelChanges.count, 1) + let change = try XCTUnwrap(modelChanges.first) + XCTAssertEqual(change.id, user.deltaIdentifier) + XCTAssertEqual(change.type, .update) + XCTAssertEqual(change.breaking, true) + XCTAssertEqual(change.solvable, true) + let updateChange = try XCTUnwrap(change.modeledUpdateChange) + + guard case let .property(propertyChange) = updateChange.updated else { + XCTFail("Change did not store the updated property") + return } - - if let convertFromTo = change.convertFromTo, let script = node.scripts[convertFromTo] { + + XCTAssertEqual(propertyChange.type, .update) + XCTAssertEqual(change.breaking, propertyChange.breaking) + XCTAssertEqual(change.solvable, propertyChange.solvable) + XCTAssertEqual(propertyChange.id, "isStudent") + + let propertyUpdate = try XCTUnwrap(propertyChange.modeledUpdateChange) + guard case let .type(from, to, forwardMigration, backwardMigration, conversionWarning) = propertyUpdate.updated else { + XCTFail("Unexpected property update change: \(propertyUpdate.updated)") + return + } + + XCTAssertEqual(from, .scalar(.string)) + XCTAssertEqual(to, .scalar(.bool)) + XCTAssertEqual(conversionWarning, nil) + + if let script = comparisonContext.scripts[forwardMigration] { XCTAssertEqual(false, try Bool.from("NO", script: script)) } else { XCTFail("Did not provide the convert script for updated property type") } - if let convertToFrom = change.convertToFrom, let script = node.scripts[convertToFrom] { + if let script = comparisonContext.scripts[backwardMigration] { XCTAssertEqual("YES", try String.from(true, script: script)) } else { XCTFail("Did not provide the convert script for updated property type") diff --git a/Tests/ApodiniMigratorTests/ApodiniMigratorCompareTests/ServiceInformationComparatorTests.swift b/Tests/ApodiniMigratorTests/ApodiniMigratorCompareTests/ServiceInformationComparatorTests.swift new file mode 100644 index 00000000..992da7f8 --- /dev/null +++ b/Tests/ApodiniMigratorTests/ApodiniMigratorCompareTests/ServiceInformationComparatorTests.swift @@ -0,0 +1,141 @@ +// +// This source file is part of the Apodini open source project +// +// SPDX-FileCopyrightText: 2019-2021 Paul Schmiedmayer and the Apodini project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + +import XCTest +@testable import ApodiniMigratorCore +@testable import ApodiniMigratorCompare + +final class ServiceInformationComparatorTests: ApodiniMigratorXCTestCase { + var serviceChanges = [ServiceInformationChange]() + + override func tearDownWithError() throws { + try super.tearDownWithError() + serviceChanges.removeAll() + } + + func testServerPathChanged() throws { + let lhs = ServiceInformation(version: .default, http: HTTPInformation(hostname: "www.test.com")) + let rhs = ServiceInformation(version: .default, http: HTTPInformation(hostname: "www.updated.com")) + + let comparator = ServiceInformationComparator(lhs: lhs, rhs: rhs) + comparator.compare(comparisonContext, &serviceChanges) + + XCTAssertEqual(serviceChanges.count, 1) + let change = try XCTUnwrap(serviceChanges.first) + XCTAssertEqual(change.breaking, true) + XCTAssertEqual(change.solvable, true) + XCTAssertEqual(change.type, .update) + XCTAssertEqual(change.id, lhs.deltaIdentifier) + let updateChange = try XCTUnwrap(change.modeledUpdateChange) + if case let .http(from, to) = updateChange.updated { + XCTAssertEqual(from, lhs.http) + XCTAssertEqual(to, rhs.http) + } else { + XCTFail("Unexpected ServiceInformationUpdateChange: \(updateChange.updated)") + } + } + + func testVersionChanged() throws { + let updatedVersion = Version(prefix: "api", major: 2, minor: 0, patch: 0) + let http = HTTPInformation(hostname: "test.com") + + let lhs = ServiceInformation(version: .default, http: http) + let rhs = ServiceInformation(version: updatedVersion, http: http) + + let comparator = ServiceInformationComparator(lhs: lhs, rhs: rhs) + comparator.compare(comparisonContext, &serviceChanges) + + XCTAssert(serviceChanges.count == 1) + let change = try XCTUnwrap(serviceChanges.first) + XCTAssertEqual(change.id, lhs.deltaIdentifier) + XCTAssertEqual(change.type, .update) + XCTAssertEqual(change.breaking, false) + XCTAssertEqual(change.solvable, true) + + let updateChange = try XCTUnwrap(change.modeledUpdateChange) + if case let .version(from, to) = updateChange.updated { + XCTAssertEqual(from, .default) + XCTAssertEqual(to, updatedVersion) + } else { + XCTFail("Unexpected ServiceInformationUpdateChange: \(updateChange.updated)") + } + } + + func testEncoderConfigurationChanged() throws { + let updatedConfiguration = EncoderConfiguration(dateEncodingStrategy: .millisecondsSince1970, dataEncodingStrategy: .base64) + + let lhsExporter = RESTExporterConfiguration(encoderConfiguration: .default, decoderConfiguration: .default) + let rhsExporter = RESTExporterConfiguration(encoderConfiguration: updatedConfiguration, decoderConfiguration: .default) + + let http = HTTPInformation(hostname: "localhost") + let lhs = ServiceInformation(version: .default, http: http, exporters: lhsExporter) + let rhs = ServiceInformation(version: .default, http: http, exporters: rhsExporter) + + + let comparator = ServiceInformationComparator(lhs: lhs, rhs: rhs) + comparator.compare(comparisonContext, &serviceChanges) + + XCTAssertEqual(serviceChanges.count, 1) + let change = try XCTUnwrap(serviceChanges.first) + XCTAssertEqual(change.id, lhs.deltaIdentifier) + XCTAssertEqual(change.type, .update) + XCTAssertEqual(change.breaking, true) + XCTAssertEqual(change.solvable, true) + let updateChange = try XCTUnwrap(change.modeledUpdateChange) + + guard case let .exporter(exporter) = updateChange.updated else { + XCTFail("Unexpected ServiceInformationUpdateChange: \(updateChange.updated)") + return + } + + XCTAssertEqual(exporter.type, .update) + XCTAssertEqual(exporter.breaking, change.breaking) + XCTAssertEqual(exporter.solvable, change.solvable) + XCTAssertEqual(exporter.id, DeltaIdentifier(ApodiniExporterType.rest.rawValue)) + + let updatedExporter = try XCTUnwrap(exporter.modeledUpdateChange) + XCTAssertEqual(updatedExporter.updated.from.typed(of: RESTExporterConfiguration.self), lhsExporter) + XCTAssertEqual(updatedExporter.updated.to.typed(of: RESTExporterConfiguration.self), rhsExporter) + } + + func testDecoderConfigurationChanged() throws { + let updatedConfiguration = DecoderConfiguration(dateDecodingStrategy: .secondsSince1970, dataDecodingStrategy: .base64) + + let lhsExporter = RESTExporterConfiguration(encoderConfiguration: .default, decoderConfiguration: .default) + let rhsExporter = RESTExporterConfiguration(encoderConfiguration: .default, decoderConfiguration: updatedConfiguration) + + let http = HTTPInformation(hostname: "localhost") + let lhs = ServiceInformation(version: .default, http: http, exporters: lhsExporter) + let rhs = ServiceInformation(version: .default, http: http, exporters: rhsExporter) + + let comparator = ServiceInformationComparator(lhs: lhs, rhs: rhs) + comparator.compare(comparisonContext, &serviceChanges) + + XCTAssertEqual(serviceChanges.count, 1) + let change = try XCTUnwrap(serviceChanges.first) + XCTAssertEqual(change.breaking, true) + XCTAssertEqual(change.solvable, true) + XCTAssertEqual(change.type, .update) + XCTAssertEqual(change.id, lhs.deltaIdentifier) + let updateChange = try XCTUnwrap(change.modeledUpdateChange) + + guard case let .exporter(exporter) = updateChange.updated else { + XCTFail("Unexpected ServiceInformationUpdateChange: \(updateChange.updated)") + return + } + + XCTAssertEqual(exporter.type, .update) + XCTAssertEqual(exporter.breaking, change.breaking) + XCTAssertEqual(exporter.solvable, change.solvable) + XCTAssertEqual(exporter.id, DeltaIdentifier(ApodiniExporterType.rest.rawValue)) + + let updatedExporter = try XCTUnwrap(exporter.modeledUpdateChange) + XCTAssertEqual(updatedExporter.updated.from.typed(of: RESTExporterConfiguration.self), lhsExporter) + XCTAssertEqual(updatedExporter.updated.to.typed(of: RESTExporterConfiguration.self), rhsExporter) + } +} diff --git a/Tests/ApodiniMigratorTests/ApodiniMigratorCore/ApodiniMigratorModelsTests.swift b/Tests/ApodiniMigratorTests/ApodiniMigratorCore/ApodiniMigratorModelsTests.swift index ced0037c..9f9bcedd 100644 --- a/Tests/ApodiniMigratorTests/ApodiniMigratorCore/ApodiniMigratorModelsTests.swift +++ b/Tests/ApodiniMigratorTests/ApodiniMigratorCore/ApodiniMigratorModelsTests.swift @@ -9,6 +9,7 @@ import XCTest @testable import ApodiniMigratorCore @testable import ApodiniMigratorClientSupport +@testable import RESTMigrator final class ApodiniMigratorModelsTests: ApodiniMigratorXCTestCase { func testDSLEndpointIdentifier() { @@ -18,6 +19,7 @@ final class ApodiniMigratorModelsTests: ApodiniMigratorXCTestCase { handlerName: "SomeHandler", deltaIdentifier: "0.1.0.0.1", operation: .create, + communicationalPattern: .requestResponse, absolutePath: "/v1/test", parameters: [], response: .scalar(.bool), @@ -28,30 +30,32 @@ final class ApodiniMigratorModelsTests: ApodiniMigratorXCTestCase { handlerName: "SomeHandler", deltaIdentifier: "getSomeHandler", operation: .read, + communicationalPattern: .requestResponse, absolutePath: "/v1/test", parameters: [], response: .scalar(.bool), errors: errors ) - XCTAssert(noIDEndpoint.deltaIdentifier == .init(noIDEndpoint.handlerName.lowerFirst)) + XCTAssert(noIDEndpoint.deltaIdentifier == .init(noIDEndpoint.handlerName.buildName())) XCTAssert(withIDEndpoint.deltaIdentifier == "getSomeHandler") } - + func testWrappedContentParameters() throws { let param1 = Parameter(name: "first", typeInformation: .scalar(.string), parameterType: .content, isRequired: true) let param2 = Parameter(name: "second", typeInformation: .scalar(.int), parameterType: .content, isRequired: false) - + let endpoint = Endpoint( handlerName: "someHandler", deltaIdentifier: "id", operation: .create, + communicationalPattern: .requestResponse, absolutePath: "/v1/test", parameters: [param1, param2], response: .scalar(.bool), errors: [] ) - + XCTAssert(endpoint.parameters.count == 1) let contentParameter = try XCTUnwrap(endpoint.parameters.first) XCTAssert(contentParameter.isWrapped) @@ -70,9 +74,9 @@ final class ApodiniMigratorModelsTests: ApodiniMigratorXCTestCase { let string = "/v1/{some}/users/{id}" let string1 = "/v1/{param}/users/{param}" let string2 = "/v2/{param}/users/{param}" // still considered equal, change is delegated to networking due to version change - - XCTAssert(EndpointPath(string) != EndpointPath(string1)) - XCTAssert(EndpointPath(string1) == EndpointPath(string2)) + + XCTAssertNotEqual(EndpointPath(string), EndpointPath(string1)) + XCTAssertEqual(EndpointPath(string1), EndpointPath(string2)) } func testVersion() throws { @@ -86,26 +90,30 @@ final class ApodiniMigratorModelsTests: ApodiniMigratorXCTestCase { } func testDocument() throws { - var document = Document() + let serviceInformation = ServiceInformation( + version: Version(prefix: "test", major: 1, minor: 2, patch: 3), + http: HTTPInformation(hostname: "127.0.0.1", port: 8080), + exporters: RESTExporterConfiguration(encoderConfiguration: .default, decoderConfiguration: .default) + ) + + var document = APIDocument(serviceInformation: serviceInformation) document.add( endpoint: .init( handlerName: "someHandler", deltaIdentifier: "endpoint", operation: .create, + communicationalPattern: .requestResponse, absolutePath: "/test1/test", parameters: [], response: .scalar(.bool), errors: [] ) ) - document.setServerPath("http://127.0.0.1:8080") - document.setVersion(Version(prefix: "test", major: 1, minor: 2, patch: 3)) - - document.setCoderConfigurations(.default, .default) - XCTAssert(document.fileName == "api_test1.2.3") - XCTAssert(!document.endpoints.isEmpty) - XCTAssertEqual(document.metaData.versionedServerPath, "http://127.0.0.1:8080/test1") - XCTAssert(!document.json.isEmpty) - XCTAssert(!document.yaml.isEmpty) + + XCTAssertEqual(document.fileName, "api_test1.2.3") + XCTAssertEqual(document.endpoints.isEmpty, false) + XCTAssertEqual(document.serviceInformation.http.urlFormatted, "http://127.0.0.1:8080") + XCTAssertEqual(document.json.isEmpty, false) + XCTAssertEqual(document.yaml.isEmpty, false) } } diff --git a/Tests/ApodiniMigratorTests/ApodiniMigratorCore/TypeInformationTests.swift b/Tests/ApodiniMigratorTests/ApodiniMigratorCore/TypeInformationTests.swift index f88f53c4..d386271f 100644 --- a/Tests/ApodiniMigratorTests/ApodiniMigratorCore/TypeInformationTests.swift +++ b/Tests/ApodiniMigratorTests/ApodiniMigratorCore/TypeInformationTests.swift @@ -17,6 +17,6 @@ final class TypeInformationTests: ApodiniMigratorXCTestCase { let instance = XCTAssertNoThrowWithResult(try TestTypes.Student.decode(from: json)) XCTAssert(instance.grades.isEmpty) XCTAssert(instance.age == 0) - XCTAssert(instance.name == "") + XCTAssert(instance.name.isEmpty) } } diff --git a/Tests/ApodiniMigratorTests/ApodiniMigratorGeneratorTests/AuxiliaryFileGeneratorTests.swift b/Tests/ApodiniMigratorTests/ApodiniMigratorGeneratorTests/AuxiliaryFileGeneratorTests.swift index 8e12cf52..43082a77 100644 --- a/Tests/ApodiniMigratorTests/ApodiniMigratorGeneratorTests/AuxiliaryFileGeneratorTests.swift +++ b/Tests/ApodiniMigratorTests/ApodiniMigratorGeneratorTests/AuxiliaryFileGeneratorTests.swift @@ -7,12 +7,12 @@ // import XCTest +@testable import RESTMigrator @testable import ApodiniMigrator @testable import ApodiniMigratorCompare import PathKit final class AuxiliaryFileGeneratorTests: ApodiniMigratorXCTestCase { - override class func setUp() { super.setUp() @@ -21,7 +21,7 @@ final class AuxiliaryFileGeneratorTests: ApodiniMigratorXCTestCase { func testTestFile() throws { let object: TypeInformation = .object( - name: .init(name: "TestObject"), + name: .init(rawValue: "TestObject"), properties: [ .init(name: "prop1", type: .scalar(.bool)), .init(name: "prop2", type: .scalar(.uint)), @@ -33,15 +33,15 @@ final class AuxiliaryFileGeneratorTests: ApodiniMigratorXCTestCase { ) let enumeration: TypeInformation = .enum( - name: .init(name: "TestEnumeration"), + name: .init(rawValue: "TestEnumeration"), rawValueType: .scalar(.string), cases: [ .init("first"), .init("second") ] ) - - let testFile = TestFileTemplate([object, enumeration], fileName: "TestFile" + .swift, packageName: "ApodiniMigrator") + + let testFile = ModelTestsFile(name: "TestFile", models: [object, enumeration]) XCTMigratorAssertEqual(testFile, .modelsTestFile) } @@ -51,12 +51,15 @@ final class AuxiliaryFileGeneratorTests: ApodiniMigratorXCTestCase { handlerName: "TestHandler", deltaIdentifier: "sayHelloWorld", operation: .read, + communicationalPattern: .requestResponse, absolutePath: "/v1/hello", parameters: [], response: .scalar(.string), errors: [.init(code: 404, message: "Could not say hello")] ) - let file = APIFile([.init(endpoint: endpoint, unavailable: false, parameters: [], path: endpoint.path)]) + + let migratedEndpoints = [MigratedEndpoint(endpoint: endpoint, unavailable: false, parameters: [], path: endpoint.identifier())] + let file = APIFile(SharedNodeReference(with: migratedEndpoints)) XCTMigratorAssertEqual(file, .aPIFile) } diff --git a/Tests/ApodiniMigratorTests/ApodiniMigratorGeneratorTests/EndpointMigratorTests.swift b/Tests/ApodiniMigratorTests/ApodiniMigratorGeneratorTests/EndpointMigratorTests.swift index daaadb2a..73f4138e 100644 --- a/Tests/ApodiniMigratorTests/ApodiniMigratorGeneratorTests/EndpointMigratorTests.swift +++ b/Tests/ApodiniMigratorTests/ApodiniMigratorGeneratorTests/EndpointMigratorTests.swift @@ -7,154 +7,223 @@ // import XCTest +@testable import RESTMigrator @testable import ApodiniMigrator @testable import ApodiniMigratorCompare -import PathKit final class EndpointMigratorTests: ApodiniMigratorXCTestCase { private let endpoint = Endpoint( handlerName: "TestHandler", deltaIdentifier: "testEndpoint", operation: .read, + communicationalPattern: .requestResponse, absolutePath: "/v1/tests/{second}", parameters: [ .init(name: "isDriving", typeInformation: .scalar(.string), parameterType: .lightweight, isRequired: false), .init(name: "first", typeInformation: .scalar(.string), parameterType: .lightweight, isRequired: true), .init(name: "second", typeInformation: .scalar(.uuid), parameterType: .path, isRequired: true), + // swiftlint:disable:next force_try .init(name: "third", typeInformation: try! TypeInformation(type: TestTypes.Car.self), parameterType: .content, isRequired: true) ], response: .reference("TestResponse"), errors: [.init(code: 404, message: "Not found")] ) - private var pathChange: UpdateChange { - .init( - element: .endpoint(endpoint.deltaIdentifier, target: .resourcePath), - from: .element(endpoint.path), - to: .element(EndpointPath("/v1/updatedTests/{second}")), + private var pathChange: EndpointChange { + .update( + id: endpoint.deltaIdentifier, + updated: .identifier(identifier: .update( + id: .init(EndpointPath.identifierType), + updated: .init( + from: AnyEndpointIdentifier(from: endpoint.identifier(for: EndpointPath.self)), + to: AnyEndpointIdentifier(from: EndpointPath(rawValue: "/v1/updatedTests/{second}")) + ), + breaking: true, + solvable: true + )), breaking: true, solvable: true ) } - private var operationChange: UpdateChange { - .init( - element: .endpoint(endpoint.deltaIdentifier, target: .operation), - from: .element(endpoint.operation), - to: .element(ApodiniMigratorCore.Operation.create), + private var operationChange: EndpointChange { + .update( + id: endpoint.deltaIdentifier, + updated: .identifier(identifier: .update( + id: .init(Operation.identifierType), + updated: .init( + from: AnyEndpointIdentifier(from: endpoint.identifier(for: ApodiniMigratorCore.Operation.self)), + to: AnyEndpointIdentifier(from: ApodiniMigratorCore.Operation.create) + ), + breaking: true, + solvable: true + )), breaking: true, solvable: true ) } - private var addParameterChange: AddChange { - .init( - element: .endpoint(endpoint.deltaIdentifier, target: .queryParameter), - added: .element(Parameter(name: "newParameter", typeInformation: .scalar(.bool), parameterType: .lightweight, isRequired: true)), - defaultValue: .json(123), + private var addParameterChange: EndpointChange { + .update( + id: endpoint.deltaIdentifier, + updated: .parameter(parameter: .addition( + id: "newParameter", + added: Parameter(name: "newParameter", typeInformation: .scalar(.bool), parameterType: .lightweight, isRequired: true), + defaultValue: 123, + breaking: true, + solvable: true + )), breaking: true, solvable: true ) } - private var deleteParameterChange: DeleteChange { - .init( - element: .endpoint(endpoint.deltaIdentifier, target: .queryParameter), - deleted: .elementID("first"), - fallbackValue: .none, + private var deleteParameterChange: EndpointChange { + .update( + id: endpoint.deltaIdentifier, + updated: .parameter(parameter: .removal( + id: "first", + fallbackValue: nil, + breaking: false, + solvable: true + )), breaking: false, solvable: true ) } - private var deleteContentParameterChange: DeleteChange { - .init( - element: .endpoint(endpoint.deltaIdentifier, target: .queryParameter), - deleted: .elementID("third"), - fallbackValue: .none, + private var deleteContentParameterChange: EndpointChange { + .update( + id: endpoint.deltaIdentifier, + updated: .parameter(parameter: .removal( + id: "third", + fallbackValue: nil, + breaking: false, + solvable: true + )), breaking: false, solvable: true ) } - private var renamedParameterChange: UpdateChange { - .init( - element: .endpoint(endpoint.deltaIdentifier, target: .queryParameter), - from: "first", - to: "someNewParameterName", - similarity: 0, + private var renamedParameterChange: EndpointChange { + .update( + id: endpoint.deltaIdentifier, + updated: .parameter(parameter: .idChange( + from: "first", + to: "someNewParameterName", + similarity: 0, + breaking: true, + solvable: true + )), breaking: true, solvable: true ) } - private var pathAndParameterKindChanges: [Change] { + private var pathAndParameterKindChanges: [EndpointChange] { [ - UpdateChange( - element: .endpoint(endpoint.deltaIdentifier, target: .queryParameter), - from: .element(ParameterType.lightweight), - to: .element(ParameterType.path), - targetID: "first", - parameterTarget: .kind, + .update( + id: endpoint.deltaIdentifier, + updated: .parameter(parameter: .update( + id: "first", + updated: .parameterType(from: .lightweight, to: .path), + breaking: true, + solvable: true + )), breaking: true, solvable: true ), - UpdateChange( - element: .endpoint(endpoint.deltaIdentifier, target: .resourcePath), - from: .element(endpoint.path), - to: .element(EndpointPath("/v1/tests/{second}/{first}")), + .update( + id: endpoint.deltaIdentifier, + updated: .identifier(identifier: .update( + id: .init(EndpointPath.identifierType), + updated: .init( + from: AnyEndpointIdentifier(from: endpoint.identifier(for: EndpointPath.self)), + to: AnyEndpointIdentifier(from: EndpointPath(rawValue: "/v1/tests/{second}/{first}")) + ), + breaking: true, + solvable: true + )), breaking: true, solvable: true ) ] } - private var parameterTypeChange: UpdateChange { - UpdateChange( - element: .endpoint(endpoint.deltaIdentifier, target: .queryParameter), - from: .element(TypeInformation.scalar(.string)), - to: .element(TypeInformation.scalar(.bool)), - targetID: "first", - convertFromTo: 1, - convertionWarning: nil, - parameterTarget: .typeInformation, + private var parameterTypeChange: EndpointChange { + .update( + id: endpoint.deltaIdentifier, + updated: .parameter(parameter: .update( + id: "first", + updated: .type( + from: .scalar(.string), + to: .scalar(.bool), + forwardMigration: 1, + conversionWarning: nil + ), + breaking: true, + solvable: true + )), breaking: true, solvable: true ) } - private var parameterNecessityToRequiredChange: UpdateChange { - .init( - element: .endpoint(endpoint.deltaIdentifier, target: .queryParameter), - from: .element(Necessity.optional), - to: .element(Necessity.required), - targetID: "isDriving", - necessityValue: .json(1), - parameterTarget: .necessity, + private var parameterNecessityToRequiredChange: EndpointChange { + .update( + id: endpoint.deltaIdentifier, + updated: .parameter(parameter: .update( + id: "isDriving", + updated: .necessity( + from: .optional, + to: .required, + necessityMigration: 1 + ), + breaking: true, + solvable: true + )), breaking: true, solvable: true ) } - private var parameterNecessityToOptionalChange: UpdateChange { - .init( - element: .endpoint(endpoint.deltaIdentifier, target: .queryParameter), - from: .element(Necessity.required), - to: .element(Necessity.optional), - targetID: "name", - parameterTarget: .necessity, + private var parameterNecessityToOptionalChange: EndpointChange { + .update( + id: endpoint.deltaIdentifier, + updated: .parameter(parameter: .update( + id: "name", + updated: .necessity( + from: .required, + to: .optional, + necessityMigration: nil + ), + breaking: true, + solvable: true + )), breaking: true, solvable: true ) } - private var responseChange: UpdateChange { - .init( - element: .endpoint(endpoint.deltaIdentifier, target: .queryParameter), - from: .element(endpoint.response), - to: .element(TypeInformation.reference("UpdatedTestResponse")), - convertToFrom: 1, - convertionWarning: nil, + private var responseChange: EndpointChange { + .update( + id: endpoint.deltaIdentifier, + updated: .response( + from: endpoint.response, + to: .reference("UpdatedTestResponse"), + backwardsMigration: 1, + migrationWarning: nil + ), + breaking: true, + solvable: true + ) + } + + private var endpointRemovalChange: EndpointChange { + .removal( + id: endpoint.deltaIdentifier, + fallbackValue: nil, breaking: true, solvable: true ) @@ -166,13 +235,18 @@ final class EndpointMigratorTests: ApodiniMigratorXCTestCase { FileHeaderComment.testsDate = .testsDate } - private func endpointFile(changes: [Change]) -> EndpointFile { - .init(typeInformation: endpoint.response, endpoints: [endpoint], changes: changes) + private func endpointFile(changes: [EndpointChange]) -> EndpointFile { + EndpointFile( + migratedEndpointsReference: SharedNodeReference(with: []), + typeInformation: endpoint.response, + endpoints: [endpoint], + changes: changes + ) } func testDefaultEndpointFile() throws { let file = endpointFile(changes: []) - + XCTMigratorAssertEqual(file, .defaultEndpointFile) } @@ -238,9 +312,8 @@ final class EndpointMigratorTests: ApodiniMigratorXCTestCase { func testEndpointResponseChange() throws { let file = endpointFile(changes: [responseChange]) - let expected = OutputFiles.endpointResponseChange.content().indentationFormatted() - XCTAssertEqual(file.indentationFormatted().sanitizedLines(), expected.sanitizedLines()) + XCTMigratorAssertEqual(file, .endpointResponseChange) } func testEndpointMultipleChanges() throws { @@ -257,16 +330,33 @@ final class EndpointMigratorTests: ApodiniMigratorXCTestCase { } func testEndpointDeletedChange() throws { - let deletedSelfChange = DeleteChange( - element: .endpoint(endpoint.deltaIdentifier, target: .`self`), - deleted: .elementID(endpoint.deltaIdentifier), - fallbackValue: .none, - breaking: true, - solvable: true - ) - - let file = endpointFile(changes: [deletedSelfChange]) + let file = endpointFile(changes: [endpointRemovalChange]) XCTMigratorAssertEqual(file, .endpointDeletedChange) } + + func testWrappedContentParameter() throws { + let param1 = Parameter(name: "first", typeInformation: .scalar(.string), parameterType: .content, isRequired: true) + let param2 = Parameter(name: "second", typeInformation: .scalar(.int), parameterType: .content, isRequired: false) + + let endpoint = Endpoint( + handlerName: "someHandler", + deltaIdentifier: "id", + operation: .create, + communicationalPattern: .requestResponse, + absolutePath: "/v1/test", + parameters: [param1, param2], + response: .scalar(.bool), + errors: [] + ) + + let file = EndpointFile( + migratedEndpointsReference: SharedNodeReference(with: []), + typeInformation: endpoint.response, + endpoints: [endpoint], + changes: [] + ) + + XCTMigratorAssertEqual(file, .endpointWrappedContentParameter) + } } diff --git a/Tests/ApodiniMigratorTests/ApodiniMigratorGeneratorTests/EnumMigratorTests.swift b/Tests/ApodiniMigratorTests/ApodiniMigratorGeneratorTests/EnumMigratorTests.swift index 368c89da..69bf4757 100644 --- a/Tests/ApodiniMigratorTests/ApodiniMigratorGeneratorTests/EnumMigratorTests.swift +++ b/Tests/ApodiniMigratorTests/ApodiniMigratorGeneratorTests/EnumMigratorTests.swift @@ -7,13 +7,14 @@ // import XCTest +@testable import RESTMigrator @testable import ApodiniMigrator @testable import ApodiniMigratorCompare import PathKit final class EnumMigratorTests: ApodiniMigratorXCTestCase { let enumeration: TypeInformation = .enum( - name: .init(name: "ProgLang"), + name: .init(rawValue: "ProgLang"), rawValueType: .scalar(.string), cases: [ .init("swift"), @@ -26,35 +27,79 @@ final class EnumMigratorTests: ApodiniMigratorXCTestCase { ] ) - private var addCaseChange: AddChange { - .init( - element: .enum(enumeration.deltaIdentifier, target: .case), - added: .element(EnumCase("go")), - defaultValue: .none, + private var addCaseChange: ModelChange { + .update( + id: enumeration.deltaIdentifier, + updated: .case(case: .addition( + id: "go", + added: EnumCase("go"), + breaking: false, + solvable: true + )), breaking: false, solvable: true ) } - var deleteCaseChange: DeleteChange { - .init( - element: .enum(enumeration.deltaIdentifier, target: .case), - deleted: .elementID("other"), - fallbackValue: .none, + var deleteCaseChange: ModelChange { + .update( + id: enumeration.deltaIdentifier, + updated: .case(case: .removal( + id: "other", + breaking: true, + solvable: true + )), breaking: true, solvable: true ) } - var renameCaseChange: UpdateChange { - .init( - element: .enum(enumeration.deltaIdentifier, target: .caseRawValue), - from: .element(EnumCase("swift")), - to: .element(EnumCase("swiftLang")), + var renameCaseChange: ModelChange { + .update( + id: enumeration.deltaIdentifier, + updated: .case(case: .idChange( + from: "swift", + to: "swiftLang", + similarity: nil, + breaking: true, + solvable: true + )), breaking: true, solvable: true ) } + + var updateRawValueChange: ModelChange { + .update( + id: enumeration.deltaIdentifier, + updated: .case(case: .update( + id: "swift", + updated: .rawValue(from: "swift", to: "swiftLang"), + breaking: true, + solvable: true + )), + breaking: true, + solvable: true + ) + } + + var deleteEnumChange: ModelChange { + .removal( + id: enumeration.deltaIdentifier, + removed: nil, + breaking: true, + solvable: true + ) + } + + var unsupportedRawValueChange: ModelChange { + .update( + id: enumeration.deltaIdentifier, + updated: .rawValueType(from: .scalar(.string), to: .scalar(.int)), + breaking: true, + solvable: false + ) + } override class func setUp() { super.setUp() @@ -73,48 +118,39 @@ final class EnumMigratorTests: ApodiniMigratorXCTestCase { } func testEnumAddedCase() throws { - let migrator = EnumMigrator(enum: enumeration, changes: [addCaseChange]) + let migrator = EnumMigrator(enumeration, changes: [addCaseChange]) XCTMigratorAssertEqual(migrator, .enumAddedCase) } func testEnumDeletedCase() throws { - let migrator = EnumMigrator(enum: enumeration, changes: [deleteCaseChange]) + let migrator = EnumMigrator(enumeration, changes: [deleteCaseChange]) XCTMigratorAssertEqual(migrator, .enumDeletedCase) } + + func testCaseRename() throws { + let migrator = EnumMigrator(enumeration, changes: [renameCaseChange]) + XCTMigratorAssertEqual(migrator, .defaultStringEnum) + } - func testEnumRenamedCase() throws { - - let migrator = EnumMigrator(enum: enumeration, changes: [renameCaseChange]) - XCTMigratorAssertEqual(migrator, .enumRenamedCase) + func testRawValueChange() throws { + let migrator = EnumMigrator(enumeration, changes: [updateRawValueChange]) + XCTMigratorAssertEqual(migrator, .enumRawValue) } func testEnumDeleted() throws { - let deletedSelfChange = DeleteChange( - element: .enum(enumeration.deltaIdentifier, target: .`self`), - deleted: .elementID(enumeration.deltaIdentifier), - fallbackValue: .none, - breaking: true, - solvable: true - ) - - let migrator = EnumMigrator(enum: enumeration, changes: [deletedSelfChange]) + let migrator = EnumMigrator(enumeration, changes: [deleteEnumChange]) XCTMigratorAssertEqual(migrator, .enumDeletedSelf) } func testEnumUnsupportedChange() throws { - let unsupportedChange = UnsupportedChange( - element: .enum(enumeration.deltaIdentifier, target: .`self`), - description: "Unsupported change! Raw value type changed" - ) - - let migrator = EnumMigrator(enum: enumeration, changes: [unsupportedChange]) + let migrator = EnumMigrator(enumeration, changes: [unsupportedRawValueChange]) XCTMigratorAssertEqual(migrator, .enumUnsupportedChange) } func testEnumMultipleChanges() throws { - let migrator = EnumMigrator(enum: enumeration, changes: [addCaseChange, deleteCaseChange, renameCaseChange]) + let migrator = EnumMigrator(enumeration, changes: [addCaseChange, deleteCaseChange, renameCaseChange, updateRawValueChange]) XCTMigratorAssertEqual(migrator, .enumMultipleChanges) } } diff --git a/Tests/ApodiniMigratorTests/ApodiniMigratorGeneratorTests/ObjectMigratorTests.swift b/Tests/ApodiniMigratorTests/ApodiniMigratorGeneratorTests/ObjectMigratorTests.swift index bebac1ef..a0418407 100644 --- a/Tests/ApodiniMigratorTests/ApodiniMigratorGeneratorTests/ObjectMigratorTests.swift +++ b/Tests/ApodiniMigratorTests/ApodiniMigratorGeneratorTests/ObjectMigratorTests.swift @@ -7,13 +7,13 @@ // import XCTest +@testable import RESTMigrator @testable import ApodiniMigrator @testable import ApodiniMigratorCompare -import PathKit final class ObjectMigratorTests: ApodiniMigratorXCTestCase { private let user: TypeInformation = .object( - name: .init(name: "User"), + name: .init(rawValue: "User"), properties: [ .init(name: "id", type: .scalar(.uuid)), .init(name: "name", type: .scalar(.string)), @@ -24,72 +24,122 @@ final class ObjectMigratorTests: ApodiniMigratorXCTestCase { ] ) - private var addPropertyChange: AddChange { - .init( - element: .object(user.deltaIdentifier, target: .property), - added: .element(TypeProperty(name: "username", type: .scalar(.string))), - defaultValue: .json(1), + private var addPropertyChange: ModelChange { + .update( + id: user.deltaIdentifier, + updated: .property(property: .addition( + id: "username", + added: TypeProperty(name: "username", type: .scalar(.string)), + defaultValue: 1, + breaking: true, + solvable: true + )), breaking: true, solvable: true ) } - private var deletePropertyChange: DeleteChange { - .init( - element: .object(user.deltaIdentifier, target: .property), - deleted: .elementID("friends"), - fallbackValue: .json(2), + private var deletePropertyChange: ModelChange { + .update( + id: user.deltaIdentifier, + updated: .property(property: .removal( + id: "friends", + fallbackValue: 2, + breaking: true, + solvable: true + )), breaking: true, solvable: true ) } - private var renamedPropertyChange: UpdateChange { - .init( - element: .object(user.deltaIdentifier, target: .property), - from: "githubProfile", - to: "githubURL", - similarity: 0, + private var renamedPropertyChange: ModelChange { + .update( + id: user.deltaIdentifier, + updated: .property(property: .idChange( + from: "githubProfile", + to: "githubURL", + similarity: 0, + breaking: true, + solvable: true + )), breaking: true, solvable: true ) } - private var propertyNecessityToRequiredChange: UpdateChange { - UpdateChange( - element: .object(user.deltaIdentifier, target: .necessity), - from: .element(Necessity.optional), - to: .element(Necessity.required), - necessityValue: .json(3), - targetID: "age", + private var propertyNecessityToRequiredChange: ModelChange { + .update( + id: user.deltaIdentifier, + updated: .property(property: .update( + id: "age", + updated: .necessity(from: .optional, to: .required, necessityMigration: 3), + breaking: true, + solvable: true + )), breaking: true, solvable: true ) } - private var propertyNecessityToOptionalChange: UpdateChange { - UpdateChange( - element: .object(user.deltaIdentifier, target: .necessity), - from: .element(Necessity.required), - to: .element(Necessity.optional), - necessityValue: .json(4), - targetID: "name", + private var propertyNecessityToOptionalChange: ModelChange { + .update( + id: user.deltaIdentifier, + updated: .property(property: .update( + id: "name", + updated: .necessity(from: .required, to: .optional, necessityMigration: 4), + breaking: true, + solvable: true + )), breaking: true, solvable: true ) } - private var propertyTypeChange: UpdateChange { - .init( - element: .object(user.deltaIdentifier, target: .property), - from: .element(TypeInformation.scalar(.string)), - to: .element(TypeInformation.scalar(.bool)), - targetID: "isStudent", - convertFromTo: 1, - convertToFrom: 2, - convertionWarning: nil, + private var propertyTypeChange: ModelChange { + .update( + id: user.deltaIdentifier, + updated: .property(property: .update( + id: "isStudent", + updated: .type( + from: .scalar(.string), + to: .scalar(.bool), + forwardMigration: 1, + backwardMigration: 2, + conversionWarning: nil + ), + breaking: true, + solvable: true + )), breaking: true, - solvable: true) + solvable: true + ) + } + + private var objectRemovalChange: ModelChange { + .removal( + id: user.deltaIdentifier, + fallbackValue: nil, + breaking: true, + solvable: true + ) + } + + private var objectUnsupportedRootTypeChange: ModelChange { + .update( + id: user.deltaIdentifier, + updated: .rootType( + from: .object, + to: .enum, + newModel: .enum( + name: .init(rawValue: user.deltaIdentifier.rawValue), + rawValueType: .scalar(.string), + cases: [EnumCase("ok")] + ) + ), + breaking: true, + solvable: false + ) } override class func setUp() { @@ -149,25 +199,12 @@ final class ObjectMigratorTests: ApodiniMigratorXCTestCase { } func testObjectDeleted() throws { - let deletedSelfChange = DeleteChange( - element: .object(user.deltaIdentifier, target: .`self`), - deleted: .elementID(user.deltaIdentifier), - fallbackValue: .none, - breaking: true, - solvable: true - ) - - let migrator = ObjectMigrator(user, changes: [deletedSelfChange]) + let migrator = ObjectMigrator(user, changes: [objectRemovalChange]) XCTMigratorAssertEqual(migrator, .objectDeletedChange) } func testObjectUnsupportedChange() throws { - let unsupportedChange = UnsupportedChange( - element: .object(user.deltaIdentifier, target: .`self`), - description: "Unsupported change! Type changed to enum" - ) - - let migrator = ObjectMigrator(user, changes: [unsupportedChange]) + let migrator = ObjectMigrator(user, changes: [objectUnsupportedRootTypeChange]) XCTMigratorAssertEqual(migrator, .objectUnsupportedChange) } } diff --git a/Tests/ApodiniMigratorTests/ApodiniMigratorShared/ApodiniMigratorSharedTests.swift b/Tests/ApodiniMigratorTests/ApodiniMigratorShared/ApodiniMigratorSharedTests.swift index 477c23ea..2de237b3 100644 --- a/Tests/ApodiniMigratorTests/ApodiniMigratorShared/ApodiniMigratorSharedTests.swift +++ b/Tests/ApodiniMigratorTests/ApodiniMigratorShared/ApodiniMigratorSharedTests.swift @@ -12,15 +12,15 @@ import XCTest final class ApodiniMigratorSharedTests: ApodiniMigratorXCTestCase { func testYAMLandJSON() throws { - for format in [OutputFormat.yaml, .json] { - let document: Document = XCTAssertNoThrowWithResult(try Documents.v1.instance()) - let path = XCTAssertNoThrowWithResult(try document.write(at: testDirectory, outputFormat: format)) - XCTAssertThrows(try Document.decode(from: path.asPath + "invalid")) - let stringContent = XCTAssertNoThrowWithResult(try path.asPath.read() as String) - let documentFromPath = XCTAssertNoThrowWithResult(try Document.decode(from: path.asPath)) - XCTAssertEqual(document, documentFromPath) - XCTAssert(format.string(of: document).isNotEmpty) - XCTAssert(stringContent.isNotEmpty) + for format in [OutputFormat.json, .yaml] { + let document: APIDocument = XCTAssertNoThrowWithResult(try Documents.v1.decodedContent()) + let path = Path(XCTAssertNoThrowWithResult(try document.write(at: testDirectory, outputFormat: format))) + XCTAssertThrows(try APIDocument.decode(from: path + "invalid")) + let stringContent = XCTAssertNoThrowWithResult(try path.read() as String) + let documentFromPath = XCTAssertNoThrowWithResult(try APIDocument.decode(from: path)) + XCTAssert(document == documentFromPath) + XCTAssertEqual(format.string(of: document).isEmpty, false) + XCTAssertEqual(stringContent.isEmpty, false) } } @@ -138,7 +138,7 @@ final class ApodiniMigratorSharedTests: ApodiniMigratorXCTestCase { let empty = "" XCTAssert(empty.lowerFirst == empty) XCTAssert(empty.upperFirst == empty) - XCTAssert(empty.lines().first == empty) + XCTAssert(empty.components(separatedBy: "\n").first == empty) let getEventsHandler = "GetEventsHandler" let events = "events" @@ -152,7 +152,7 @@ final class ApodiniMigratorSharedTests: ApodiniMigratorXCTestCase { func testArray() { var numbers = [1, 2, 3, 4, 5, 6, 6, 6, 6, 7] let replaced = numbers.replacingOccurrences(ofElement: 6, with: 9) - XCTAssert(replaced.filter { $0 == 6 }.isEmpty ) + XCTAssert(!replaced.contains { $0 == 6 }) numbers.replacingOccurrences(of: 6, with: 9) XCTAssert(numbers == replaced) } diff --git a/Tests/ApodiniMigratorTests/ClientLibraryGenerationMigrationTests.swift b/Tests/ApodiniMigratorTests/ClientLibraryGenerationMigrationTests.swift index 84d8eee2..8cecbae8 100644 --- a/Tests/ApodiniMigratorTests/ClientLibraryGenerationMigrationTests.swift +++ b/Tests/ApodiniMigratorTests/ClientLibraryGenerationMigrationTests.swift @@ -7,26 +7,26 @@ // import XCTest -@testable import ApodiniMigrator +@testable import RESTMigrator final class ClientLibraryGenerationMigrationTests: ApodiniMigratorXCTestCase { func testV1LibraryGeneration() throws { - let document = try Documents.v1.instance() as Document - let migrator = XCTAssertNoThrowWithResult(try Migrator( - packageName: "QONECTIQ", - packagePath: testDirectory, - documentPath: Documents.v1.path.string + let document = try Documents.v1.decodedContent() as APIDocument + let migrator = XCTAssertNoThrowWithResult(try RESTMigrator( + documentPath: Documents.v1.bundlePath.string )) - XCTAssertNoThrow(try migrator.run()) + XCTAssertNoThrow(try migrator.run(packageName: "QONECTIQ", packagePath: testDirectory)) let swiftFiles = try testDirectoryPath.recursiveSwiftFiles().map { $0.lastComponent } - let modelNames = document.allModels().map { $0.typeString + .swift } + let modelNames = document.models.map { $0.typeString + .swift } modelNames.forEach { XCTAssert(swiftFiles.contains($0)) } - let endpointFileNames = document.endpoints.map { $0.response.nestedTypeString + "+Endpoint" + .swift }.unique() + let endpointFileNames = document.endpoints + .map { $0.response.nestedTypeString + "+Endpoint" + .swift } + .unique() endpointFileNames.forEach { XCTAssert(swiftFiles.contains($0)) } @@ -36,22 +36,22 @@ final class ClientLibraryGenerationMigrationTests: ApodiniMigratorXCTestCase { } func testV2LibraryGeneration() throws { - let document = try Documents.v2.instance() as Document - let migrator = XCTAssertNoThrowWithResult(try Migrator( - packageName: "QONECTIQ", - packagePath: testDirectory, - documentPath: Documents.v2.path.string + let document = try Documents.v2.decodedContent() as APIDocument + let migrator = XCTAssertNoThrowWithResult(try RESTMigrator( + documentPath: Documents.v2.bundlePath.string )) - XCTAssertNoThrow(try migrator.run()) + XCTAssertNoThrow(try migrator.run(packageName: "QONECTIQ", packagePath: testDirectory)) let swiftFiles = try testDirectoryPath.recursiveSwiftFiles().map { $0.lastComponent } - let modelNames = document.allModels().map { $0.typeString + .swift } + let modelNames = document.models.map { $0.typeString + .swift } modelNames.forEach { XCTAssert(swiftFiles.contains($0)) } - let endpointFileNames = document.endpoints.map { $0.response.nestedTypeString + "+Endpoint" + .swift }.unique() + let endpointFileNames = document.endpoints + .map { $0.response.nestedTypeString + "+Endpoint" + .swift } + .unique() endpointFileNames.forEach { XCTAssert(swiftFiles.contains($0)) } @@ -61,37 +61,48 @@ final class ClientLibraryGenerationMigrationTests: ApodiniMigratorXCTestCase { } func testMigratorThrowIncompatibleMigrationGuide() throws { - let migrationGuide = try Documents.migrationGuide.instance() as MigrationGuide - XCTAssertThrows(try Migrator(packageName: "Test", packagePath: testDirectory, documentPath: Documents.v2.path.string, migrationGuide: migrationGuide)) + XCTAssertThrows(try RESTMigrator( + documentPath: Documents.v2.bundlePath.string, + migrationGuidePath: Documents.migrationGuide.bundlePath.string + )) } func testPackageMigration() throws { - let migrationGuide = try MigrationGuide.decode(from: try Documents.migrationGuide.data()) - let migrator = XCTAssertNoThrowWithResult(try Migrator( - packageName: "TestMigPackage", - packagePath: testDirectory, - documentPath: Documents.v1.path.string, - migrationGuide: migrationGuide + let migrator = XCTAssertNoThrowWithResult(try RESTMigrator( + documentPath: Documents.v1.bundlePath.string, + migrationGuidePath: Documents.migrationGuide.bundlePath.string )) - XCTAssertNoThrow(try migrator.run()) - XCTAssert(try testDirectoryPath.recursiveSwiftFiles().isNotEmpty) + XCTAssertNoThrow(try migrator.run(packageName: "TestMigPackage", packagePath: testDirectory)) + XCTAssertEqual(try testDirectoryPath.recursiveSwiftFiles().isEmpty, false) } func testMigrationGuideThrowing() throws { - XCTAssertThrows(try MigrationGuide.from(Path(#file), .init(.endpoints))) + XCTAssertThrows(try MigrationGuide.from(Path(#file), .init("Endpoints"))) XCTAssertThrows(try MigrationGuide.from("", "")) } - func testMigrationGuideGeneration() throws { - let doc1 = try Documents.v1.instance() as Document - let doc2 = try Documents.v2.instance() as Document + func testMigrationGuideGenerationYAML() throws { + let doc1 = try Documents.v1.decodedContent() as APIDocument + let doc2 = try Documents.v2.decodedContent() as APIDocument let migrationGuide = MigrationGuide(for: doc1, rhs: doc2) try (testDirectoryPath + "migration_guide.yaml").write(migrationGuide.yaml) let decoded = try MigrationGuide.decode(from: testDirectoryPath + "migration_guide.yaml") - XCTAssertEqual(decoded, migrationGuide) - XCTAssertNotEqual(decoded, .empty) + XCTAssert(decoded == migrationGuide) + XCTAssert(decoded != .empty()) + } + + func testMigrationGuideGenerationJSON() throws { + let doc1 = try Documents.v1.decodedContent() as APIDocument + let doc2 = try Documents.v2.decodedContent() as APIDocument + + let migrationGuide = MigrationGuide(for: doc1, rhs: doc2) + try (testDirectoryPath + "migration_guide.json").write(migrationGuide.json) + + let decoded = try MigrationGuide.decode(from: testDirectoryPath + "migration_guide.json") + XCTAssert(decoded == migrationGuide) + XCTAssert(decoded != .empty()) } } diff --git a/Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Auxiliary/APIFile.md b/Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Auxiliary/APIFile.swift similarity index 96% rename from Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Auxiliary/APIFile.md rename to Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Auxiliary/APIFile.swift index 641f2442..c133696a 100644 --- a/Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Auxiliary/APIFile.md +++ b/Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Auxiliary/APIFile.swift @@ -1,6 +1,4 @@ // -// API.swift -// // Created by ApodiniMigrator on 15.08.20 // Copyright © 2020 TUM LS1. All rights reserved. // diff --git a/Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Auxiliary/APIFile.md.license b/Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Auxiliary/APIFile.swift.license similarity index 100% rename from Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Auxiliary/APIFile.md.license rename to Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Auxiliary/APIFile.swift.license diff --git a/Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Auxiliary/ModelsTestFile.md b/Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Auxiliary/ModelsTestFile.swift similarity index 86% rename from Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Auxiliary/ModelsTestFile.md rename to Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Auxiliary/ModelsTestFile.swift index d257317e..752cbf84 100644 --- a/Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Auxiliary/ModelsTestFile.md +++ b/Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Auxiliary/ModelsTestFile.swift @@ -1,6 +1,4 @@ // -// TestFile.swift -// // Created by ApodiniMigrator on 15.08.20 // Copyright © 2020 TUM LS1. All rights reserved. // @@ -24,12 +22,12 @@ final class ApodiniMigratorTests: XCTestCase { let json: JSONValue = """ { - "prop1" : false, - "prop2" : 0, - "prop3" : {}, - "prop4" : 0, - "prop5" : null, - "prop6" : "" + "prop1" : false, + "prop2" : 0, + "prop3" : {}, + "prop4" : 0, + "prop5" : null, + "prop6" : "" } """ diff --git a/Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Auxiliary/ModelsTestFile.md.license b/Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Auxiliary/ModelsTestFile.swift.license similarity index 100% rename from Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Auxiliary/ModelsTestFile.md.license rename to Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Auxiliary/ModelsTestFile.swift.license diff --git a/Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Endpoint/DefaultEndpointFile.md b/Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Endpoint/DefaultEndpointFile.swift similarity index 97% rename from Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Endpoint/DefaultEndpointFile.md rename to Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Endpoint/DefaultEndpointFile.swift index ef621834..0163ca23 100644 --- a/Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Endpoint/DefaultEndpointFile.md +++ b/Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Endpoint/DefaultEndpointFile.swift @@ -1,6 +1,4 @@ // -// TestResponse+Endpoint.swift -// // Created by ApodiniMigrator on 15.08.20 // Copyright © 2020 TUM LS1. All rights reserved. // diff --git a/Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Endpoint/DefaultEndpointFile.md.license b/Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Endpoint/DefaultEndpointFile.swift.license similarity index 100% rename from Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Endpoint/DefaultEndpointFile.md.license rename to Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Endpoint/DefaultEndpointFile.swift.license diff --git a/Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Endpoint/EndpointAddParameterChange.md b/Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Endpoint/EndpointAddParameterChange.swift similarity index 97% rename from Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Endpoint/EndpointAddParameterChange.md rename to Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Endpoint/EndpointAddParameterChange.swift index 2001d674..8e8df573 100644 --- a/Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Endpoint/EndpointAddParameterChange.md +++ b/Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Endpoint/EndpointAddParameterChange.swift @@ -1,6 +1,4 @@ // -// TestResponse+Endpoint.swift -// // Created by ApodiniMigrator on 15.08.20 // Copyright © 2020 TUM LS1. All rights reserved. // diff --git a/Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Endpoint/EndpointAddParameterChange.md.license b/Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Endpoint/EndpointAddParameterChange.swift.license similarity index 100% rename from Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Endpoint/EndpointAddParameterChange.md.license rename to Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Endpoint/EndpointAddParameterChange.swift.license diff --git a/Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Endpoint/EndpointDeleteContentParameterChange.md b/Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Endpoint/EndpointDeleteContentParameterChange.swift similarity index 97% rename from Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Endpoint/EndpointDeleteContentParameterChange.md rename to Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Endpoint/EndpointDeleteContentParameterChange.swift index ed2698bd..f8a04324 100644 --- a/Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Endpoint/EndpointDeleteContentParameterChange.md +++ b/Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Endpoint/EndpointDeleteContentParameterChange.swift @@ -1,6 +1,4 @@ // -// TestResponse+Endpoint.swift -// // Created by ApodiniMigrator on 15.08.20 // Copyright © 2020 TUM LS1. All rights reserved. // diff --git a/Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Endpoint/EndpointDeleteContentParameterChange.md.license b/Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Endpoint/EndpointDeleteContentParameterChange.swift.license similarity index 100% rename from Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Endpoint/EndpointDeleteContentParameterChange.md.license rename to Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Endpoint/EndpointDeleteContentParameterChange.swift.license diff --git a/Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Endpoint/EndpointDeleteParameterChange.md b/Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Endpoint/EndpointDeleteParameterChange.swift similarity index 97% rename from Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Endpoint/EndpointDeleteParameterChange.md rename to Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Endpoint/EndpointDeleteParameterChange.swift index 2d81d515..a1303830 100644 --- a/Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Endpoint/EndpointDeleteParameterChange.md +++ b/Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Endpoint/EndpointDeleteParameterChange.swift @@ -1,6 +1,4 @@ // -// TestResponse+Endpoint.swift -// // Created by ApodiniMigrator on 15.08.20 // Copyright © 2020 TUM LS1. All rights reserved. // diff --git a/Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Endpoint/EndpointDeleteParameterChange.md.license b/Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Endpoint/EndpointDeleteParameterChange.swift.license similarity index 100% rename from Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Endpoint/EndpointDeleteParameterChange.md.license rename to Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Endpoint/EndpointDeleteParameterChange.swift.license diff --git a/Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Endpoint/EndpointDeletedChange.md b/Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Endpoint/EndpointDeletedChange.swift similarity index 95% rename from Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Endpoint/EndpointDeletedChange.md rename to Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Endpoint/EndpointDeletedChange.swift index ceb7dbf2..3a7ee004 100644 --- a/Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Endpoint/EndpointDeletedChange.md +++ b/Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Endpoint/EndpointDeletedChange.swift @@ -1,6 +1,4 @@ // -// TestResponse+Endpoint.swift -// // Created by ApodiniMigrator on 15.08.20 // Copyright © 2020 TUM LS1. All rights reserved. // diff --git a/Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Endpoint/EndpointDeletedChange.md.license b/Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Endpoint/EndpointDeletedChange.swift.license similarity index 100% rename from Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Endpoint/EndpointDeletedChange.md.license rename to Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Endpoint/EndpointDeletedChange.swift.license diff --git a/Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Endpoint/EndpointMultipleChanges.md b/Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Endpoint/EndpointMultipleChanges.swift similarity index 97% rename from Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Endpoint/EndpointMultipleChanges.md rename to Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Endpoint/EndpointMultipleChanges.swift index b07eb844..e070025f 100644 --- a/Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Endpoint/EndpointMultipleChanges.md +++ b/Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Endpoint/EndpointMultipleChanges.swift @@ -1,6 +1,4 @@ // -// TestResponse+Endpoint.swift -// // Created by ApodiniMigrator on 15.08.20 // Copyright © 2020 TUM LS1. All rights reserved. // diff --git a/Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Endpoint/EndpointMultipleChanges.md.license b/Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Endpoint/EndpointMultipleChanges.swift.license similarity index 100% rename from Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Endpoint/EndpointMultipleChanges.md.license rename to Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Endpoint/EndpointMultipleChanges.swift.license diff --git a/Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Endpoint/EndpointOperationChange.md b/Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Endpoint/EndpointOperationChange.swift similarity index 97% rename from Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Endpoint/EndpointOperationChange.md rename to Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Endpoint/EndpointOperationChange.swift index 48713dba..07a0f956 100644 --- a/Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Endpoint/EndpointOperationChange.md +++ b/Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Endpoint/EndpointOperationChange.swift @@ -1,6 +1,4 @@ // -// TestResponse+Endpoint.swift -// // Created by ApodiniMigrator on 15.08.20 // Copyright © 2020 TUM LS1. All rights reserved. // diff --git a/Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Endpoint/EndpointOperationChange.md.license b/Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Endpoint/EndpointOperationChange.swift.license similarity index 100% rename from Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Endpoint/EndpointOperationChange.md.license rename to Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Endpoint/EndpointOperationChange.swift.license diff --git a/Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Endpoint/EndpointParameterKindAndPathChange.md b/Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Endpoint/EndpointParameterKindAndPathChange.swift similarity index 97% rename from Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Endpoint/EndpointParameterKindAndPathChange.md rename to Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Endpoint/EndpointParameterKindAndPathChange.swift index cbca2cf1..5ee005f0 100644 --- a/Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Endpoint/EndpointParameterKindAndPathChange.md +++ b/Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Endpoint/EndpointParameterKindAndPathChange.swift @@ -1,6 +1,4 @@ // -// TestResponse+Endpoint.swift -// // Created by ApodiniMigrator on 15.08.20 // Copyright © 2020 TUM LS1. All rights reserved. // diff --git a/Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Endpoint/EndpointParameterKindAndPathChange.md.license b/Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Endpoint/EndpointParameterKindAndPathChange.swift.license similarity index 100% rename from Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Endpoint/EndpointParameterKindAndPathChange.md.license rename to Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Endpoint/EndpointParameterKindAndPathChange.swift.license diff --git a/Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Endpoint/EndpointParameterNecessityToRequiredChange.md b/Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Endpoint/EndpointParameterNecessityToRequiredChange.swift similarity index 97% rename from Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Endpoint/EndpointParameterNecessityToRequiredChange.md rename to Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Endpoint/EndpointParameterNecessityToRequiredChange.swift index 19bdaa9a..345e8cb0 100644 --- a/Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Endpoint/EndpointParameterNecessityToRequiredChange.md +++ b/Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Endpoint/EndpointParameterNecessityToRequiredChange.swift @@ -1,6 +1,4 @@ // -// TestResponse+Endpoint.swift -// // Created by ApodiniMigrator on 15.08.20 // Copyright © 2020 TUM LS1. All rights reserved. // diff --git a/Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Endpoint/EndpointParameterNecessityToRequiredChange.md.license b/Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Endpoint/EndpointParameterNecessityToRequiredChange.swift.license similarity index 100% rename from Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Endpoint/EndpointParameterNecessityToRequiredChange.md.license rename to Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Endpoint/EndpointParameterNecessityToRequiredChange.swift.license diff --git a/Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Endpoint/EndpointParameterTypeChange.md b/Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Endpoint/EndpointParameterTypeChange.swift similarity index 97% rename from Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Endpoint/EndpointParameterTypeChange.md rename to Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Endpoint/EndpointParameterTypeChange.swift index bc282ce5..42070ab0 100644 --- a/Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Endpoint/EndpointParameterTypeChange.md +++ b/Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Endpoint/EndpointParameterTypeChange.swift @@ -1,6 +1,4 @@ // -// TestResponse+Endpoint.swift -// // Created by ApodiniMigrator on 15.08.20 // Copyright © 2020 TUM LS1. All rights reserved. // diff --git a/Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Endpoint/EndpointParameterTypeChange.md.license b/Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Endpoint/EndpointParameterTypeChange.swift.license similarity index 100% rename from Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Endpoint/EndpointParameterTypeChange.md.license rename to Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Endpoint/EndpointParameterTypeChange.swift.license diff --git a/Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Endpoint/EndpointPathChange.md b/Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Endpoint/EndpointPathChange.swift similarity index 97% rename from Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Endpoint/EndpointPathChange.md rename to Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Endpoint/EndpointPathChange.swift index edd58700..3b8dc458 100644 --- a/Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Endpoint/EndpointPathChange.md +++ b/Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Endpoint/EndpointPathChange.swift @@ -1,6 +1,4 @@ // -// TestResponse+Endpoint.swift -// // Created by ApodiniMigrator on 15.08.20 // Copyright © 2020 TUM LS1. All rights reserved. // diff --git a/Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Endpoint/EndpointPathChange.md.license b/Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Endpoint/EndpointPathChange.swift.license similarity index 100% rename from Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Endpoint/EndpointPathChange.md.license rename to Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Endpoint/EndpointPathChange.swift.license diff --git a/Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Endpoint/EndpointRenameParameterChange.md b/Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Endpoint/EndpointRenameParameterChange.swift similarity index 97% rename from Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Endpoint/EndpointRenameParameterChange.md rename to Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Endpoint/EndpointRenameParameterChange.swift index 7a999043..c0845242 100644 --- a/Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Endpoint/EndpointRenameParameterChange.md +++ b/Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Endpoint/EndpointRenameParameterChange.swift @@ -1,6 +1,4 @@ // -// TestResponse+Endpoint.swift -// // Created by ApodiniMigrator on 15.08.20 // Copyright © 2020 TUM LS1. All rights reserved. // diff --git a/Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Endpoint/EndpointRenameParameterChange.md.license b/Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Endpoint/EndpointRenameParameterChange.swift.license similarity index 100% rename from Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Endpoint/EndpointRenameParameterChange.md.license rename to Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Endpoint/EndpointRenameParameterChange.swift.license diff --git a/Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Endpoint/EndpointResponseChange.md b/Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Endpoint/EndpointResponseChange.swift similarity index 97% rename from Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Endpoint/EndpointResponseChange.md rename to Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Endpoint/EndpointResponseChange.swift index 85294bcf..55e80825 100644 --- a/Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Endpoint/EndpointResponseChange.md +++ b/Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Endpoint/EndpointResponseChange.swift @@ -1,6 +1,4 @@ // -// TestResponse+Endpoint.swift -// // Created by ApodiniMigrator on 15.08.20 // Copyright © 2020 TUM LS1. All rights reserved. // diff --git a/Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Endpoint/EndpointResponseChange.md.license b/Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Endpoint/EndpointResponseChange.swift.license similarity index 100% rename from Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Endpoint/EndpointResponseChange.md.license rename to Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Endpoint/EndpointResponseChange.swift.license diff --git a/Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Endpoint/EndpointWrappedContentParameter.swift b/Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Endpoint/EndpointWrappedContentParameter.swift new file mode 100644 index 00000000..1ed4ed15 --- /dev/null +++ b/Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Endpoint/EndpointWrappedContentParameter.swift @@ -0,0 +1,33 @@ +// +// Created by ApodiniMigrator on 15.08.20 +// Copyright © 2020 TUM LS1. All rights reserved. +// + +import Foundation + +// MARK: - Endpoints +public extension Bool { + /// API call for someHandler at: test + static func id( + wrappedContentParameter: SomeWrappedContent, + authorization: String? = nil, + httpHeaders: HTTPHeaders = [:] + ) -> ApodiniPublisher { + var headers = httpHeaders + headers.setContentType(to: "application/json") + + var errors: [ApodiniError] = [] + + let handler = Handler( + path: "test", + httpMethod: .post, + parameters: [:], + headers: headers, + content: NetworkingService.encode(wrappedContentParameter), + authorization: authorization, + errors: errors + ) + + return NetworkingService.trigger(handler) + } +} diff --git a/Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Enum/DefaultIntEnum.md.license b/Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Endpoint/EndpointWrappedContentParameter.swift.license similarity index 100% rename from Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Enum/DefaultIntEnum.md.license rename to Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Endpoint/EndpointWrappedContentParameter.swift.license diff --git a/Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Enum/DefaultIntEnum.md b/Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Enum/DefaultIntEnum.swift similarity index 91% rename from Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Enum/DefaultIntEnum.md rename to Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Enum/DefaultIntEnum.swift index 2a58f417..b8d6cd92 100644 --- a/Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Enum/DefaultIntEnum.md +++ b/Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Enum/DefaultIntEnum.swift @@ -1,6 +1,4 @@ // -// ProgLang.swift -// // Created by ApodiniMigrator on 15.08.20 // Copyright © 2020 TUM LS1. All rights reserved. // @@ -38,9 +36,6 @@ public enum ProgLang: Int, Codable, CaseIterable { guard deprecated.contains(self) else { return self } - if let alternativeCase = Self.allCases.first(where: { !deprecated.contains($0) }) { - return alternativeCase - } throw ApodiniError(code: 404, message: "The web service does not support the cases of this enum anymore") } } diff --git a/Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Enum/DefaultObjectFile.md.license b/Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Enum/DefaultIntEnum.swift.license similarity index 100% rename from Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Enum/DefaultObjectFile.md.license rename to Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Enum/DefaultIntEnum.swift.license diff --git a/Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Enum/DefaultObjectFile.md b/Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Enum/DefaultObjectFile.swift similarity index 99% rename from Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Enum/DefaultObjectFile.md rename to Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Enum/DefaultObjectFile.swift index 4d1270d6..b6db1528 100644 --- a/Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Enum/DefaultObjectFile.md +++ b/Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Enum/DefaultObjectFile.swift @@ -1,6 +1,4 @@ // -// User.swift -// // Created by ApodiniMigrator on 15.08.20 // Copyright © 2020 TUM LS1. All rights reserved. // diff --git a/Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Enum/DefaultStringEnum.md.license b/Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Enum/DefaultObjectFile.swift.license similarity index 100% rename from Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Enum/DefaultStringEnum.md.license rename to Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Enum/DefaultObjectFile.swift.license diff --git a/Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Enum/DefaultStringEnum.md b/Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Enum/DefaultStringEnum.swift similarity index 90% rename from Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Enum/DefaultStringEnum.md rename to Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Enum/DefaultStringEnum.swift index 51d048ee..f0e1b10e 100644 --- a/Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Enum/DefaultStringEnum.md +++ b/Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Enum/DefaultStringEnum.swift @@ -1,6 +1,4 @@ // -// ProgLang.swift -// // Created by ApodiniMigrator on 15.08.20 // Copyright © 2020 TUM LS1. All rights reserved. // @@ -38,9 +36,6 @@ public enum ProgLang: String, Codable, CaseIterable { guard deprecated.contains(self) else { return self } - if let alternativeCase = Self.allCases.first(where: { !deprecated.contains($0) }) { - return alternativeCase - } throw ApodiniError(code: 404, message: "The web service does not support the cases of this enum anymore") } } diff --git a/Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Enum/EnumAddedCase.md.license b/Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Enum/DefaultStringEnum.swift.license similarity index 100% rename from Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Enum/EnumAddedCase.md.license rename to Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Enum/DefaultStringEnum.swift.license diff --git a/Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Enum/EnumAddedCase.md b/Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Enum/EnumAddedCase.swift similarity index 91% rename from Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Enum/EnumAddedCase.md rename to Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Enum/EnumAddedCase.swift index dfc1a523..54552b8f 100644 --- a/Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Enum/EnumAddedCase.md +++ b/Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Enum/EnumAddedCase.swift @@ -1,6 +1,4 @@ // -// ProgLang.swift -// // Created by ApodiniMigrator on 15.08.20 // Copyright © 2020 TUM LS1. All rights reserved. // @@ -40,9 +38,6 @@ public enum ProgLang: String, Codable, CaseIterable { guard deprecated.contains(self) else { return self } - if let alternativeCase = Self.allCases.first(where: { !deprecated.contains($0) }) { - return alternativeCase - } throw ApodiniError(code: 404, message: "The web service does not support the cases of this enum anymore") } } diff --git a/Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Enum/EnumDeletedCase.md.license b/Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Enum/EnumAddedCase.swift.license similarity index 100% rename from Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Enum/EnumDeletedCase.md.license rename to Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Enum/EnumAddedCase.swift.license diff --git a/Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Enum/EnumDeletedCase.md b/Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Enum/EnumDeletedCase.swift similarity index 90% rename from Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Enum/EnumDeletedCase.md rename to Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Enum/EnumDeletedCase.swift index 8931251b..f10383f0 100644 --- a/Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Enum/EnumDeletedCase.md +++ b/Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Enum/EnumDeletedCase.swift @@ -1,6 +1,4 @@ // -// ProgLang.swift -// // Created by ApodiniMigrator on 15.08.20 // Copyright © 2020 TUM LS1. All rights reserved. // @@ -38,9 +36,6 @@ public enum ProgLang: String, Codable, CaseIterable { guard deprecated.contains(self) else { return self } - if let alternativeCase = Self.allCases.first(where: { !deprecated.contains($0) }) { - return alternativeCase - } throw ApodiniError(code: 404, message: "The web service does not support the cases of this enum anymore") } } diff --git a/Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Enum/EnumDeletedSelf.md.license b/Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Enum/EnumDeletedCase.swift.license similarity index 100% rename from Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Enum/EnumDeletedSelf.md.license rename to Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Enum/EnumDeletedCase.swift.license diff --git a/Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Enum/EnumDeletedSelf.md b/Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Enum/EnumDeletedSelf.swift similarity index 91% rename from Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Enum/EnumDeletedSelf.md rename to Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Enum/EnumDeletedSelf.swift index 6950256f..33ebb461 100644 --- a/Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Enum/EnumDeletedSelf.md +++ b/Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Enum/EnumDeletedSelf.swift @@ -1,6 +1,4 @@ // -// ProgLang.swift -// // Created by ApodiniMigrator on 15.08.20 // Copyright © 2020 TUM LS1. All rights reserved. // @@ -39,9 +37,6 @@ public enum ProgLang: String, Codable, CaseIterable { guard deprecated.contains(self) else { return self } - if let alternativeCase = Self.allCases.first(where: { !deprecated.contains($0) }) { - return alternativeCase - } throw ApodiniError(code: 404, message: "The web service does not support the cases of this enum anymore") } } diff --git a/Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Enum/EnumMultipleChanges.md.license b/Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Enum/EnumDeletedSelf.swift.license similarity index 100% rename from Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Enum/EnumMultipleChanges.md.license rename to Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Enum/EnumDeletedSelf.swift.license diff --git a/Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Enum/EnumMultipleChanges.md b/Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Enum/EnumMultipleChanges.swift similarity index 91% rename from Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Enum/EnumMultipleChanges.md rename to Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Enum/EnumMultipleChanges.swift index d8ca4011..cc60e843 100644 --- a/Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Enum/EnumMultipleChanges.md +++ b/Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Enum/EnumMultipleChanges.swift @@ -1,6 +1,4 @@ // -// ProgLang.swift -// // Created by ApodiniMigrator on 15.08.20 // Copyright © 2020 TUM LS1. All rights reserved. // @@ -40,9 +38,6 @@ public enum ProgLang: String, Codable, CaseIterable { guard deprecated.contains(self) else { return self } - if let alternativeCase = Self.allCases.first(where: { !deprecated.contains($0) }) { - return alternativeCase - } throw ApodiniError(code: 404, message: "The web service does not support the cases of this enum anymore") } } diff --git a/Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Enum/EnumRenamedCase.md.license b/Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Enum/EnumMultipleChanges.swift.license similarity index 100% rename from Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Enum/EnumRenamedCase.md.license rename to Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Enum/EnumMultipleChanges.swift.license diff --git a/Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Enum/EnumRenamedCase.md b/Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Enum/EnumRawValue.swift similarity index 90% rename from Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Enum/EnumRenamedCase.md rename to Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Enum/EnumRawValue.swift index 3e396874..ac26bc6a 100644 --- a/Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Enum/EnumRenamedCase.md +++ b/Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Enum/EnumRawValue.swift @@ -1,6 +1,4 @@ // -// ProgLang.swift -// // Created by ApodiniMigrator on 15.08.20 // Copyright © 2020 TUM LS1. All rights reserved. // @@ -38,9 +36,6 @@ public enum ProgLang: String, Codable, CaseIterable { guard deprecated.contains(self) else { return self } - if let alternativeCase = Self.allCases.first(where: { !deprecated.contains($0) }) { - return alternativeCase - } throw ApodiniError(code: 404, message: "The web service does not support the cases of this enum anymore") } } diff --git a/Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Enum/EnumUnsupportedChange.md.license b/Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Enum/EnumRawValue.swift.license similarity index 100% rename from Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Enum/EnumUnsupportedChange.md.license rename to Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Enum/EnumRawValue.swift.license diff --git a/Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Enum/EnumUnsupportedChange.md b/Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Enum/EnumUnsupportedChange.md deleted file mode 100644 index 22123f5c..00000000 --- a/Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Enum/EnumUnsupportedChange.md +++ /dev/null @@ -1,63 +0,0 @@ -// -// ProgLang.swift -// -// Created by ApodiniMigrator on 15.08.20 -// Copyright © 2020 TUM LS1. All rights reserved. -// - -import Foundation - -// MARK: - Model -@available(*, deprecated, message: "Unsupported change! Raw value type changed") -public enum ProgLang: String, Codable, CaseIterable { - case java - case javaScript - case objectiveC - case other - case python - case ruby - case swift - - // MARK: - Deprecated - private static let deprecatedCases: [Self] = [] - - // MARK: - Encodable - public func encode(to encoder: Encoder) throws { - var container = encoder.singleValueContainer() - - try container.encode(try encodableValue().rawValue) - } - - // MARK: - Decodable - public init(from decoder: Decoder) throws { - self = Self(rawValue: try decoder.singleValueContainer().decode(RawValue.self)) ?? .java - } - - // MARK: - Utils - private func encodableValue() throws -> Self { - let deprecated = Self.deprecatedCases - guard deprecated.contains(self) else { - return self - } - if let alternativeCase = Self.allCases.first(where: { !deprecated.contains($0) }) { - return alternativeCase - } - throw ApodiniError(code: 404, message: "The web service does not support the cases of this enum anymore") - } -} - -// MARK: - CustomStringConvertible -extension ProgLang: CustomStringConvertible { - /// Textual representation - public var description: String { - rawValue.description - } -} - -// MARK: - LosslessStringConvertible -extension ProgLang: LosslessStringConvertible { - /// Instantiates an instance of the conforming type from a string representation. - public init?(_ description: String) { - self.init(rawValue: description) - } -} diff --git a/Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Enum/EnumUnsupportedChange.swift b/Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Enum/EnumUnsupportedChange.swift new file mode 100644 index 00000000..69a59be5 --- /dev/null +++ b/Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Enum/EnumUnsupportedChange.swift @@ -0,0 +1,58 @@ +// +// Created by ApodiniMigrator on 15.08.20 +// Copyright © 2020 TUM LS1. All rights reserved. +// + +import Foundation + +// MARK: - Model +@available(*, deprecated, message: "The raw value type of this enum has changed to Int. ApodiniMigrator is not able to migrate this change.") +public enum ProgLang: String, Codable, CaseIterable { + case java + case javaScript + case objectiveC + case other + case python + case ruby + case swift + + // MARK: - Deprecated + private static let deprecatedCases: [Self] = [] + + // MARK: - Encodable + public func encode(to encoder: Encoder) throws { + var container = encoder.singleValueContainer() + + try container.encode(try encodableValue().rawValue) + } + + // MARK: - Decodable + public init(from decoder: Decoder) throws { + self = Self(rawValue: try decoder.singleValueContainer().decode(RawValue.self)) ?? .java + } + + // MARK: - Utils + private func encodableValue() throws -> Self { + let deprecated = Self.deprecatedCases + guard deprecated.contains(self) else { + return self + } + throw ApodiniError(code: 404, message: "The web service does not support the cases of this enum anymore") + } +} + +// MARK: - CustomStringConvertible +extension ProgLang: CustomStringConvertible { + /// Textual representation + public var description: String { + rawValue.description + } +} + +// MARK: - LosslessStringConvertible +extension ProgLang: LosslessStringConvertible { + /// Instantiates an instance of the conforming type from a string representation. + public init?(_ description: String) { + self.init(rawValue: description) + } +} diff --git a/Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Object/ObjectAddedProperty.md.license b/Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Enum/EnumUnsupportedChange.swift.license similarity index 100% rename from Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Object/ObjectAddedProperty.md.license rename to Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Enum/EnumUnsupportedChange.swift.license diff --git a/Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Object/ObjectAddedProperty.md b/Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Object/ObjectAddedProperty.swift similarity index 99% rename from Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Object/ObjectAddedProperty.md rename to Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Object/ObjectAddedProperty.swift index c1db888a..6be6afb5 100644 --- a/Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Object/ObjectAddedProperty.md +++ b/Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Object/ObjectAddedProperty.swift @@ -1,6 +1,4 @@ // -// User.swift -// // Created by ApodiniMigrator on 15.08.20 // Copyright © 2020 TUM LS1. All rights reserved. // diff --git a/Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Object/ObjectDeletedChange.md.license b/Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Object/ObjectAddedProperty.swift.license similarity index 100% rename from Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Object/ObjectDeletedChange.md.license rename to Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Object/ObjectAddedProperty.swift.license diff --git a/Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Object/ObjectDeletedChange.md b/Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Object/ObjectDeletedChange.swift similarity index 99% rename from Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Object/ObjectDeletedChange.md rename to Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Object/ObjectDeletedChange.swift index bda772eb..7505aca9 100644 --- a/Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Object/ObjectDeletedChange.md +++ b/Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Object/ObjectDeletedChange.swift @@ -1,6 +1,4 @@ // -// User.swift -// // Created by ApodiniMigrator on 15.08.20 // Copyright © 2020 TUM LS1. All rights reserved. // diff --git a/Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Object/ObjectDeletedProperty.md.license b/Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Object/ObjectDeletedChange.swift.license similarity index 100% rename from Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Object/ObjectDeletedProperty.md.license rename to Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Object/ObjectDeletedChange.swift.license diff --git a/Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Object/ObjectDeletedProperty.md b/Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Object/ObjectDeletedProperty.swift similarity index 99% rename from Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Object/ObjectDeletedProperty.md rename to Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Object/ObjectDeletedProperty.swift index c50260f4..37783d7b 100644 --- a/Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Object/ObjectDeletedProperty.md +++ b/Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Object/ObjectDeletedProperty.swift @@ -1,6 +1,4 @@ // -// User.swift -// // Created by ApodiniMigrator on 15.08.20 // Copyright © 2020 TUM LS1. All rights reserved. // diff --git a/Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Object/ObjectMultipleChange.md.license b/Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Object/ObjectDeletedProperty.swift.license similarity index 100% rename from Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Object/ObjectMultipleChange.md.license rename to Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Object/ObjectDeletedProperty.swift.license diff --git a/Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Object/ObjectMultipleChange.md b/Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Object/ObjectMultipleChange.swift similarity index 99% rename from Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Object/ObjectMultipleChange.md rename to Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Object/ObjectMultipleChange.swift index be371a58..941d8725 100644 --- a/Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Object/ObjectMultipleChange.md +++ b/Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Object/ObjectMultipleChange.swift @@ -1,6 +1,4 @@ // -// User.swift -// // Created by ApodiniMigrator on 15.08.20 // Copyright © 2020 TUM LS1. All rights reserved. // diff --git a/Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Object/ObjectPropertyNecessityToOptionalChange.md.license b/Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Object/ObjectMultipleChange.swift.license similarity index 100% rename from Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Object/ObjectPropertyNecessityToOptionalChange.md.license rename to Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Object/ObjectMultipleChange.swift.license diff --git a/Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Object/ObjectPropertyNecessityToOptionalChange.md b/Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Object/ObjectPropertyNecessityToOptionalChange.swift similarity index 99% rename from Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Object/ObjectPropertyNecessityToOptionalChange.md rename to Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Object/ObjectPropertyNecessityToOptionalChange.swift index f2cccbac..9d702539 100644 --- a/Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Object/ObjectPropertyNecessityToOptionalChange.md +++ b/Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Object/ObjectPropertyNecessityToOptionalChange.swift @@ -1,6 +1,4 @@ // -// User.swift -// // Created by ApodiniMigrator on 15.08.20 // Copyright © 2020 TUM LS1. All rights reserved. // diff --git a/Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Object/ObjectPropertyNecessityToRequiredChange.md.license b/Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Object/ObjectPropertyNecessityToOptionalChange.swift.license similarity index 100% rename from Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Object/ObjectPropertyNecessityToRequiredChange.md.license rename to Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Object/ObjectPropertyNecessityToOptionalChange.swift.license diff --git a/Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Object/ObjectPropertyNecessityToRequiredChange.md b/Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Object/ObjectPropertyNecessityToRequiredChange.swift similarity index 99% rename from Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Object/ObjectPropertyNecessityToRequiredChange.md rename to Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Object/ObjectPropertyNecessityToRequiredChange.swift index 3ef1e42e..a678daa5 100644 --- a/Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Object/ObjectPropertyNecessityToRequiredChange.md +++ b/Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Object/ObjectPropertyNecessityToRequiredChange.swift @@ -1,6 +1,4 @@ // -// User.swift -// // Created by ApodiniMigrator on 15.08.20 // Copyright © 2020 TUM LS1. All rights reserved. // diff --git a/Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Object/ObjectPropertyTypeChange.md.license b/Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Object/ObjectPropertyNecessityToRequiredChange.swift.license similarity index 100% rename from Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Object/ObjectPropertyTypeChange.md.license rename to Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Object/ObjectPropertyNecessityToRequiredChange.swift.license diff --git a/Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Object/ObjectPropertyTypeChange.md b/Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Object/ObjectPropertyTypeChange.swift similarity index 99% rename from Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Object/ObjectPropertyTypeChange.md rename to Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Object/ObjectPropertyTypeChange.swift index f21ada7c..f53f9ff5 100644 --- a/Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Object/ObjectPropertyTypeChange.md +++ b/Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Object/ObjectPropertyTypeChange.swift @@ -1,6 +1,4 @@ // -// User.swift -// // Created by ApodiniMigrator on 15.08.20 // Copyright © 2020 TUM LS1. All rights reserved. // diff --git a/Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Object/ObjectRenamedProperty.md.license b/Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Object/ObjectPropertyTypeChange.swift.license similarity index 100% rename from Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Object/ObjectRenamedProperty.md.license rename to Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Object/ObjectPropertyTypeChange.swift.license diff --git a/Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Object/ObjectRenamedProperty.md b/Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Object/ObjectRenamedProperty.swift similarity index 99% rename from Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Object/ObjectRenamedProperty.md rename to Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Object/ObjectRenamedProperty.swift index f5f11c01..fcd5031f 100644 --- a/Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Object/ObjectRenamedProperty.md +++ b/Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Object/ObjectRenamedProperty.swift @@ -1,6 +1,4 @@ // -// User.swift -// // Created by ApodiniMigrator on 15.08.20 // Copyright © 2020 TUM LS1. All rights reserved. // diff --git a/Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Object/ObjectUnsupportedChange.md.license b/Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Object/ObjectRenamedProperty.swift.license similarity index 100% rename from Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Object/ObjectUnsupportedChange.md.license rename to Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Object/ObjectRenamedProperty.swift.license diff --git a/Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Object/ObjectUnsupportedChange.md b/Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Object/ObjectUnsupportedChange.swift similarity index 92% rename from Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Object/ObjectUnsupportedChange.md rename to Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Object/ObjectUnsupportedChange.swift index e7352f56..5d392465 100644 --- a/Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Object/ObjectUnsupportedChange.md +++ b/Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Object/ObjectUnsupportedChange.swift @@ -1,6 +1,4 @@ // -// User.swift -// // Created by ApodiniMigrator on 15.08.20 // Copyright © 2020 TUM LS1. All rights reserved. // @@ -8,7 +6,7 @@ import Foundation // MARK: - Model -@available(*, deprecated, message: "Unsupported change! Type changed to enum") +@available(*, deprecated, message: "ApodiniMigrator is not able to handle the migration of User. Change from enum to object or vice versa is currently not supported.") public struct User: Codable { // MARK: - CodingKeys private enum CodingKeys: String, CodingKey { diff --git a/Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Object/ObjectUnsupportedChange.swift.license b/Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Object/ObjectUnsupportedChange.swift.license new file mode 100644 index 00000000..b4b35f1c --- /dev/null +++ b/Tests/ApodiniMigratorTests/Resources/ExpectedOutputs/Object/ObjectUnsupportedChange.swift.license @@ -0,0 +1,7 @@ +// +// This source file is part of the Apodini open source project +// +// SPDX-FileCopyrightText: 2019-2021 Paul Schmiedmayer and the Apodini project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// \ No newline at end of file diff --git a/Tests/ApodiniMigratorTests/Utils/ApodiniMigratorXCTestCase.swift b/Tests/ApodiniMigratorTests/Utils/ApodiniMigratorXCTestCase.swift index b3acaeff..3599e76b 100644 --- a/Tests/ApodiniMigratorTests/Utils/ApodiniMigratorXCTestCase.swift +++ b/Tests/ApodiniMigratorTests/Utils/ApodiniMigratorXCTestCase.swift @@ -9,17 +9,18 @@ import XCTest import PathKit @testable import ApodiniMigratorCompare +@testable import RESTMigrator @testable import ApodiniMigrator class ApodiniMigratorXCTestCase: XCTestCase { - var node = ChangeContextNode() + var comparisonContext = ChangeComparisonContext() let testDirectory = "./\(UUID().uuidString)" var testDirectoryPath: Path { - testDirectory.asPath + Path(testDirectory) } - private func testTestDirectoryCreated() throws { + func testTestDirectoryCreated() throws { XCTAssert(testDirectoryPath.exists) XCTAssert(try testDirectoryPath.children().isEmpty) } @@ -34,17 +35,17 @@ class ApodiniMigratorXCTestCase: XCTestCase { override func tearDownWithError() throws { try super.tearDownWithError() - - node = ChangeContextNode() + + comparisonContext = ChangeComparisonContext() try testDirectoryPath.delete() } - func XCTAssertNoThrowWithResult(_ expression: @autoclosure () throws -> T) -> T { - XCTAssertNoThrow(try expression()) + func XCTAssertNoThrowWithResult(_ expression: @autoclosure () throws -> T, file: StaticString = #file, line: UInt = #line) -> T { + XCTAssertNoThrow(try expression(), file: file, line: line) do { return try expression() } catch { - XCTFail(error.localizedDescription) + XCTFail(error.localizedDescription, file: file, line: line) } preconditionFailure("Expression threw an error") } @@ -52,7 +53,7 @@ class ApodiniMigratorXCTestCase: XCTestCase { func XCTAssertThrows(_ expression: @autoclosure () throws -> T) { let expectation = XCTestExpectation(description: "Expression did throw") do { - _ = try expression() + try _ = expression() XCTFail("Expression did not throw") } catch { expectation.fulfill() @@ -60,15 +61,32 @@ class ApodiniMigratorXCTestCase: XCTestCase { } func firstNonEqualLine(lhs: String, _ rhs: String, function: StaticString = #function, file: StaticString = #file, line: UInt = #line) { - for (lhsLine, rhsLine) in zip(lhs.lines(), rhs.lines()) where lhsLine != rhsLine { + let lhsLines = lhs.components(separatedBy: "\n") + let rhsLines = rhs.components(separatedBy: "\n") + + for (lhsLine, rhsLine) in zip(lhsLines, rhsLines) where lhsLine != rhsLine { print("Lhs line: \(lhsLine)") print("Rhs line: \(rhsLine)") fatalError("Found non-equal line in \(function)", file: file, line: line) } } - - func XCTMigratorAssertEqual(_ rendarable: Renderable, _ resource: OutputFiles) { - XCTAssertEqual(rendarable.indentationFormatted(), resource.content().indentationFormatted()) + + @inlinable + func XCTMigratorAssertEqual( + _ rendarable: GeneratedFile, + _ resource: OutputFiles, + with context: MigrationContext = MigrationContext(packageName: "ApodiniMigrator"), + _ message: @autoclosure () -> String = "", + file: StaticString = #filePath, + line: UInt = #line + ) { + XCTAssertEqual( + rendarable.formattedFile(with: context), + resource.content(), + message(), + file: file, + line: line + ) } func canImportJavaScriptCore() -> Bool { @@ -80,3 +98,14 @@ class ApodiniMigratorXCTestCase: XCTestCase { #endif } } + +extension MigrationContext { + init(packageName: String) { + self.init( + bundle: .module, + logger: .init(label: "org.apodini.test") + ) + + placeholderValues[.packageName] = packageName + } +} diff --git a/Tests/ApodiniMigratorTests/Utils/Documents.swift b/Tests/ApodiniMigratorTests/Utils/Documents.swift new file mode 100644 index 00000000..873e7b91 --- /dev/null +++ b/Tests/ApodiniMigratorTests/Utils/Documents.swift @@ -0,0 +1,19 @@ +// +// This source file is part of the Apodini open source project +// +// SPDX-FileCopyrightText: 2019-2021 Paul Schmiedmayer and the Apodini project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + +import Foundation + +enum Documents: String, TestResource { + case v1 = "api_qonectiq1.0.0" + case v2 = "api_qonectiq2.0.0" + case migrationGuide = "migration_guide" + + var fileName: String { + rawValue + ".json" + } +} diff --git a/Tests/ApodiniMigratorTests/Utils/Resources.swift b/Tests/ApodiniMigratorTests/Utils/OutputFiles.swift similarity index 70% rename from Tests/ApodiniMigratorTests/Utils/Resources.swift rename to Tests/ApodiniMigratorTests/Utils/OutputFiles.swift index 38380bdb..31d4847a 100644 --- a/Tests/ApodiniMigratorTests/Utils/Resources.swift +++ b/Tests/ApodiniMigratorTests/Utils/OutputFiles.swift @@ -7,33 +7,18 @@ // import Foundation -import ApodiniMigrator -extension Resource { - var bundle: Bundle { .module } -} - -enum Documents: String, Resource { - case v1 = "api_qonectiq1.0.0" - case v2 = "api_qonectiq2.0.0" - case migrationGuide = "migration_guide" - - var fileExtension: FileExtension { .json } - - var name: String { rawValue } -} - -enum OutputFiles: String, Resource { +enum OutputFiles: String, TestResource { // enum files case defaultStringEnum case defaultIntEnum case enumAddedCase case enumDeletedCase - case enumRenamedCase + case enumRawValue case enumDeletedSelf case enumUnsupportedChange case enumMultipleChanges - + // object files case defaultObjectFile case objectAddedProperty @@ -45,11 +30,11 @@ enum OutputFiles: String, Resource { case objectUnsupportedChange case objectDeletedChange case objectMultipleChange - + // auxiliary case modelsTestFile case aPIFile - + // endpoint files case defaultEndpointFile case endpointPathChange @@ -58,14 +43,15 @@ enum OutputFiles: String, Resource { case endpointDeleteParameterChange case endpointDeleteContentParameterChange case endpointRenameParameterChange - case endpointParameterNecessityToRequiredChange + case endpointParameterNecessityToRequiredChange // swiftlint:disable:this identifier_name case endpointParameterKindAndPathChange case endpointParameterTypeChange case endpointResponseChange case endpointDeletedChange case endpointMultipleChanges - - var fileExtension: FileExtension { .markdown } - - var name: String { rawValue.upperFirst } + case endpointWrappedContentParameter + + var fileName: String { + rawValue.upperFirst + ".swift" + } } diff --git a/Tests/ApodiniMigratorTests/Utils/TestResource.swift b/Tests/ApodiniMigratorTests/Utils/TestResource.swift new file mode 100644 index 00000000..7250e94d --- /dev/null +++ b/Tests/ApodiniMigratorTests/Utils/TestResource.swift @@ -0,0 +1,48 @@ +// +// This source file is part of the Apodini open source project +// +// SPDX-FileCopyrightText: 2019-2021 Paul Schmiedmayer and the Apodini project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + +import Foundation +import XCTest +import RESTMigrator + +protocol TestResource { + var bundle: Bundle { get } + + var fileName: String { get } + var bundleFileURL: URL { get } + + func content() -> String +} + +extension TestResource { + var bundle: Bundle { .module } + + var bundleFileURL: URL { + guard let fileUrl = bundle.url(forResource: fileName, withExtension: nil) else { + fatalError("Resource \(fileName) not found!") + } + + return fileUrl + } + + var bundlePath: Path { + Path(bundleFileURL.path) + } + + func content() -> String { + guard let content = try? String(contentsOf: bundleFileURL, encoding: .utf8) else { + fatalError("Failed to read the resource \(fileName)") + } + + return content + } + + func decodedContent() throws -> D { + try D.decode(from: content()) + } +} diff --git a/Tests/ApodiniMigratorTests/Utils/TestTypes.swift b/Tests/ApodiniMigratorTests/Utils/TestTypes.swift index 91fb56d9..c8ed8927 100644 --- a/Tests/ApodiniMigratorTests/Utils/TestTypes.swift +++ b/Tests/ApodiniMigratorTests/Utils/TestTypes.swift @@ -56,6 +56,7 @@ extension TestTypes { let url: URL let scores: [Set] let name: String? + // swiftlint:disable:next discouraged_optional_collection let nestedDirections: Set<[[[[[Direction]?]?]?]]> // testing recursive storing and reconstructing in `TypesStore` let shops: [Shop] let cars: [String: Car] diff --git a/Tests/ApodiniMigratorTests/Utils/XCTAssertRuntimeFailure.swift b/Tests/ApodiniMigratorTests/Utils/XCTAssertRuntimeFailure.swift new file mode 100644 index 00000000..0ba91930 --- /dev/null +++ b/Tests/ApodiniMigratorTests/Utils/XCTAssertRuntimeFailure.swift @@ -0,0 +1,45 @@ +// +// This source file is part of the Apodini open source project +// +// SPDX-FileCopyrightText: 2019-2021 Paul Schmiedmayer and the Apodini project authors (see CONTRIBUTORS.md) +// +// SPDX-License-Identifier: MIT +// + +import XCTest + +#if canImport(XCTAssertCrash) +@_implementationOnly import XCTAssertCrash + +/// Asserts that an expression leads to a runtime failure. +/// +/// - Parameters: +/// - expression: The expression which should be evaluated and asserted to result in a runtime failure. +/// Note, while the closure is throwing, a thrown Error is not considered a runtime failure. +/// Encountering a thrown Swift Error is considered a failure. +/// - message: The message should there be no runtime failure. +/// - file: The file this method is called from. +/// - line: The line this method is called from. +public func XCTAssertRuntimeFailure( + _ expression: @escaping @autoclosure () throws -> T, + _ message: @autoclosure () -> String = "XCTAssertRuntimeFailure didn't fail as expected!", + file: StaticString = #filePath, + line: UInt = #line) { + XCTAssertCrash( + XCTAssertNoThrow(try expression()), + message(), + file: file, + line: line, + skipIfBeingDebugged: false) +} +#else +/// Empty implementation used for platforms that don't support `CwlPreconditionTesting`. +public func XCTAssertRuntimeFailure( + _ expression: @escaping @autoclosure () throws -> T, + _ message: @autoclosure () -> String = "", + file: StaticString = #filePath, + line: UInt = #line) { + // Empty implementation for Linux Tests + print("[NOTICE] XCTAssertRuntimeFailure unsupported on this platform!") +} +#endif diff --git a/codecov.yml b/codecov.yml index 9b04b7e0..73df7bf7 100644 --- a/codecov.yml +++ b/codecov.yml @@ -8,6 +8,7 @@ ignore: - "Tests" +- "Sources/ApodiniMigratorCompare/LegacyChangeModel" codecov: require_ci_to_pass: yes