├── .gitattributes ├── .github └── workflows │ ├── ci.yaml │ └── release.yaml ├── .gitignore ├── LICENSE ├── README.md ├── build.gradle.kts ├── decomat-core ├── build.gradle.kts └── src │ ├── commonMain │ └── kotlin │ │ └── io │ │ └── decomat │ │ ├── Annotations.kt │ │ ├── ContextComponents.kt │ │ ├── CustomPattern.kt │ │ ├── Is.kt │ │ ├── Matching.kt │ │ ├── Pattern.kt │ │ ├── PatternM.kt │ │ ├── ProductClass.kt │ │ ├── ThenPattern1.kt │ │ ├── Typed.kt │ │ └── fail │ │ └── PatternErrors.kt │ ├── commonTest │ └── kotlin │ │ └── io │ │ └── decomat │ │ ├── DecomatTest.kt │ │ ├── ExampleDsl.kt │ │ ├── MatchTest.kt │ │ ├── PersonDsl.kt │ │ ├── PersonDslTest.kt │ │ ├── ThenTest.kt │ │ ├── manual │ │ └── ThenPattern2.kt │ │ └── middle │ │ ├── ExampleMiddleDsl.kt │ │ ├── MatchTest.kt │ │ ├── PersonMiddleDsl.kt │ │ └── PersonMiddleDslTest.kt │ └── templates │ └── Pattern3.ftl ├── decomat-examples ├── build.gradle.kts └── src │ ├── commonMain │ └── kotlin │ │ └── io │ │ └── decomat │ │ └── examples │ │ ├── customPatternData1 │ │ └── Data.kt │ │ ├── customPatternData2 │ │ └── Data.kt │ │ ├── querydsl │ │ └── queryDslCustomExample.kt │ │ ├── querydslcustom │ │ └── QueryDslCustomExample.kt │ │ └── querydslmiddle │ │ └── queryDslCustomMiddleExample.kt │ └── commonTest │ └── kotlin │ └── io │ └── decomat │ └── examples │ ├── CustomPatternTest.kt │ ├── CustomPatternTest2.kt │ ├── CustomPatternTestData.kt │ ├── CustomPatternTestData2.kt │ ├── QueryPatternCustomTest.kt │ └── QueryPatternTest.kt ├── decomat-ksp ├── build.gradle.kts └── src │ └── main │ ├── kotlin │ └── io │ │ └── decomat │ │ ├── DecomatProcessor.kt │ │ ├── DecomatProvider.kt │ │ └── FreemarkerTest.kt │ └── resources │ └── META-INF │ └── services │ └── com.google.devtools.ksp.processing.SymbolProcessorProvider ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── kotlin-js-store └── yarn.lock ├── settings.gradle.kts └── test.kt /.gitattributes: -------------------------------------------------------------------------------- 1 | # 2 | # https://help.github.com/articles/dealing-with-line-endings/ 3 | # 4 | # Linux start script should use lf 5 | /gradlew text eol=lf 6 | 7 | # These are Windows script files and should use crlf 8 | *.bat text eol=crlf 9 | 10 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [pull_request, push] 4 | 5 | jobs: 6 | build: 7 | runs-on: ${{ matrix.os }} 8 | strategy: 9 | matrix: 10 | # OSX takes 2x longer to build i.e. usually 8m instead of 4m for the linux/windows builds 11 | # maybe want to skip it for non-release builds. Alternatively maybe parallelize the osx targets 12 | # e.g. one VM can do buildMacosX64 buildMacosArm64 buildIosX64 13 | # and the other can do buildIosArm64 buildIosSimulatorArm64 14 | # or something like that 15 | os: [ ubuntu-latest, windows-latest ] #, macOS-latest 16 | include: 17 | - os: ubuntu-latest 18 | platform: linux 19 | build_command: build 20 | # - os: macOS-latest 21 | # platform: mac 22 | # build_command: build 23 | - os: windows-latest 24 | platform: windows 25 | build_command: build 26 | steps: 27 | - uses: actions/checkout@v3 28 | - name: Set up JDK 11 29 | uses: actions/setup-java@v3 30 | with: 31 | java-version: '11' 32 | distribution: 'adopt' 33 | - name: Validate Gradle wrapper 34 | uses: gradle/wrapper-validation-action@v1 35 | - run: ./gradlew ${{ matrix.build_command }} -Pplatform=${{ matrix.platform }} --stacktrace -Pnosign -PisCI 36 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | workflow_dispatch 5 | 6 | jobs: 7 | create_staging_repository: 8 | runs-on: ubuntu-latest 9 | name: Create staging repository 10 | outputs: 11 | repository_id: ${{ steps.create.outputs.repository_id }} 12 | steps: 13 | - id: create 14 | uses: nexus-actions/create-nexus-staging-repo@main 15 | with: 16 | username: ${{ secrets.ORG_GRADLE_PROJECT_SONATYPEUSERNAME }} 17 | password: ${{ secrets.ORG_GRADLE_PROJECT_SONATYPEPASSWORD }} 18 | staging_profile_id: ${{ secrets.SONATYPE_PROFILE_ID }} 19 | # Defaults to the https://oss.sonatype.org, not https://s01.oss.sonatype.org 20 | base_url: https://s01.oss.sonatype.org/service/local/ 21 | description: ${{ github.repository }}/${{ github.workflow }}#${{ github.run_number }} 22 | 23 | release: 24 | needs: [create_staging_repository] 25 | runs-on: ${{ matrix.os }} 26 | strategy: 27 | matrix: 28 | os: [ ubuntu-latest, macOS-latest, windows-latest ] 29 | include: 30 | - os: ubuntu-latest 31 | # On ubuntu publish everything you can i.e. the linuxX64 and jvm binaries 32 | publish_command: build publishAllPublicationsToOss 33 | platform: linux 34 | - os: macOS-latest 35 | # publishTvosX64PublicationToOss publishTvosArm64PublicationToOss publishWatchosX64PublicationToOss publishWatchosArm32PublicationToOss publishWatchosArm64PublicationToOss 36 | publish_command: >- 37 | build 38 | publishMacosX64PublicationToOss 39 | publishMacosArm64PublicationToOss 40 | publishIosX64PublicationToOss 41 | publishIosArm64PublicationToOssRepository 42 | publishIosSimulatorArm64PublicationToOss 43 | publishTvosX64PublicationToOss 44 | publishTvosArm64PublicationToOss 45 | publishWatchosX64PublicationToOss 46 | publishWatchosArm32PublicationToOss 47 | publishWatchosArm64PublicationToOss 48 | platform: mac 49 | - os: windows-latest 50 | publish_command: build publishMingwX64PublicationToOss 51 | platform: windows 52 | env: 53 | SONATYPE_REPOSITORY_ID: ${{ needs.create_staging_repository.outputs.repository_id }} 54 | SONATYPE_USERNAME: ${{ secrets.ORG_GRADLE_PROJECT_SONATYPEUSERNAME }} 55 | SONATYPE_PASSWORD: ${{ secrets.ORG_GRADLE_PROJECT_SONATYPEPASSWORD }} 56 | NEW_SIGNING_KEY_ID_BASE64: ${{ secrets.NEW_SIGNING_KEY_ID_BASE64 }} 57 | NEW_SIGNING_KEY_ID_BASE64_PASS: ${{ secrets.NEW_SIGNING_KEY_ID_BASE64_PASS }} 58 | steps: 59 | - uses: actions/checkout@v3 60 | - name: Set up JDK 11 61 | uses: actions/setup-java@v3 62 | with: 63 | java-version: '11' 64 | distribution: 'adopt' 65 | - name: Validate Gradle wrapper 66 | uses: gradle/wrapper-validation-action@v1 67 | - run: ./gradlew ${{ matrix.publish_command }} -Pplatform=${{ matrix.platform }} -PisCI -no-daemon --stacktrace 68 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### Varia 2 | 3 | build/ 4 | .gradle 5 | .idea 6 | 7 | ### Kotlin template 8 | .project/* 9 | **/.settings/* 10 | project/target/* 11 | project/project/* 12 | decomat-core/target/* 13 | decomat-ksp/target/* 14 | 15 | # Log file 16 | *.log 17 | 18 | # BlueJ files 19 | *.ctxt 20 | 21 | # Mobile Tools for Java (J2ME) 22 | .mtj.tmp/ 23 | 24 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 25 | hs_err_pid* 26 | replay_pid* 27 | 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Decomat 2 | 3 | Scala-Style Deconstructive Pattern-Matching for Kotlin. 4 | 5 | Decomat is available on Maven Central. To use it, add the following to your `build.gradle.kts`: 6 | ``` 7 | implementation("io.exoquery:decomat-core:0.0.2") 8 | ksp("io.exoquery:decomat-ksp:0.0.2") 9 | ``` 10 | 11 | ## Introduction 12 | 13 | Decomat is a library that gives Kotlin a way to do pattern-matching on ADTs (Algebraic Data Types) in a way 14 | that is similar to Scala's pattern-matching. For example: 15 | 16 | ```scala 17 | case class Customer(name: Name, affiliate: Affiliate) 18 | case class Name(first: String, last: String) 19 | sealed trait Affiliate 20 | case class Partner(id: Int) extends Affiliate 21 | case class Organization(name: String) extends Affiliate 22 | 23 | someone match { 24 | case Customer(Name(first @ "Joe", last), Partner(id)) => func(first, last, id) 25 | case Customer(Name(first @ "Jack", last), Organization("BigOrg")) => func(first, last) 26 | } 27 | ``` 28 | 29 | Similarly, in Kotlin with Decomat you can do this: 30 | ```kotlin 31 | 32 | bigListOfPeople.mapNotNull { p -> 33 | p.match( 34 | case( Customer[FullName[Is("Joe"), Is()], Partner[Is()]] ) 35 | .then { first, last, id -> func(first, last, id) }, 36 | ) 37 | } 38 | 39 | ``` 40 | 41 | 42 | 43 | ```kotlin 44 | on(someone).match( 45 | case( Customer[Name[Is("Joe"), Is()], Partner[Is()]] ) 46 | .then { first, last, id -> func(first, last, id) }, 47 | case( Customer[Name[Is("Jack"), Is()], Organization[Is("BigOrg")]] ) 48 | .then { first, last -> func(first, last) } 49 | ) 50 | ``` 51 | 52 | Whereas normally the following would be needed: 53 | ```kotlin 54 | when(someone) { 55 | is Customer -> 56 | if (someone.name.first == "Joe") { 57 | when (val aff = someone.affiliate) { 58 | is Partner -> { 59 | func(someone.name.first, someone.name.last, aff.id) 60 | } 61 | else -> fail() 62 | } 63 | } else if (someone.name.first == "Jack") { 64 | when (val aff = someone.affiliate) { 65 | is Organization -> { 66 | if (aff.name == "BigOrg") { 67 | func(someone.name.first, someone.name.last) 68 | } else fail() 69 | } 70 | else -> fail() 71 | } 72 | } else fail() 73 | } 74 | ``` 75 | 76 | Decomat is not a full replacement of Scala's pattern-matching, but it does have some of the same 77 | features and usage patterns in a limited scope. The primary insight behind Decomat is that for in most of 78 | the cases where Scala ADT pattern matching is used: 79 | 80 | * No more than two components need to be deconstructed (3 will be partially supported soon) 81 | * The deconstruction itself does not need to be more than two levels deep 82 | * The components that need to be deconstructed are usually known as the ADT case-classes are being written. 83 | * Frequently, other parts of the main object need to be checked during the pattern matching but they do not 84 | need to be deconstructed. This can typically be done with a simple `if` statement (see `thenIfThis` below). 85 | 86 | ## Tutorial 87 | 88 | #### 1. Build 89 | In order to get started with Decomat, add the needed dependencies to your `build.gradle.kts` 90 | file and enable KSP. Decomat relies on various extension methods that are generated by KSP. 91 | ``` 92 | // build.gradle.kts 93 | plugins { 94 | ... 95 | id("com.google.devtools.ksp") version "" 96 | } 97 | 98 | implementation("io.exoquery:decomat-core:") 99 | ksp("io.exoquery:decomat-ksp:") 100 | ``` 101 | 102 | #### 2. Annotate 103 | 104 | Then: 105 | * Add the `@Matchable` annotation your class and `@Component` annotation to (up to two) constructor parameters. 106 | * Make the Data Class extend `HasProductClass`. 107 | * Add the `productComponents` field to your class and pass `this` and the component-fields into it. 108 | * Add an empty companion-object 109 | ```kotlin 110 | @Matchable 111 | data class Customer(@Component val name: Name, @Component val affiliate: Affiliate) { 112 | override val productComponents = productComponentsOf(this, name, affiliate) 113 | companion object {} 114 | } 115 | ``` 116 | 117 | Follow these steps for all other classes that you want to pattern match on, in the case above to `Name` and `Partner` as follows: 118 | 119 | ```kotlin 120 | @Matchable 121 | data class Name(@Component val first: String, @Component val last: String) { 122 | override val productComponents = productComponentsOf(this, first, last) 123 | companion object {} 124 | } 125 | 126 | @Matchable 127 | data class Partner(@Component val id: Int) { 128 | override val productComponents = productComponentsOf(this, id) 129 | companion object {} 130 | } 131 | ``` 132 | 133 | Then use KSP to generate the needed extension methods, in IntelliJ this typically just means 134 | running the 'Rebuild Project' command. The extension-methods will be generated inside of your 135 | project under `projectDir/build/generated/ksp/main/kotlin/`. They will 136 | be placed in the same package as the `@Matchable` data classes. 137 | 138 | > Note that ONLY the parameters that you actually want to deconstruct shuold be annotated with `@Component` 139 | > and only 2 are supported. You can use the `thenIfThis` and `thenThis` methods to conveniently interact 140 | > with non-component methods during filtration. 141 | > There can be other non-component parameters in the constructor before, after, and 142 | > in-between them: 143 | > ```kotlin 144 | > @Matchable 145 | > data class Customer( 146 | > val something: String, 147 | > @Component val name: Name, 148 | > val somethingElse: String, 149 | > @Component val affiliate: Affiliate, 150 | > val yetSomethingElse: String 151 | > ) { ... } 152 | > ``` 153 | 154 | #### 3. Use! 155 | 156 | Then you can use the `on` and `case` functions to pattern match on the ADTs and the `then` function to 157 | perform transformations. 158 | ```kotlin 159 | on(someone).match( 160 | case( Customer[Name[Is("Joe"), Is()], Partner(Is())] ) 161 | .then { first, last, id -> func(first, last, id) }, 162 | // Other cases... 163 | } 164 | ``` 165 | Note that Scala also allows you to match a variable based on just a type. For example: 166 | ```scala 167 | someone match { 168 | case Customer(Name(first, last), partner: Partner) => func(first, last, part) 169 | } 170 | ``` 171 | In Decomat, you can do using the the `Is` function with a type and empty parameter-list. 172 | ```kotlin 173 | on(someone).match( 174 | case( Customer[Name[Is(), Is()], Is()] ) 175 | // Note how since we are not deconstructing the Partner class anymore, the 3rd parameter 176 | // switches from the `id: Int` type to the `partner: Partner` type. 177 | .then { first, last, partner /*: Partner*/ -> func(first, last, partner) }, 178 | // Other cases... 179 | ) 180 | ``` 181 | 182 | There are several other methods provided for pattern-matching convenience. 183 | 184 | ##### thenIf 185 | 186 | The `thenIf` method allows you to perform a transformation only if the predicate is true. This is similar 187 | to adding a `if` clause to a Scala pattern-match case. For example: 188 | 189 | ```scala 190 | someone match { 191 | case Customer(Name(first, last), Partner(id)) if (first == "Joe") => func(first, last, id) 192 | ... 193 | } 194 | ``` 195 | In Decomat, this would be done as follows: 196 | ```kotlin 197 | on(someone).match( 198 | case( Customer[Name[Is(), Is()], Partner(Is())] ) 199 | .thenIf { first, last, id -> first == "Joe" } 200 | .then { first, last, id -> func(first, last, id) }, 201 | // Other cases... 202 | ) 203 | ``` 204 | 205 | ##### thenIfThis 206 | 207 | If you want to filter by a non `@Component` annoated field, you can use the `thenIfThis` method. 208 | This method allows you to use the pattern-matched object as a reciever. For example: 209 | 210 | ```kotlin 211 | @Matchable 212 | data class Customer(val something: String, @Component val name: Name, @Component val affiliate: Affiliate) { ... } 213 | 214 | on(something).match( 215 | case( Customer[Name[Is(), Is()], Partner(Is())] ) 216 | .thenIfThis {{ first, last, id -> 217 | // Note that the first, last, and id properties are available here but you do not necessarily need to use them, 218 | // since you can use the `this` keyword to refer to the `Customer` instance (also `this` can be omitted entirely). 219 | something == "something" 220 | }} 221 | .then { name, affiliate -> func(name, affiliate) }, 222 | // Other cases... 223 | ) 224 | ``` 225 | Here we are using the `Customer` class as a reciever in the `thenIfThis` class above. The properties 226 | `first`, `last`, and `id` are also available to use if you need them. 227 | 228 | > The actual signature of `thenIfThis` is: 229 | > ```kotlin 230 | > R.() -> (component1, component2, ...) -> Boolean 231 | > ``` 232 | > That is why the double braches `{{ ... }}` are needed. 233 | 234 | ##### thenThis 235 | 236 | If you want to use any fields of the pattern-matched object that are not one of the components, you can use the `thenThis` method. 237 | This method allows you to use the pattern-matched object as a reciever. For example: 238 | 239 | ```kotlin 240 | @Matchable 241 | data class Customer(val something: String, @Component val name: Name, @Component val affiliate: Affiliate) { 242 | val somethingElse = "somethingElse" 243 | ... 244 | } 245 | 246 | on(something).match( 247 | case( Customer[Name[Is(), Is()], Partner(Is())] ) 248 | .thenThis { first, last, id -> 249 | // You can use the `this` keyword to refer to the `Customer` instance (also `this` can be omitted entirely). 250 | // (the components first, last, id are also available here for convenience) 251 | this.something + this.somethingElse 252 | } 253 | // Other cases... 254 | ) 255 | ``` 256 | 257 | ##### Is 258 | 259 | The `Is(...)` pattern is use to match the innermost patterns. It can be use to match by value, or by type. 260 | 261 | Matching by value: 262 | ```kotlin 263 | ```kotlin 264 | // Match only when affiliate is a Partner with id 123 265 | on(something).match( 266 | case( Customer[..., Is(Partner(123))] ).then { ... }, 267 | // Other cases... 268 | ) 269 | ``` 270 | 271 | Matching by type: 272 | ```kotlin 273 | // Match only when affiliate is of the type: Partner 274 | on(something).match( 275 | case( Customer[..., Is] ).then { ... }, 276 | // Other cases... 277 | ) 278 | ``` 279 | 280 | It is also possible to use the `Is` pattern to match by a custom predicate. Use this for more complex pattern 281 | matching but be warned that it may be less performant than the other methods because the predicate does not inline. 282 | ```kotlin 283 | on(something).match( 284 | case( Customer[..., Is { p -> (p.id == 123 || p.id == 456) }] ).then { ... }, 285 | // Other cases... 286 | ) 287 | ``` 288 | 289 | You can also define custom Is-variants based on predicates for example: 290 | ```kotlin 291 | def isPartnerWithIds(vararg ids: Int) = IsIs { p -> p is Partner && ids.contains(p.id) } 292 | on(something).match( 293 | case( Customer[..., isPartnerWithIds(123, 456)] ).then { ... }, 294 | // Other cases... 295 | ) 296 | ``` 297 | 298 | Note that in many cases, you can use the `thenIf` method instead of the `Is { ... }` predicate function which 299 | does inline leading to better performance. 300 | ```kotlin 301 | on(something).match( 302 | case( Customer[..., Is() ) 303 | // This will be inlined 304 | .thenIf { _, aff -> aff is Partner && (aff.id == 123 || aff.id == 456) } 305 | .then { ... }, 306 | // Other cases... 307 | ) 308 | ``` 309 | 310 | ## Custom Patterns 311 | 312 | One extremely powerful feature of Scala pattern-matching is that one can use custom patterns in a composable manner. 313 | For example: 314 | ```scala 315 | // Create a Data model 316 | case class Person(name: Name, age: Int) 317 | sealed trait Name 318 | case class SimpleName(first: String, last: String) extends Name 319 | case class FullName(first: String, middle: String, last: String) extends Name 320 | 321 | // Create a custom pattern 322 | object FirstLast { 323 | def unapply(name: Name): Option[(String, String)] = name match { 324 | case SimpleName(first, last) => Some(first, last) 325 | case FullName(first, _, last) => Some(first, last) 326 | case _ => None 327 | } 328 | } 329 | 330 | // Now we can use the pattern to match and extract custom data 331 | val p: Person = ... 332 | p match { 333 | case Person(FirstLast("Joe", last), age) => ... 334 | } 335 | ``` 336 | Similarly, Decomat allows you to create custom patterns. For example: 337 | ```kotlin 338 | // First Create our data model 339 | @Matchable 340 | data class Person(@Component val name: Name, @Component val age: Int): HasProductClass { 341 | override val productComponents = ProductClass2(this, name, age) 342 | companion object { } 343 | } 344 | sealed interface Name 345 | data class SimpleName(val first: String, val last: String): Name 346 | data class FullName(val first: String, val middle: String, val last: String): Name 347 | 348 | // Then create our custom pattern matcher. Use the customPattern1 or customPattern2 functions to create the custom pattern. 349 | object FirstLast { 350 | operator fun get(first: Pattern0, last: Pattern0) = 351 | customPattern2(first, last) { it: Name -> 352 | when(it) { 353 | is SimpleName -> first.matches(it.first) && last.matches(it.last) 354 | is FullName -> first.matches(it.first) && last.matches(it.last) 355 | else -> false 356 | } 357 | } 358 | } 359 | 360 | // Then use the `FirstLast` custom pattern to match and extract data 361 | val p: Person = ... 362 | val out = 363 | on(p).match( 364 | case(Person[FirstLast[Is("Joe"), Is()], Is()]).then { (first, last), age -> ... } 365 | ) 366 | ``` 367 | 368 | 369 | 370 | Note that when Scala pattern matching clauses get complex, it is common to use pattern matching itself in order to deconstruct 371 | patterns into smaller patterns. That means that if we make `SimpleName` and `FullName` matchable, we can 372 | use them with Decomat's matching instead of Kotlin `when` statement. This gives us more versatility. 373 | For example: 374 | ```kotlin 375 | // Annotate SimpleName and FullName as @Matchable in additionl to `Person` 376 | @Matchable 377 | data class Person(@Component val name: Name, @Component val age: Int): HasProductClass { 378 | override val productComponents = ProductClass2(this, name, age) 379 | companion object { } 380 | } 381 | sealed interface Name 382 | @Matchable 383 | data class SimpleName(@Component val first: String, @Component val last: String): Name, HasProductClass { 384 | override val productComponents = ProductClass2(this, first, last) 385 | companion object { } 386 | } 387 | @Matchable 388 | data class FullName(@Component val first: String, val middle: String, @Component val last: String): Name, HasProductClass { 389 | override val productComponents = ProductClass3(this, first, middle, last) 390 | companion object { } 391 | } 392 | 393 | // Then create our custom pattern matcher which itself uses on/match functions: 394 | object FirstLast { 395 | operator fun get(first: Pattern0, last: Pattern0) = 396 | customPattern2(first, last) { it: Name -> 397 | on(it).match( 398 | case(FullName[Is(), Is()]).then { first, last -> Components2(first, last) }, 399 | case(SimpleName[Is(), Is()]).then { first, last -> Components2(first, last) } 400 | ) 401 | } 402 | } 403 | 404 | // Then use the `FirstLast` custom pattern to match and extract data the same as before... 405 | val p: Person = ... 406 | val out = 407 | on(p).match( 408 | case(Person[FirstLast[Is("Joe"), Is()], Is()]).then { (first, last), age -> ... } 409 | ) 410 | ``` 411 | 412 | This latter approach is particularly useful when you want the custom pattern matching function itself to have 413 | complex nested conditional logic. For example: 414 | ```kotlin 415 | // Match all full-names where the first-name is "Joe" or "Jack" 416 | // Match all simple-names where the last-name is "Bloggs" and "Roogs" 417 | object FirstLast { 418 | operator fun get(first: Pattern0, last: Pattern0) = 419 | customPattern2(first, last) { it: Name -> 420 | on(it).match( 421 | case(FullName[Is { it == "Joe" || it == "Jack" }, Is()]) 422 | .then { first, last -> Components2(first, last) }, 423 | case(SimpleName[Is(), Is { it == "Bloggs" || it == "Roogs" }]) 424 | .then { first, last -> Components2(first, last) } 425 | ) 426 | } 427 | } 428 | ``` 429 | 430 | ## ADTs with Type Parameters (i.e. GADTs) 431 | 432 | Decomat supports ADTs with type parameters but they are not used in the Pattern-components. Instead, 433 | they are converted into start-projections. This is because typing all of the parameters would make the 434 | matching highly restrictive. (Also, type-parameters cannot be used as part of the pattern-matching 435 | due to type-erasure.) 436 | 437 | For example: 438 | ```kotlin 439 | @Metadata 440 | sealed interface Query 441 | data class Map(@Component val head: Query, @Component val body: Query): Query { 442 | // ... 443 | } 444 | data class Entity(@Component val value: T): Query { 445 | fun someField(getter: () -> R): Query = Property(this, getter()) 446 | // ... 447 | } 448 | ``` 449 | 450 | The `Query` interface must be up-casted into into a star-projection when it is used in a match. 451 | ```kotlin 452 | val query: Query = ... 453 | on(query as Query<*>).match( 454 | case( Map[Is(), Is()] ) 455 | .then { head: Query<*>, body: Query<*> -> func(head, body) }, 456 | case( Entity[Is()] ) 457 | .then { value: Entity<*> -> func(value) }, 458 | // Other cases... 459 | ) 460 | ``` 461 | Note how the `head` and `body` elements are star projections instead of the origin types? 462 | This is done so that the `Map` case can match any `Query` type, otherwise the matching logic would be too restrictive. 463 | (E.g. it would be difficult to deduce the type of the `head` and `body` elements causing the generated code to be incorrect) 464 | 465 | If you want to experiment with fully-typed ADT-components nonetheless, use `@Matchable(simplifyTypes = false)`. 466 | 467 | ## Changing the Annotation Name 468 | 469 | Kotlin allows changing an import name using the `import ... as ...` syntax. This can be used to change the 470 | `@Matchable` annotation name to something else, however due to issue [#783](https://github.com/google/ksp/issues/783) it is not possible to genenerically 471 | detect this change inside of a KSP processor. Therefore, if you change the annotation name, you must also 472 | add the following setting to your `build.gradle.kts` file: 473 | ```kotlin 474 | // build.gradle.kts 475 | ksp { 476 | arg("matchableName", "Mat") 477 | arg("componentName", "Slot") 478 | } 479 | ``` 480 | Then rename the `@Matchable` annotation to `@Mat` and the `@Component` annotation to `@Slot` 481 | in the import: 482 | ```kotlin 483 | import io.decomat.Matchable as Mat 484 | import io.decomat.Component as Slot 485 | 486 | // Then use the annotations as follows: 487 | @Mat 488 | data class Person(@Slot val firstName: String, @Slot val lastName: String) { 489 | override val productComponents = productComponentsOf(this, firstName, lastName) 490 | companion object {} 491 | } 492 | ``` 493 | 494 | 495 | -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.gradle.api.tasks.testing.logging.TestExceptionFormat 2 | import org.gradle.api.tasks.testing.logging.TestLogEvent 3 | 4 | plugins { 5 | `maven-publish` 6 | signing 7 | kotlin("multiplatform") version "1.9.22" apply false 8 | id("io.github.gradle-nexus.publish-plugin") version "1.1.0" 9 | id("org.jetbrains.dokka") version "1.9.20" 10 | } 11 | 12 | allprojects { 13 | group = "io.exoquery" 14 | version = "0.6.0" 15 | 16 | //val varintName = project.name 17 | 18 | apply { 19 | plugin("org.jetbrains.dokka") 20 | plugin("maven-publish") 21 | plugin("signing") 22 | } 23 | 24 | repositories { 25 | mavenCentral() 26 | maven(url = "https://plugins.gradle.org/m2/") 27 | maven(url = "https://jitpack.io") 28 | } 29 | 30 | val dokkaHtml by tasks.getting(org.jetbrains.dokka.gradle.DokkaTask::class) 31 | val javadocJar: TaskProvider by tasks.registering(Jar::class) { 32 | dependsOn(dokkaHtml) 33 | archiveClassifier.set("javadoc") 34 | from(dokkaHtml.outputDirectory) 35 | } 36 | 37 | publishing { 38 | val user = System.getenv("SONATYPE_USERNAME") 39 | val pass = System.getenv("SONATYPE_PASSWORD") 40 | 41 | repositories { 42 | maven { 43 | name = "Oss" 44 | setUrl { 45 | val repositoryId = System.getenv("SONATYPE_REPOSITORY_ID") ?: error("Missing env variable: SONATYPE_REPOSITORY_ID") 46 | "https://s01.oss.sonatype.org/service/local/staging/deployByRepositoryId/$repositoryId/" 47 | } 48 | credentials { 49 | username = user 50 | password = pass 51 | } 52 | } 53 | maven { 54 | name = "Snapshot" 55 | setUrl { "https://s01.oss.sonatype.org/content/repositories/snapshots/" } 56 | credentials { 57 | username = user 58 | password = pass 59 | } 60 | } 61 | } 62 | 63 | publications.withType { 64 | artifact(javadocJar) 65 | 66 | pom { 67 | name.set("decomat") 68 | description.set("DecoMat - Deconstructive Pattern Matching for Kotlin") 69 | url.set("https://github.com/exoquery/decomat") 70 | 71 | licenses { 72 | license { 73 | name.set("The Apache Software License, Version 2.0") 74 | url.set("http://www.apache.org/licenses/LICENSE-2.0.txt") 75 | distribution.set("repo") 76 | } 77 | } 78 | 79 | developers { 80 | developer { 81 | name.set("Alexander Ioffe") 82 | email.set("deusaquilus@gmail.com") 83 | organization.set("github") 84 | organizationUrl.set("http://www.github.com") 85 | } 86 | } 87 | 88 | scm { 89 | url.set("https://github.com/exoquery/decomat/tree/main") 90 | connection.set("scm:git:git://github.com/ExoQuery/DecoMat.git") 91 | developerConnection.set("scm:git:ssh://github.com:ExoQuery/DecoMat.git") 92 | } 93 | } 94 | } 95 | } 96 | 97 | val isCI = project.hasProperty("isCI") 98 | val isLocal = !isCI 99 | val noSign = project.hasProperty("nosign") 100 | val doNotSign = isLocal || noSign 101 | 102 | signing { 103 | // Sign if we're not doing a local build and we haven't specifically disabled it 104 | if (!doNotSign) { 105 | val signingKeyRaw = System.getenv("NEW_SIGNING_KEY_ID_BASE64") 106 | if (signingKeyRaw == null) error("ERROR: No Signing Key Found") 107 | // Seems like the right way was to have newlines after all the exported (ascii armored) lines 108 | // and you can put them into the github-var with newlines but if you 109 | // include the "-----BEGIN PGP PRIVATE KEY BLOCK-----" and "-----END PGP PRIVATE KEY BLOCK-----" 110 | // parts with that then errors happen. Have a look at https://github.com/gradle/gradle/issues/15718 for more detail 111 | // Ultimately however `iurysza` is only partially correct and they key-itself does not need to be escaped 112 | // and can be put into a github-var with newlines. 113 | val signingKey = 114 | "-----BEGIN PGP PRIVATE KEY BLOCK-----\n\n${signingKeyRaw}\n-----END PGP PRIVATE KEY BLOCK-----" 115 | useInMemoryPgpKeys( 116 | System.getenv("NEW_SIGNING_KEY_ID_BASE64_ID"), 117 | signingKey, 118 | System.getenv("NEW_SIGNING_KEY_ID_BASE64_PASS") 119 | ) 120 | sign(publishing.publications) 121 | } 122 | } 123 | 124 | // Fix for Kotlin issue: https://youtrack.jetbrains.com/issue/KT-61313 125 | tasks.withType().configureEach { 126 | val pubName = name.removePrefix("sign").removeSuffix("Publication") 127 | 128 | // These tasks only exist for native targets, hence findByName() to avoid trying to find them for other targets 129 | 130 | // Task ':linkDebugTest' uses this output of task ':signPublication' without declaring an explicit or implicit dependency 131 | tasks.findByName("linkDebugTest$pubName")?.let { 132 | mustRunAfter(it) 133 | } 134 | // Task ':compileTestKotlin' uses this output of task ':signPublication' without declaring an explicit or implicit dependency 135 | tasks.findByName("compileTestKotlin$pubName")?.let { 136 | mustRunAfter(it) 137 | } 138 | } 139 | 140 | // Was having odd issues happening in CI releases like this: 141 | // e.g. Task ':pprint-kotlin-core:publishPublicationToOssRepository' uses this output of task ':pprint-kotlin-core:signPublication' without declaring an explicit or implicit dependency. 142 | // I tried a few things that caused other issues. Ultimately the working solution I got from here: 143 | // https://github.com/gradle/gradle/issues/26091#issuecomment-1722947958 144 | tasks.withType().configureEach { 145 | val signingTasks = tasks.withType() 146 | mustRunAfter(signingTasks) 147 | 148 | // Also, do not publish the decomat-examples project 149 | onlyIf { 150 | !this.project.name.contains("decomat-examples") 151 | } 152 | } 153 | } 154 | 155 | 156 | tasks.register("publishLinuxLocal") { 157 | dependsOn( 158 | ":${Release.Project.`decomat-core`}:publishToMavenLocal", 159 | ":${Release.Project.`decomat-ksp`}:publishToMavenLocal" 160 | ) 161 | } 162 | 163 | 164 | object Release { 165 | 166 | object Project { 167 | val `decomat-core` = "decomat-core" 168 | val `decomat-ksp` = "decomat-ksp" 169 | } 170 | 171 | val macBuildCommands = 172 | listOf( 173 | "iosX64", 174 | "iosArm64", 175 | "tvosX64", 176 | "tvosArm64", 177 | "watchosX64", 178 | "watchosArm32", 179 | "watchosArm64", 180 | "macosX64", 181 | "macosArm64", 182 | "iosSimulatorArm64" 183 | ).map { "publish${it.capitalize()}PublicationToOssRepository" } 184 | 185 | val windowsBuildCommands = 186 | listOf( 187 | "mingwX64" 188 | ).map { "publish${it.capitalize()}PublicationToOssRepository" } 189 | 190 | fun String.capitalize() = this.replaceFirstChar { it.uppercase() } 191 | } 192 | -------------------------------------------------------------------------------- /decomat-core/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import dev.anies.gradle.template.TemplateTask 2 | import java.io.OutputStreamWriter 3 | import java.io.File 4 | import java.io.FileWriter 5 | import java.io.IOException 6 | import freemarker.template.TemplateDirectiveModel 7 | import freemarker.template.TemplateException 8 | import freemarker.core.Environment 9 | import freemarker.template.Configuration 10 | import freemarker.template.TemplateDirectiveBody 11 | import freemarker.template.TemplateModel 12 | import freemarker.template.SimpleScalar 13 | import freemarker.template.Template 14 | import org.gradle.api.tasks.testing.logging.TestExceptionFormat 15 | import org.gradle.api.tasks.testing.logging.TestLogEvent 16 | import org.gradle.internal.impldep.org.apache.commons.io.output.ByteArrayOutputStream 17 | import org.jetbrains.kotlin.gradle.targets.js.dsl.ExperimentalWasmDsl 18 | import org.jetbrains.kotlin.gradle.tasks.KotlinCompile 19 | import org.jetbrains.kotlin.gradle.tasks.KotlinNativeCompile 20 | import java.io.Writer 21 | import java.nio.charset.Charset 22 | 23 | plugins { 24 | kotlin("multiplatform") 25 | id("dev.anies.gradle.template") 26 | signing 27 | } 28 | 29 | kotlin { 30 | val isCI = project.hasProperty("isCI") 31 | val platform = 32 | if (project.hasProperty("platform")) 33 | project.property("platform") 34 | else 35 | "any" 36 | val isLinux = platform == "linux" 37 | val isMac = platform == "mac" 38 | val isWindows = platform == "windows" 39 | 40 | jvm { 41 | jvmToolchain(11) 42 | } 43 | 44 | if(!isCI) { 45 | js { 46 | browser() 47 | nodejs() 48 | } 49 | 50 | linuxX64() 51 | macosX64() 52 | mingwX64() 53 | } 54 | 55 | // If we are a CI, build all the targets for the specified platform 56 | if (isLinux && isCI) { 57 | js { 58 | browser() 59 | nodejs() 60 | } 61 | 62 | linuxX64() 63 | linuxArm64() 64 | 65 | @OptIn(ExperimentalWasmDsl::class) 66 | wasmWasi() 67 | @OptIn(ExperimentalWasmDsl::class) 68 | wasmJs() 69 | 70 | androidNativeX64() 71 | androidNativeX86() 72 | androidNativeArm32() 73 | androidNativeArm64() 74 | 75 | // Need to know about this since we publish the -tooling metadata from 76 | // the linux containers. Although it doesn't build these it needs to know about them. 77 | macosX64() 78 | macosArm64() 79 | iosX64() 80 | iosArm64() 81 | iosSimulatorArm64() 82 | tvosX64() 83 | tvosArm64() 84 | watchosX64() 85 | watchosArm32() 86 | watchosArm64() 87 | 88 | mingwX64() 89 | } 90 | 91 | if (isMac && isCI) { 92 | macosX64() 93 | macosArm64() 94 | iosX64() 95 | iosArm64() 96 | iosSimulatorArm64() 97 | tvosX64() 98 | tvosArm64() 99 | watchosX64() 100 | watchosArm32() 101 | watchosArm64() 102 | } 103 | if (isWindows && isCI) { 104 | mingwX64() 105 | } 106 | 107 | 108 | sourceSets { 109 | commonMain { 110 | kotlin.srcDir("$buildDir/templates/") 111 | dependencies { 112 | } 113 | } 114 | 115 | commonTest { 116 | // Including this twice actually seems to introduce build breakages in ThenTest 117 | // which thinks that `case(Pattern2...)` doesn't exist and tries to use .case from Comparator 118 | //kotlin.srcDir("$buildDir/templates/") 119 | dependencies { 120 | implementation(kotlin("test")) 121 | implementation(kotlin("test-common")) 122 | implementation(kotlin("test-annotations-common")) 123 | } 124 | } 125 | } 126 | } 127 | 128 | tasks.withType().configureEach { 129 | testLogging { 130 | showStandardStreams = true 131 | showExceptions = true 132 | exceptionFormat = TestExceptionFormat.SHORT 133 | events(TestLogEvent.STARTED, TestLogEvent.PASSED, TestLogEvent.SKIPPED, TestLogEvent.FAILED) 134 | } 135 | } 136 | 137 | tasks.register("template_base", TemplateTask::class) { 138 | data = mutableMapOf("key" to "value") 139 | from("src/templates/") 140 | into("build/templates/io/decomat") 141 | } 142 | 143 | val runFreemarkerTemplate by tasks.registering { 144 | doLast { 145 | val cfg = Configuration(Configuration.VERSION_2_3_0) 146 | cfg.setDefaultEncoding("UTF-8") 147 | 148 | // if 'decomat-core/src/templates/' output doesn't exist, create it 149 | val created = File(projectDir, "build/templates/io/decomat").mkdirs() 150 | //println("----- Creating dirs: build/templates/io/decomat: ${created} ------") 151 | 152 | println("----- Creating Template -----") 153 | val template = Template("", File(projectDir, "src/templates/Pattern3.ftl").readText(), cfg) 154 | val root = 155 | HashMap() 156 | .apply { 157 | put("output", OutputDirective()) 158 | //put("model", model.encode()) 159 | } 160 | 161 | println("----- Creating Sink: build/templates/Pattern3.ftl.gen -----") 162 | val file = File(projectDir, "build/templates/Pattern3.ftl.gen") 163 | file.createNewFile() 164 | val os = file.outputStream() 165 | 166 | val out: Writer = OutputStreamWriter(os) 167 | 168 | println("----- Executing Template -----") 169 | template.process(root, out) 170 | out.flush() 171 | os.close() 172 | } 173 | } 174 | 175 | 176 | tasks.withType { 177 | dependsOn(runFreemarkerTemplate) 178 | } 179 | 180 | tasks.withType { 181 | dependsOn(runFreemarkerTemplate) 182 | } 183 | 184 | // THIS is the actual task used by the KMP multiplatform plugin. Without this, 185 | // the freemarker dependency won't run in time for the dependencies to pick it up. 186 | // That means that a command like `./gradlew clean build` for the base-project will fail. 187 | tasks.withType> { 188 | dependsOn(runFreemarkerTemplate) 189 | } 190 | 191 | class OutputDirective : TemplateDirectiveModel { 192 | @Throws(TemplateException::class, IOException::class) 193 | override fun execute( 194 | env: Environment, 195 | params: Map<*, *>, 196 | loopVars: Array, 197 | body: TemplateDirectiveBody 198 | ) { 199 | val file: SimpleScalar? = params["file"] as SimpleScalar? 200 | val fw = FileWriter(File(file.toString())) 201 | body.render(fw) 202 | fw.flush() 203 | } 204 | } 205 | -------------------------------------------------------------------------------- /decomat-core/src/commonMain/kotlin/io/decomat/Annotations.kt: -------------------------------------------------------------------------------- 1 | package io.decomat 2 | 3 | @Retention(AnnotationRetention.SOURCE) 4 | @Target(AnnotationTarget.CLASS) 5 | annotation class Matchable(val simplifyTypes: Boolean = false) 6 | 7 | @Retention(AnnotationRetention.SOURCE) 8 | @Target(AnnotationTarget.VALUE_PARAMETER) 9 | annotation class Component 10 | 11 | @Retention(AnnotationRetention.SOURCE) 12 | @Target(AnnotationTarget.VALUE_PARAMETER) 13 | annotation class MiddleComponent 14 | 15 | @Retention(AnnotationRetention.SOURCE) 16 | @Target(AnnotationTarget.VALUE_PARAMETER) 17 | annotation class ConstructorComponent 18 | -------------------------------------------------------------------------------- /decomat-core/src/commonMain/kotlin/io/decomat/ContextComponents.kt: -------------------------------------------------------------------------------- 1 | package io.decomat 2 | 3 | data class ContextComponents1(val compInner: A, val comp: R) 4 | data class ContextComponents1L(val compLeft: A, val comp: R) 5 | data class ContextComponents1R(val compRight: A, val comp: R) 6 | data class ContextComponents2(val compLeft: A, val compRight: B, val comp: R) 7 | object ContextComponents { 8 | fun of(a: A, r: R) = ContextComponents1(a, r) 9 | fun ofLeft(a: A, r: R) = ContextComponents1L(a, r) 10 | fun ofRight(a: A, r: R) = ContextComponents1R(a, r) 11 | fun of(a: A, b: B, r: R) = ContextComponents2(a, b, r) 12 | } 13 | -------------------------------------------------------------------------------- /decomat-core/src/commonMain/kotlin/io/decomat/CustomPattern.kt: -------------------------------------------------------------------------------- 1 | package io.decomat 2 | 3 | import io.decomat.fail.* 4 | 5 | inline fun , R1, reified R> customPattern1( 6 | patternName: String, 7 | nested1: P1, 8 | noinline matchName: (R) -> Components1? 9 | ): Pattern1 = CustomPattern1(patternName, nested1, matchName, Typed(), { it is R }) 10 | 11 | inline fun , P2 : Pattern, R1, R2, reified R> customPattern2( 12 | patternName: String, 13 | nested1: P1, 14 | nested2: P2, 15 | noinline matchName: (R) -> Components2? 16 | ): Pattern2 = CustomPattern2(patternName, nested1, nested2, matchName, Typed(), { it is R }) 17 | 18 | inline fun , M, P2 : Pattern, R1, R2, reified R> customPattern2M( 19 | patternName: String, 20 | nested1: P1, 21 | nested2: P2, 22 | noinline matchName: (R) -> Components2M? 23 | ): Pattern2M = CustomPattern2M(patternName, nested1, nested2, matchName, Typed(), { it is R }) 24 | 25 | class CustomPattern1, R1, R>( 26 | val patternName: String, 27 | val innerMatch: P1, 28 | val match: (R) -> Components1?, 29 | val tpe: Typed, 30 | val typecheck: (Any) -> Boolean 31 | ): Pattern1(innerMatch, tpe) { 32 | override fun matches(comps: ProductClass): Boolean = 33 | typecheck(comps.productClassValueUntyped) && 34 | match(comps.productClassValue).let { 35 | it != null && innerMatch.matchesAny(it.a as Any) 36 | } 37 | 38 | override fun divideIntoComponents(instance: ProductClass): Components1 = 39 | match(instance.productClassValue) ?: failToDivide(patternName, instance) 40 | 41 | // The point of custom patterns is that you can you can defined them for arbitrary objects 42 | // if these aribtrary objects can be divided into components then they can be matched 43 | override fun divideIntoComponentsAny(instance: kotlin.Any): Components1 = 44 | if (typecheck(instance)) 45 | match(instance as R) ?: failToMatch(patternName, instance, tpe) 46 | else 47 | failToCheck(patternName, instance, tpe) 48 | } 49 | 50 | 51 | 52 | class CustomPattern2, P2: Pattern, R1, R2, R>( 53 | val patternName: String, 54 | val innerMatchA: P1, 55 | val innerMatchB: P2, 56 | val match: (R) -> Components2?, 57 | val tpe: Typed, 58 | val typecheck: (Any) -> Boolean 59 | ): Pattern2(innerMatchA, innerMatchB, tpe) { 60 | override fun matches(comps: ProductClass): Boolean = 61 | typecheck(comps.productClassValueUntyped) && 62 | match(comps.productClassValue).let { 63 | it != null && 64 | innerMatchA.matchesAny(it.a as Any) && 65 | innerMatchB.matchesAny(it.b as Any) 66 | } 67 | 68 | override fun divideIntoComponents(instance: ProductClass): Components2 = 69 | match(instance.productClassValue) ?: failToDivide(patternName, instance) 70 | 71 | override fun divideIntoComponentsAny(instance: kotlin.Any): Components2 = 72 | if (typecheck(instance)) 73 | match(instance as R) ?: failToMatch(patternName, instance, tpe) 74 | else 75 | failToDivide(patternName, instance) 76 | } 77 | 78 | 79 | 80 | class CustomPattern2M, M, P2: Pattern, R1, R2, R>( 81 | val patternName: String, 82 | val innerMatchA: P1, 83 | val innerMatchB: P2, 84 | val match: (R) -> Components2M?, 85 | val tpe: Typed, 86 | val typecheck: (Any) -> Boolean 87 | ): Pattern2M(innerMatchA, innerMatchB, tpe) { 88 | override fun matches(comps: ProductClass): Boolean = 89 | typecheck(comps.productClassValueUntyped) && 90 | match(comps.productClassValue).let { 91 | it != null && 92 | innerMatchA.matchesAny(it.a as Any) && 93 | innerMatchB.matchesAny(it.b as Any) 94 | } 95 | 96 | 97 | override fun divideInto3Components(instance: ProductClass): Components2M = 98 | match(instance.productClassValue) ?: failToDivide(patternName, instance) 99 | 100 | override fun divideInto3ComponentsAny(instance: kotlin.Any): Components2M = 101 | if (typecheck(instance)) 102 | match(instance as R) ?: failToMatch(patternName, instance, tpe) 103 | else 104 | failToDivide(patternName, instance) 105 | } 106 | 107 | 108 | -------------------------------------------------------------------------------- /decomat-core/src/commonMain/kotlin/io/decomat/Is.kt: -------------------------------------------------------------------------------- 1 | package io.decomat 2 | 3 | @Suppress("PARAMETER_NAME_CHANGED_ON_OVERRIDE") 4 | class Is private constructor (private val type: Typed, private val valueCompare: ValueCompare): Pattern0(type) { 5 | override fun matches(r: ProductClass): Boolean = 6 | type.typecheck(r.productClassValueUntyped) && when(valueCompare) { 7 | is DoCompare -> r.productClassValue == valueCompare.value 8 | is DoComparePredicate -> valueCompare.f(r.productClassValue) 9 | is DontCompare -> true 10 | } 11 | 12 | companion object { 13 | private sealed interface ValueCompare 14 | private data class DoCompare(val value: R): ValueCompare 15 | private data class DoComparePredicate(val f: (R) -> Boolean): ValueCompare 16 | private object DontCompare: ValueCompare 17 | 18 | fun TypedAs(type: Typed) = Is(type, DontCompare) 19 | fun ValuedAs(type: Typed, value: R) = Is(type, DoCompare(value)) 20 | fun PredicateAs(type: Typed, value: (R) -> Boolean) = Is(type, DoComparePredicate(value)) 21 | inline operator fun invoke(): Is = Is.TypedAs(Typed()) 22 | inline operator fun invoke(value: R): Is = Is.ValuedAs(Typed(), value) 23 | inline operator fun invoke(noinline f: (R) -> Boolean): Is = Is.PredicateAs(Typed(), f) 24 | } 25 | } 26 | 27 | inline fun IsAny() = Is() 28 | -------------------------------------------------------------------------------- /decomat-core/src/commonMain/kotlin/io/decomat/Matching.kt: -------------------------------------------------------------------------------- 1 | package io.decomat 2 | 3 | // TODO need to test this with null 4 | @Suppress("UNCHECKED_CAST") 5 | internal fun wrapNonComps(a: kotlin.Any?) = 6 | when(a) { 7 | is ProductClass<*> -> 8 | a as ProductClass // nested components 9 | else -> 10 | ProductClass0(a) as ProductClass // leaf level entity 11 | } 12 | 13 | 14 | 15 | fun M.match(vararg cases: Case): O? = 16 | cases.find { theCase -> theCase.matches(this as Any) }?.eval(this as Any) 17 | 18 | fun on(value: M): DoMatch = DoMatch(value) 19 | 20 | // TODO Have an else-clause 21 | class DoMatch(private val value: R) { 22 | /** Don't want to be strict about what types the case-has (i.e. R) since 23 | * if they don't match the 1st part `theCase.matches` will just return `false` and 24 | * evaluation will not proceed. Also, this is needed when we want to do 25 | * contravariant matches (i.e. the thing we're matching-on is more general 26 | * then the thing we are trying to match it to). 27 | */ 28 | fun match(vararg cases: Case): O? = 29 | cases.find { theCase -> theCase.matches(value as Any) }?.eval(value as Any) 30 | 31 | // fun matchAny(vararg cases: Case): O? = 32 | // cases.find { theCase -> theCase.matches(value as Any) }?.eval(value as Any) 33 | } 34 | 35 | 36 | interface Stage { 37 | val pat: P 38 | val check: (R) -> Boolean 39 | fun notRightCls(value: R): Nothing = throw IllegalArgumentException("The value $value was not a ProductClass") 40 | } 41 | 42 | 43 | interface Case { 44 | fun matches(value: @UnsafeVariance R): Boolean 45 | fun eval(value: @UnsafeVariance R): O 46 | fun evalSafe(value: @UnsafeVariance R): O? = 47 | if (!matches(value)) null 48 | else eval(value) 49 | } 50 | 51 | inline fun Case.matches(value: Any) = 52 | this.matches(value as? R ?: throw IllegalArgumentException("The value $value is not of type ${R::class}")) 53 | 54 | inline fun Case.eval(value: Any) = 55 | this.eval(value as? R ?: throw IllegalArgumentException("The value $value is not of type ${R::class}")) 56 | 57 | 58 | class StageCase private constructor ( 59 | private val pat: Pattern, 60 | private val check: (R) -> Boolean, 61 | private val evalCase: (R) -> O 62 | ): Case { 63 | // NOTE: The typing here is WRONG! `@UnsafeVariance R` has to be treated as `Any` which is indeed possible in this case. 64 | // that is why we need to do matchesAny FIRST because that actually does a typecheck and only then can we do check(value) 65 | override fun matches(value: @UnsafeVariance R): Boolean = 66 | pat.matchesAny(value as Any) && check(value) 67 | 68 | override fun eval(value: @UnsafeVariance R): O = evalCase(value) 69 | 70 | /** 71 | * The reason we introduce this companion constructor that generaalizes 72 | * StageCase to Case is because we 73 | * want to return Case here instead of StageCase 74 | * since it makes certain errors in pattern matching simpler to understand 75 | * e.g: something like this: 76 | * Required: 77 | * Case 78 | * Found: 79 | * StageCase 80 | * -- Misleads the user to to think that it's found StageCase instead of the required 'Case' 81 | * Instead the error now reads like this: 82 | * Required: 83 | * Case 84 | * Found: 85 | * Case 86 | * 87 | * It is noteworthy that this is a general pattern for product types in Scala (3), Haskell and 88 | * other languages. For example, in Scala 3, using a `enum case...` datatype will always 89 | * cause the constructor to instantiate the general type. 90 | */ 91 | companion object { 92 | operator fun invoke( 93 | pat: Pattern, 94 | check: (R) -> Boolean, 95 | evalCase: (R) -> O 96 | ): Case = 97 | StageCase(pat, check, evalCase) 98 | } 99 | } -------------------------------------------------------------------------------- /decomat-core/src/commonMain/kotlin/io/decomat/Pattern.kt: -------------------------------------------------------------------------------- 1 | package io.decomat 2 | 3 | import io.decomat.fail.fail 4 | import io.decomat.fail.failToDivide 5 | 6 | /** 7 | * Note that all the patterns need to be covariant, otherwise something like this: 8 | * `Pattern1, R1>, R>` won't be a subtype of something like: 9 | * `Pattern1, Name>, Person>` because Pattern0 10 | * won't be unified with `Pattern`. Therefore all inner-patterns need 11 | * to be covariant. 12 | */ 13 | sealed interface Pattern { 14 | val typeR: Typed 15 | fun matches(comps: ProductClass<@UnsafeVariance R>): Boolean 16 | @Suppress("UNCHECKED_CAST") 17 | fun matchesAny(comps: Any): Boolean = 18 | when(comps) { 19 | is ProductClass<*> -> { 20 | typeR.typecheck(comps.productClassValueUntyped) && matches(comps as ProductClass) 21 | } 22 | else -> { 23 | if (!typeR.typecheck(comps)) { 24 | false 25 | } else { 26 | val compsReal = ProductClass0(comps as R) 27 | // At this point we know that `comps` matches R so we can use compsReal as the ProductClass0 it is meant to be 28 | matches(compsReal) 29 | } 30 | } 31 | } 32 | } 33 | 34 | 35 | 36 | // TODO write a custom function with Pattern etc... that is equivalen of `unapply`. They need to be open classes 37 | abstract class Pattern0(override val typeR: Typed<@UnsafeVariance R>): Pattern { 38 | // `matches` function is delegated to implementors e.g. `Any` 39 | } 40 | 41 | abstract class Pattern1, out R1, out R>(val pattern1: P1, override val typeR: Typed<@UnsafeVariance R>): Pattern { 42 | override fun matches(comps: ProductClass<@UnsafeVariance R>) = 43 | // E.g. for Distinct.M(...): Pattern1<...> check that the thing we are trying to match as `Distinct` 44 | // is actually a `Distinct` instances 45 | typeR.typecheck(comps.productClassValueUntyped) && 46 | when(val compsDef = comps.isIfHas()) { 47 | is ProductClass1<*, *> -> 48 | wrapNonComps(compsDef.a).let { pattern1.matches(it) } 49 | else -> false 50 | } 51 | 52 | open fun divideIntoComponentsAny(instance: kotlin.Any): Components1<@UnsafeVariance R1> = 53 | when(instance) { 54 | is HasProductClass<*> -> 55 | divideIntoComponentsAny(instance.productComponents) 56 | is ProductClass1<*, *> -> 57 | if (!typeR.typecheck(instance.productClassValueUntyped)) fail("The type ${instance.productClassValueUntyped} has an unexpected return type") 58 | else divideIntoComponents(instance as ProductClass) 59 | else -> fail("Cannot divide $instance into components. It is not a Product1 class.") 60 | } 61 | 62 | open fun divideIntoComponents(instance: ProductClass<@UnsafeVariance R>): Components1<@UnsafeVariance R1> = 63 | when(val inst = instance.isIfHas()) { 64 | is ProductClass1<*, *> -> Components1(inst.a as R1) 65 | else -> fail("must match properly") // todo refine message 66 | } 67 | } 68 | 69 | // TODO Describe how Comp2 turns into Comp0 etc... for the FlatMap case 70 | abstract class Pattern2, out P2: Pattern, out R1, out R2, out R>(val pattern1: P1, val pattern2: P2, override val typeR: Typed<@UnsafeVariance R>): 71 | Pattern { 72 | open override fun matches(instance: ProductClass<@UnsafeVariance R>) = 73 | when(val inst = instance.isIfHas()) { 74 | is ProductClass2<*, *, *> -> 75 | typeR.typecheck(instance.productClassValueUntyped) && 76 | wrapNonComps(inst.a).let { pattern1.matches(it) } && 77 | wrapNonComps(inst.b).let { pattern2.matches(it) } 78 | else -> false 79 | } 80 | 81 | // assumign the matches function already said 'false' if it doesn't match so at this point just throw an error 82 | open fun divideIntoComponentsAny(instance: kotlin.Any): Components2<@UnsafeVariance R1, @UnsafeVariance R2> = 83 | when(instance) { 84 | is HasProductClass<*> -> 85 | divideIntoComponentsAny(instance.productComponents) 86 | is ProductClass2<*, *, *> -> 87 | if (!typeR.typecheck(instance.productClassValueUntyped)) fail("Invalid type of data. ${instance.productClassValueUntyped} is not a ${typeR.cls.simpleName}") 88 | else divideIntoComponents(instance as ProductClass) 89 | else -> fail("Cannot divide $instance into components. It is not a Product2 class.") 90 | } 91 | 92 | open fun divideIntoComponents(instance: ProductClass<@UnsafeVariance R>): Components2<@UnsafeVariance R1, @UnsafeVariance R2> = 93 | when(val inst = instance.isIfHas()) { 94 | is ProductClass2<*, *, *> -> 95 | // for FlatMap_M: ~Pattern2 R1 and R2 will be Query 96 | // (i wrote ~Pattern2<...> because it's HasProductComponents) 97 | Components2(inst.a as R1, inst.b as R2) 98 | else -> fail("must match properly") // todo refine message 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /decomat-core/src/commonMain/kotlin/io/decomat/PatternM.kt: -------------------------------------------------------------------------------- 1 | package io.decomat 2 | 3 | import io.decomat.fail.fail 4 | 5 | // TODO Describe how Comp2 turns into Comp0 etc... for the FlatMap case 6 | abstract class Pattern2M, M, P2: Pattern, R1, R2, R>(val pattern1: P1, val pattern2: P2, override val typeR: Typed): 7 | Pattern { 8 | open override fun matches(instance: ProductClass) = 9 | when(val inst = instance.isIfHas()) { 10 | is ProductClass2M<*, *, *, *> -> 11 | typeR.typecheck(instance.productClassValueUntyped) && 12 | wrapNonComps(inst.a).let { pattern1.matches(it) } && 13 | wrapNonComps(inst.b).let { pattern2.matches(it) } 14 | else -> false 15 | } 16 | 17 | // use 3-component division in then then-functions for Pattern2M-family but 18 | // use the 2-component division in the superclass of this in the Pattenr2M family 19 | // (the fact that we do this allows us to reuse then thenXX methods for the Pattern2M family 20 | // otherwise would would need to reimplement all of them for Pattern2M giving it the performance 21 | // probem of Pattern3) 22 | open fun divideInto3ComponentsAny(instance: kotlin.Any): Components2M = 23 | when(instance) { 24 | is HasProductClass<*> -> 25 | divideInto3ComponentsAny(instance.productComponents) 26 | is ProductClass2M<*, *, *, *> -> 27 | if (!typeR.typecheck(instance.productClassValueUntyped)) fail("Invalid type of data. ${instance.productClassValueUntyped} is not a ${typeR.cls.simpleName}") 28 | else divideInto3Components(instance as ProductClass) 29 | else -> fail("Cannot divide $instance into components. It is not a Product2 class.") 30 | } 31 | 32 | open fun divideInto3Components(instance: ProductClass): Components2M = 33 | when(val inst = instance.isIfHas()) { 34 | is ProductClass2M<*, *, *, *> -> 35 | // for FlatMap_M: ~Pattern2 R1 and R2 will be Query 36 | // (i wrote ~Pattern2<...> because it's HasProductComponents) 37 | Components2M(inst.a as R1, inst.m as M, inst.b as R2) 38 | else -> fail("must match properly") // todo refine message 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /decomat-core/src/commonMain/kotlin/io/decomat/ProductClass.kt: -------------------------------------------------------------------------------- 1 | package io.decomat 2 | 3 | sealed interface ProductClass { 4 | // Normally we want the ProductClass element to go in as typed so implementors should 5 | // primarily use `productClassValue` passing it into the constructor. However 6 | // for situations where we do not know that the product class meets the type-description 7 | // for certain (e.g. CustomPattern) it is necessary to use the untyped variation. 8 | val productClassValueUntyped: Any 9 | val productClassValue: T 10 | fun isIfHas() = 11 | when(val thisComp = this) { 12 | is HasProductClass -> thisComp.productComponents 13 | else -> this 14 | } 15 | } 16 | 17 | // Does not seem to work, no annotations are found 18 | //@Suppress("UNCHECKED_CAST") 19 | //interface HasProductClassAuto: HasProductClass { 20 | // override val productComponents: ProductClass get() = 21 | // cache.computeIfAbsent(this) { _doInit() } as ProductClass 22 | // 23 | // fun _doInit(): ProductClass { 24 | // val cls = this::class 25 | // val ctor = cls.primaryConstructor ?: fail("No primary constructor found in the class ${this}") 26 | // println(cls.memberProperties.map { "${it.name} (${it.annotations})" }) 27 | // val componentNames = ctor.parameters.filter { it.annotations.any { anno -> anno.annotationClass.qualifiedName == "io.decomat.Component" } }.map { it.name } 28 | // if (componentNames.isEmpty()) fail("No components annotated with @Component found in the class ${this}. Found components: ${ctor.parameters.map { it.name }}.") 29 | // val components: List, *>> = 30 | // cls.memberProperties.filter { componentNames.contains(it.name) } 31 | // if (components.size != componentNames.size) 32 | // fail("Not all the parameters with @Component annoations (${componentNames.joinToString(", ")}) were found to be components (${components.joinToString { "," }})") 33 | // 34 | // fun comp(i: Int) = components[i].getter.call(this) 35 | // 36 | // val productClass = 37 | // when(components.size) { 38 | // 1 -> ProductClass1(this as T, comp(0)) 39 | // 2 -> ProductClass2(this as T, comp(0), comp(1)) 40 | // 3 -> ProductClass3(this as T, comp(0), comp(1), comp(2)) 41 | // else -> fail("Num components needs to be 1, 2, or 3 but was: ${components.size}") 42 | // } 43 | // 44 | // return productClass 45 | // } 46 | // 47 | // // Sigh, need to use a global cache to compute product-classes we've since since you can't assign 48 | // // values directly to varaibles inside of kotlin interfaces 49 | // companion object { 50 | // val cache = WeakHashMap, ProductClass<*>>() 51 | // } 52 | //} 53 | 54 | interface HasProductClass: ProductClass { 55 | val productComponents: ProductClass 56 | override val productClassValueUntyped: Any get() = productComponents.productClassValue as Any 57 | override val productClassValue get() = productComponents.productClassValue 58 | } 59 | 60 | fun productComponentsOf(host: T) = ProductClass0(host) 61 | fun productComponentsOf(host: T, componentA: A) = ProductClass1(host, componentA) 62 | fun productComponentsOf(host: T, componentA: A, componentB: B) = ProductClass2(host, componentA, componentB) 63 | fun productComponentsOf(host: T, componentA: A, componentM: M, componentB: B) = ProductClass2M(host, componentA, componentM, componentB) 64 | 65 | data class ProductClass0(override val productClassValue: T): ProductClass { 66 | override val productClassValueUntyped: Any = productClassValue as Any 67 | } 68 | data class ProductClass1(override val productClassValue: T, val a: A): ProductClass { 69 | override val productClassValueUntyped: Any = productClassValue as Any 70 | } 71 | data class ProductClass2(override val productClassValue: T, val a: A, val b: B): ProductClass { 72 | val matchComp get(): Components2 = Components2(a, b) 73 | override val productClassValueUntyped: Any = productClassValue as Any 74 | } 75 | 76 | data class ProductClass2M(override val productClassValue: T, val a: A, val m: M, val b: B): ProductClass { 77 | val matchComp get(): Components2M = Components2M(a, m, b) 78 | override val productClassValueUntyped: Any = productClassValue as Any 79 | } 80 | 81 | /** I think in order to avoid nastiness in kapshot experiments with hard-typing this has to be 82 | * covariant, need to test! 83 | */ 84 | 85 | 86 | sealed interface Components 87 | data class Components1(val a: @UnsafeVariance A): Components 88 | // For example: data class FlatMap(head: Query, body: Query) extends Comp2 89 | data class Components2(val a: @UnsafeVariance A, val b: @UnsafeVariance B): Components 90 | 91 | data class Components2M(val a: @UnsafeVariance A, val m: @UnsafeVariance M, val b: @UnsafeVariance B): Components 92 | -------------------------------------------------------------------------------- /decomat-core/src/commonMain/kotlin/io/decomat/ThenPattern1.kt: -------------------------------------------------------------------------------- 1 | package io.decomat 2 | 3 | fun case(pat: Pattern0) = ThenIs(pat, {true}) 4 | 5 | data class PrematchCase(val prematch: Boolean) { 6 | inline operator fun invoke(pat: Pattern0) = ThenIs(pat, {prematch}) 7 | inline operator fun invoke(pat: Pattern1, R1, R>) = Then0(pat, {prematch}) 8 | inline operator fun invoke(pat: Pattern1, R11, R1>, R1, R>) = Then1(pat, {prematch}) 9 | inline operator fun invoke(pat: Pattern1, Pattern, R11, R12, R1>, R1, R>) = Then2(pat, {prematch}) 10 | inline operator fun invoke(pat: Pattern1, M1, Pattern, R11, R12, R1>, R1, R>) = Then2M(pat, {prematch}) 11 | 12 | inline operator fun invoke(pat: Pattern2, Pattern0, R1, R2, R>) = Then00(pat, {prematch}) 13 | inline operator fun invoke(pat: Pattern2, Pattern1, R21, R2>, R1, R2, R>) = Then01(pat, {prematch}) 14 | inline operator fun invoke(pat: Pattern2, Pattern2, Pattern, R21, R22, R2>, R1, R2, R>) = Then02(pat, {prematch}) 15 | inline operator fun invoke(pat: Pattern2M, M, Pattern0, R1, R2, R>) = Then0M0(pat, {prematch}) 16 | inline operator fun invoke(pat: Pattern2M, M, Pattern1, R21, R2>, R1, R2, R>) = Then0M1(pat, {prematch}) 17 | inline operator fun invoke(pat: Pattern2M, M, Pattern2, Pattern, R21, R22, R2>, R1, R2, R>) = Then0M2(pat, {prematch}) 18 | // Then10 19 | inline operator fun invoke(pat: Pattern2, R11, R1>, Pattern0, R1, R2, R>) = Then10(pat, {prematch}) 20 | inline operator fun invoke(pat: Pattern2, R11, R1>, Pattern1, R12, R2>, R1, R2, R>) = Then11(pat, {prematch}) 21 | inline operator fun invoke(pat: Pattern2, R11, R1>, Pattern2, Pattern, R21, R22, R2>, R1, R2, R>) = Then12(pat, {prematch}) 22 | inline operator fun invoke(pat: Pattern2M, R11, R1>, M, Pattern0, R1, R2, R>) = Then1M0(pat, {prematch}) 23 | inline operator fun invoke(pat: Pattern2M, R11, R1>, M1, Pattern1, R12, R2>, R1, R2, R>) = Then1M1(pat, {prematch}) 24 | inline operator fun invoke(pat: Pattern2M, R11, R1>, M, Pattern2, Pattern, R21, R22, R2>, R1, R2, R>) = Then1M2(pat, {prematch}) 25 | // Then20 26 | inline operator fun invoke(pat: Pattern2, Pattern, R11, R12, R1>, Pattern0, R1, R2, R>) = Then20(pat, {prematch}) 27 | inline operator fun invoke(pat: Pattern2, Pattern, R11, R12, R1>, Pattern1, R13, R2>, R1, R2, R>) = Then21(pat, {prematch}) 28 | inline operator fun invoke(pat: Pattern2, Pattern, R11, R12, R1>, Pattern2, Pattern, R21, R22, R2>, R1, R2, R>) = Then22(pat, {prematch}) 29 | inline operator fun invoke(pat: Pattern2M, Pattern, R11, R12, R1>, M1, Pattern0, R1, R2, R>) = Then2M0(pat, {prematch}) 30 | inline operator fun invoke(pat: Pattern2M, Pattern, R11, R12, R1>, M1, Pattern1, R13, R2>, R1, R2, R>) = Then2M1(pat, {prematch}) 31 | inline operator fun invoke(pat: Pattern2M, Pattern, R11, R12, R1>, M, Pattern2, Pattern, R21, R22, R2>, R1, R2, R>) = Then2M2(pat, {prematch}) 32 | } 33 | 34 | fun caseEarly(prematch: Boolean) = PrematchCase(prematch) 35 | 36 | class ThenIs( 37 | override val pat: Pattern0, 38 | override val check: (R) -> Boolean 39 | ): Stage, R> { 40 | inline fun useComponents(r: R, f: (R) -> O): O { 41 | return f(r) 42 | } 43 | 44 | inline fun thenIf(crossinline f: (R) -> Boolean) = ThenIs(pat) { r: R -> check(r) && useComponents(r, f) } 45 | inline fun thenIfThis(crossinline f: R.(R) -> Boolean) = ThenIs(pat) { r: R -> check(r) && useComponents(r, { c -> f(r, c) }) } 46 | inline fun then(crossinline f: (R) -> O): Case = StageCase(pat, check) { r: R -> useComponents(r, f) } 47 | inline fun thenThis(crossinline f: R.(R) -> O): Case = StageCase(pat, check) { r: R -> useComponents(r, { c -> f(r, c) }) } 48 | } 49 | 50 | 51 | 52 | fun case(pat: Pattern1, R1, R>) = Then0(pat, {true}) 53 | 54 | 55 | class Then0( 56 | override val pat: Pattern1, R1, R>, 57 | override val check: (R) -> Boolean 58 | ): Stage, R1, R>, R> { 59 | inline fun useComponents(r: R, f: (R1) -> O): O { 60 | val (r1) = pat.divideIntoComponentsAny(r as Any) 61 | return f(r1) 62 | } 63 | 64 | inline fun thenIf(crossinline f: (R1) -> Boolean) = Then0(pat) { r: R -> check(r) && useComponents(r, f) } 65 | inline fun thenIfThis(crossinline f: R.(R1) -> Boolean) = Then0(pat) { r: R -> check(r) && useComponents(r, { c -> f(r, c) }) } 66 | inline fun then(crossinline f: (R1) -> O): Case = StageCase(pat, check) { r: R -> useComponents(r, f) } 67 | inline fun thenThis(crossinline f: R.(R1) -> O): Case = StageCase(pat, check) { r: R -> useComponents(r, { c -> f(r, c) }) } 68 | } 69 | 70 | fun case(pat: Pattern1, R11, R1>, R1, R>) = Then1(pat, {true}) 71 | 72 | 73 | class Then1( 74 | override val pat: Pattern1, R11, R1>, R1, R>, 75 | override val check: (R) -> Boolean 76 | ): Stage, R11, R1>, R1, R>, R> { 77 | inline fun useComponents(r: R, f: (ContextComponents1, Components1) -> O): O { 78 | val (r1) = pat.divideIntoComponentsAny(r as Any) 79 | val (r11) = pat.pattern1.divideIntoComponentsAny(r1 as Any) 80 | return f(ContextComponents.of(r1, r), Components1(r11)) 81 | } 82 | 83 | inline fun thenIf(crossinline f: (ContextComponents1).(Components1) -> Boolean) = Then1(pat) { r: R -> check(r) && useComponents(r, { cc, c -> with(cc) { f(cc, c) } }) } 84 | inline fun thenIfThis(crossinline f: R.(Components1) -> Boolean) = Then1(pat) { r: R -> check(r) && useComponents(r, { _, c -> f(r, c) }) } 85 | inline fun then(crossinline f: (ContextComponents1).(Components1) -> O) = StageCase(pat, check) { r: R -> useComponents(r, { cc, c -> f(cc, c) }) } 86 | inline fun thenThis(crossinline f: R.(Components1) -> O) = StageCase(pat, check) { r: R -> useComponents(r, { _, c -> f(r, c) }) } 87 | } 88 | 89 | /** Generic match for Pattern2 where we don't know what A and B are */ 90 | fun case(pat: Pattern1, Pattern, R11, R12, R1>, R1, R>) = Then2(pat, {true}) 91 | 92 | 93 | class Then2( 94 | override val pat: Pattern1, Pattern, R11, R12, R1>, R1, R>, 95 | override val check: (R) -> Boolean 96 | ): Stage, Pattern, R11, R12, R1>, R1, R>, R> { 97 | inline fun useComponents(r: R, f: (ContextComponents1, Components2) -> O): O { 98 | val (r1) = pat.divideIntoComponentsAny(r as Any) 99 | val (r11, r12) = pat.pattern1.divideIntoComponentsAny(r1 as Any) 100 | return f(ContextComponents.of(r1, r), Components2(r11, r12)) 101 | } 102 | 103 | inline fun thenIf(crossinline f: (ContextComponents1).(Components2) -> Boolean) = Then2(pat) { r: R -> check(r) && useComponents(r, f) } 104 | inline fun thenIfThis(crossinline f: R.(Components2) -> Boolean) = Then2(pat) { r: R -> check(r) && useComponents(r, { _, c -> f(r, c) }) } 105 | inline fun then(crossinline f: (ContextComponents1).(Components2) -> O) = StageCase(pat, check) { r: R -> useComponents(r, f) } 106 | inline fun thenThis(crossinline f: R.(Components2) -> O) = StageCase(pat, check) { r: R -> useComponents(r, { _, c -> f(r, c) }) } 107 | } 108 | 109 | fun case(pat: Pattern1, M1, Pattern, R11, R12, R1>, R1, R>) = Then2M(pat, {true}) 110 | 111 | class Then2M( 112 | override val pat: Pattern1, M1, Pattern, R11, R12, R1>, R1, R>, 113 | override val check: (R) -> Boolean 114 | ): Stage, M1, Pattern, R11, R12, R1>, R1, R>, R> { 115 | inline fun useComponents(r: R, f: (ContextComponents1, Components2M) -> O): O { 116 | val (r1) = pat.divideIntoComponentsAny(r as Any) 117 | val (r11, m1, r12) = pat.pattern1.divideInto3ComponentsAny(r1 as Any) 118 | return f(ContextComponents.of(r1, r), Components2M(r11, m1, r12)) 119 | } 120 | 121 | inline fun thenIf(crossinline f: (ContextComponents1).(Components2M) -> Boolean) = Then2M(pat) { r: R -> check(r) && useComponents(r, f) } 122 | inline fun thenIfThis(crossinline f: R.(Components2M) -> Boolean) = Then2M(pat) { r: R -> check(r) && useComponents(r, { _, c -> f(r, c) }) } 123 | inline fun then(crossinline f: (ContextComponents1).(Components2M) -> O) = StageCase(pat, check) { r: R -> useComponents(r, f) } 124 | inline fun thenThis(crossinline f: R.(Components2M) -> O) = StageCase(pat, check) { r: R -> useComponents(r, { _, c -> f(r, c) }) } 125 | } 126 | 127 | 128 | //fun , P1: Pattern, P2: Pattern, R1, R2, R> case(pat: Pattern2) = GenericThen2X(pat, {true}) 129 | //class GenericThen2X, P1: Pattern, P2: Pattern, R1, R2, R>( 130 | // override val pat: Pattern2, 131 | // override val check: (R) -> Boolean 132 | //): Stage, R> { 133 | // inline fun useComponents(r: R, f: (R1, R2) -> O): O = 134 | // (r as? ProductClass<*>)?.let { 135 | // val (r1, r2) = pat.divideIntoComponentsAny(it) 136 | // f(r1, r2) 137 | // } ?: notRightCls(r) 138 | // 139 | // inline fun thenIf(crossinline f: (R1, R2) -> Boolean) = GenericThen2(pat) { r: R -> useComponents(r, f) } 140 | // inline fun thenIfThis(crossinline f: R.() -> (R1, R2) -> Boolean) = GenericThen2(pat) { r: R -> useComponents(r, f(r)) } 141 | // inline fun then(crossinline f: (R1, R2) -> O) = StageCase(pat, check) { r: R -> useComponents(r, f) } 142 | // inline fun thenThis(crossinline f: R.() -> (R1, R2) -> O) = StageCase(pat, check) { r: R -> useComponents(r, f(r)) } 143 | //} 144 | -------------------------------------------------------------------------------- /decomat-core/src/commonMain/kotlin/io/decomat/Typed.kt: -------------------------------------------------------------------------------- 1 | package io.decomat 2 | 3 | import kotlin.reflect.KClass 4 | import kotlin.reflect.KType 5 | import kotlin.reflect.typeOf 6 | 7 | // Holder that we can use in various paces that guarentees the KType is the same as a parameter type 8 | // Originally there was a KClass here for diagnostic information but I have removed that because 9 | // the reflection is expensive. If I want to add it back just make sure it is lazy 10 | // e.g. the invoke() passes a `{ T:class }` into the constructor which takes a () -> KClass 11 | // element. Then return the type name via a typeName method or getter property. 12 | // Also, perhaps this should be controlled by a System property 13 | // e.g. we can check if System.property("decomat.diagnostic.info") is true and the 14 | // only propagate the reflection information if it is. 15 | class Typed(val typecheck: (Any?) -> Boolean, val cls: KClass<*>) { 16 | companion object { 17 | inline operator fun invoke() = Typed({ it is T }, T::class) 18 | } 19 | } 20 | 21 | internal class IsAnyBase: Pattern0(NothingWrapper.Nothing) { 22 | override fun matches(comps: ProductClass): Boolean = true 23 | object NothingWrapper { 24 | private val nothing: Nothing get() = TODO("Nothing can't be ever assigned or returned") 25 | val Nothing: Typed = Typed({ false }, kotlin.Nothing::class) 26 | } 27 | } 28 | 29 | private object NothingWrapper { 30 | private val nothing: Nothing get() = TODO("Nothing can't be ever assigned or returned") 31 | val Nothing: Typed = Typed({ false }, kotlin.Nothing::class) 32 | } -------------------------------------------------------------------------------- /decomat-core/src/commonMain/kotlin/io/decomat/fail/PatternErrors.kt: -------------------------------------------------------------------------------- 1 | package io.decomat.fail 2 | 3 | import io.decomat.Typed 4 | 5 | fun failToDivide(patName: String, value: kotlin.Any): Nothing = 6 | fail("Cannot divide $value into components in `$patName`. It is not a product-class.") 7 | 8 | fun failToCheck(patName: String, value: kotlin.Any, typed: Typed<*>): Nothing = 9 | fail("Typecheck for $value failed in `$patName` because it was not a ${typed.cls}.") 10 | 11 | fun failToMatch(patName: String, value: kotlin.Any, typed: Typed<*>): Nothing = 12 | fail("Match for $value failed in `$patName`. (The expected type was ${typed.cls}).") 13 | 14 | fun fail(msg: String): Nothing = throw IllegalArgumentException(msg) -------------------------------------------------------------------------------- /decomat-core/src/commonTest/kotlin/io/decomat/DecomatTest.kt: -------------------------------------------------------------------------------- 1 | package io.decomat 2 | 3 | interface DecomatTest { 4 | val foo get() = Entity("foo") 5 | val bar get() = Entity("bar") 6 | val baz get() = Entity("baz") 7 | val waz get() = Entity("waz") 8 | val kaz get() = Entity("kaz") 9 | } 10 | 11 | sealed interface Res 12 | data class Res1(val a: A): Res 13 | data class Res2(val a: A, val b: B): Res 14 | data class Res3(val a: A, val b: B, val c: C): Res 15 | data class Res4(val a: A, val b: B, val c: C, val d: D): Res 16 | data class Res5(val a: A, val b: B, val c: C, val d: D, val e: E): Res 17 | data class Res6(val a: A, val b: B, val c: C, val d: D, val e: E, val f: F): Res 18 | 19 | -------------------------------------------------------------------------------- /decomat-core/src/commonTest/kotlin/io/decomat/ExampleDsl.kt: -------------------------------------------------------------------------------- 1 | package io.decomat 2 | 3 | /* 4 | For some reason here I used the pattern where single letters A, B etc... are the pattern 5 | vars and the AP, BP are the pattern atom as opposed to the generated code where 6 | AP, BP are the pattern vars and A, B are the atoms 7 | */ 8 | 9 | sealed interface Query 10 | data class FlatMap(val head: Query, val body: Query): Query, HasProductClass { 11 | override val productComponents = ProductClass2(this, head, body) 12 | 13 | // Needed for static extension FlatMap_M 14 | companion object { 15 | } 16 | } 17 | 18 | operator fun , B: Pattern, AP: Query, BP: Query> FlatMap.Companion.get(a: A, b: B) = FlatMap_M(a, b) 19 | class FlatMap_M, B: Pattern, AP: Query, BP: Query>(a: A, b: B): Pattern2(a, b, Typed()) 20 | //class FlatMap_Is: Any(Typed()) 21 | 22 | data class Map(val head: Query, val body: Query): Query, HasProductClass { 23 | override val productComponents = ProductClass2(this, head, body) 24 | 25 | companion object { 26 | } 27 | } 28 | 29 | 30 | operator fun , B: Pattern, AP: Query, BP: Query> Map.Companion.get(a: A, b: B) = Map_M(a, b) 31 | val Is.Companion.Map get() = Is() 32 | 33 | class Map_M, P2: Pattern, A1: Query, A2: Query>(a: P1, b: P2): Pattern2(a, b, Typed()) 34 | //class Map_Is: Any(Typed()) 35 | 36 | data class Distinct(val body: Query): Query, HasProductClass { 37 | override val productComponents get() = ProductClass1(this, body) 38 | companion object { 39 | } 40 | } 41 | class Distinct_M, A: Query>(a: P): Pattern1(a, Typed()) 42 | 43 | operator fun , AP: Query> Distinct.Companion.get(a: A) = Distinct_M(a) 44 | 45 | 46 | data class Entity(val name: String): Query 47 | -------------------------------------------------------------------------------- /decomat-core/src/commonTest/kotlin/io/decomat/MatchTest.kt: -------------------------------------------------------------------------------- 1 | package io.decomat 2 | 3 | import kotlin.js.JsName 4 | import kotlin.test.Test 5 | import kotlin.test.assertFalse 6 | import kotlin.test.assertTrue 7 | 8 | @Suppress("DANGEROUS_CHARACTERS") 9 | internal class MatchTest { 10 | val foo = Entity("foo") 11 | val bar = Entity("bar") 12 | val baz = Entity("baz") 13 | val waz = Entity("waz") 14 | val kaz = Entity("kaz") 15 | 16 | 17 | /* ---------------------------- flatMap(?,Map(Map)) ---------------------------- */ 18 | 19 | // flatMap(?, ?) 20 | @Test 21 | fun flatMap_QxQ() { 22 | assertTrue(FlatMap_M(Is(), Is()).matches(FlatMap(foo, bar))) 23 | assertTrue(FlatMap_M(Is(), Is()).matches(FlatMap(foo, Map(foo, Map(waz, kaz))))) 24 | } 25 | 26 | // flatMap(?, Map(?)) 27 | @Test 28 | fun flatMap_QxMapQ() = 29 | assertTrue( 30 | FlatMap_M(Is(), Is()).matches(FlatMap(foo, Map(bar, baz))) 31 | ) 32 | 33 | // flatMap(?, Map(Map)) 34 | @Test 35 | fun flatMap_QxMapMap() = 36 | assertTrue( 37 | FlatMap_M(Is(), Map_M(Is(), Is())).matches(FlatMap(foo, Map(foo, Map(waz, kaz)))) 38 | ) 39 | 40 | // flatMap(?, Map(!Entity) - Negative 41 | @Test 42 | fun flatMap_QxMapEntity_Negative() = 43 | assertFalse( 44 | FlatMap_M(Is(), Map_M(Is(), Is())).matches(FlatMap(foo, Map(foo, Map(waz, kaz)))) 45 | ) 46 | 47 | // flatMap(?, Map(!Map)) - Negative 48 | @Test 49 | fun flatMap_QxMapNotMap_Negative() = 50 | assertFalse( 51 | FlatMap_M(Is(), Map_M(Is(), Is())).matches(FlatMap(foo, Map(foo, foo))) 52 | ) 53 | 54 | // flatMap(foo, Map(?)) 55 | @Test 56 | fun flatMap_FOOxMapQ() = 57 | assertTrue( 58 | FlatMap_M(Is(foo), Map_M(Is(), Is())).matches(FlatMap(foo, Map(foo, foo))) 59 | ) 60 | 61 | // flatMap(!foo, Map(?)) - Negative 62 | @Test 63 | fun flatMap_NotFOOxMapQ_Negative() = 64 | assertFalse( 65 | FlatMap_M(Is(bar), Map_M(Is(), Is())).matches(FlatMap(foo, Map(foo, foo))) 66 | ) 67 | 68 | /* ---------------------------- BracketSyntax ---------------------------- */ 69 | // flatMap(?, Map(Map)) 70 | @Test 71 | fun flatMap_QxMapMap2() = 72 | assertTrue(FlatMap[Is(), Map[Is(), Is()]].matches(FlatMap(foo, Map(foo, Map(waz, kaz))))) 73 | 74 | // flatMap(?, Map(!Map)) 75 | @Test 76 | fun flatMap_QxMapNotMap() = 77 | assertFalse(FlatMap[Is(), Map[Is(), Is()]].matches(FlatMap(foo, Map(foo, Map(waz, kaz))))) 78 | 79 | // flatMap(?, Map(Is_Map)) 80 | @Test 81 | fun flatMap_QxMapIsMap() = 82 | assertTrue(FlatMap[Is(), Map[Is(), Is.Map]].matches(FlatMap(foo, Map(foo, Map(waz, kaz))))) 83 | } 84 | -------------------------------------------------------------------------------- /decomat-core/src/commonTest/kotlin/io/decomat/PersonDsl.kt: -------------------------------------------------------------------------------- 1 | package io.decomat 2 | 3 | data class Name(val first: String, val last: String): HasProductClass { 4 | override val productComponents = ProductClass2(this, first, last) 5 | companion object { 6 | } 7 | } 8 | @Suppress("FINAL_UPPER_BOUND") 9 | class Name_M, P2: Pattern, A1: String, A2: String>(a: P1, b: P2): Pattern2(a, b, Typed()) 10 | @Suppress("FINAL_UPPER_BOUND") 11 | operator fun , P2: Pattern, A1: String, A2: String> Name.Companion.get(a: P1, b: P2): Pattern2 = Name_M(a, b) 12 | 13 | data class Person(val name: Name, val age: Int): HasProductClass { 14 | override val productComponents = ProductClass1(this, name) 15 | 16 | companion object { 17 | } 18 | } 19 | 20 | @Suppress("FINAL_UPPER_BOUND") 21 | class Person_M, A1: Name>(a: P1): Pattern1(a, Typed()) 22 | @Suppress("FINAL_UPPER_BOUND") 23 | operator fun , A1: Name> Person.Companion.get(a: P1): Pattern1 = Person_M(a) 24 | -------------------------------------------------------------------------------- /decomat-core/src/commonTest/kotlin/io/decomat/PersonDslTest.kt: -------------------------------------------------------------------------------- 1 | package io.decomat 2 | 3 | import kotlin.test.Test 4 | import kotlin.test.assertEquals 5 | 6 | @Suppress("DANGEROUS_CHARACTERS") 7 | class PersonDslTest: DecomatTest { 8 | 9 | val wrongAnswer = Res2("Wrong", "Answer") 10 | 11 | // Person(Name("Joe", "Bloggs" not "Roggs")) 12 | @Test 13 | fun person_name_joe_bloggs_not_roggs() { 14 | val result = 15 | on(Person(Name("Joe", "Bloggs"), 123)).match( 16 | case(Person[Name[Is("Joe"), Is("Roggs")]]) 17 | .then { name -> wrongAnswer }, 18 | case(Person[Name[Is(), Is("Roggs")]]) 19 | .then { name -> wrongAnswer }, 20 | case(Person[Name[Is("Joe"), Is("Bloggs")]]) 21 | .then { (first, last) -> Res2(first, last) } 22 | ) 23 | 24 | assertEquals(result, Res2("Joe", "Bloggs")) 25 | } 26 | 27 | // Testing early-exist case 28 | @Test 29 | fun person_name_joe_bloggs_not_roggs_early() { 30 | val result = 31 | on(Person(Name("Joe", "Bloggs"), 123)).match( 32 | case(Person[Name[Is("Joe"), Is("Roggs")]]) 33 | .then { name -> wrongAnswer }, 34 | case(Person[Name[Is(), Is("Roggs")]]) 35 | .then { name -> wrongAnswer }, 36 | caseEarly(false)(Person[Name[Is("Joe"), Is("Bloggs")]]) 37 | .then { name -> wrongAnswer }, 38 | case(Person[Name[Is("Joe"), Is("Bloggs")]]) 39 | .then { (first, last) -> Res2(first, last) } 40 | ) 41 | 42 | assertEquals(result, Res2("Joe", "Bloggs")) 43 | } 44 | 45 | @Test 46 | fun name_joe_bloggs_not_roggs_early() { 47 | val result = 48 | on(Name("Joe", "Bloggs")).match( 49 | case(Name[Is("Joe"), Is("Roggs")]) 50 | .then { _, _ -> wrongAnswer }, 51 | case(Name[Is(), Is("Roggs")]) 52 | .then { _, _ -> wrongAnswer }, 53 | caseEarly(false).invoke(Name[Is("Joe"), Is("Bloggs")]) 54 | .then { _, _ -> wrongAnswer }, 55 | case(Name[Is("Joe"), Is("Bloggs")]) 56 | .then { first, last -> Res2(first, last) } 57 | ) 58 | 59 | assertEquals(result, Res2("Joe", "Bloggs")) 60 | } 61 | 62 | // Person(Name(== "Joe", == "Bloggs" not "Roggs")) 63 | @Test 64 | fun person_name_joe_bloggs_not_roggs1() { 65 | Person(Name("Joe", "Bloggs"), 123).match( 66 | //fastCase("foo" == "foo")(Name[Is(), Is()]).then { 67 | // 68 | //} 69 | case(Person[Name[Is { it == "Joe" }, Is { it == "Roggs" }]]) 70 | .then { name -> wrongAnswer }, 71 | case(Person[Name[Is(), Is { it == "Roggs" }]]) 72 | .then { name -> wrongAnswer }, 73 | case(Person[Name[Is { it == "Joe" }, Is { it == "Bloggs" }]]) 74 | .then { (first, last) -> Res2(first, last) } 75 | ) 76 | } 77 | 78 | @Test 79 | fun person_name_joe_bloggs_not_roggs_2() { 80 | val result = 81 | on(Person(Name("Joe", "Bloggs"), 123)).match( 82 | case(Person[Name[Is {it == "Joe"}, Is {it == "Roggs"}]]) 83 | .then { name -> wrongAnswer }, 84 | case(Person[Name[Is(), Is {it == "Roggs"}]]) 85 | .then { name -> wrongAnswer }, 86 | case(Person[Name[Is {it == "Joe"}, Is {it == "Bloggs"}]]) 87 | .then { (first, last) -> Res2(first, last) } 88 | ) 89 | 90 | assertEquals(result, Res2("Joe", "Bloggs")) 91 | } 92 | 93 | // Person(Name(== "Joe" or "Jack", Is)) 94 | @Test 95 | fun person_name_joe_or_jack_is() { 96 | fun isOneOf(vararg ids: String) = Is { ids.contains(it) } 97 | val result = 98 | on(Person(Name("Joe", "Bloggs"), 123)).match( 99 | case(Person[Name[isOneOf("Bill", "Will"), Is()]]) 100 | .then { name -> wrongAnswer }, 101 | case(Person[Name[isOneOf("Joe", "Jack"), Is()]]) 102 | .then { (first, last) -> Res2(first, last) } 103 | ) 104 | 105 | assertEquals(result, Res2("Joe", "Bloggs")) 106 | } 107 | 108 | // Person(Name("Joe", Is)) - Deconstruct 109 | @Test 110 | fun person_name_joe_is__deconstruct() { 111 | val result = 112 | on(Person(Name("Joe", "Bloggs"), 123)).match( 113 | case(Person[Name[Is("Joe"), Is("Roggs")]]).then { (first, last) -> wrongAnswer }, 114 | case(Person[Name[Is(), Is("Roggs")]]).then { (first, last) -> wrongAnswer }, 115 | case(Person[Name[Is("Joe"), Is("Bloggs")]]).then { (first, last) -> Res2(first, last) } 116 | ) 117 | 118 | assertEquals(result, Res2("Joe", "Bloggs")) 119 | } 120 | 121 | // Person(Name("Joe", Is)) - thenIf 122 | @Test 123 | fun person_name_joe_is__thenIf() { 124 | val result = 125 | on(Person(Name("Joe", "Bloggs"), 123)).match( 126 | case(Person[Name[Is("Joe"), Is("Roggs")]]).then { (first, last) -> wrongAnswer }, 127 | case(Person[Name[Is(), Is()]]) 128 | .thenIf { (first, last) -> last == "Roggs" } 129 | .then { (first, last) -> wrongAnswer }, 130 | case(Person[Name[Is("Joe"), Is("Bloggs")]]).then { (first, last) -> Res2(first, last) } 131 | ) 132 | 133 | assertEquals(result, Res2("Joe", "Bloggs")) 134 | } 135 | 136 | 137 | } -------------------------------------------------------------------------------- /decomat-core/src/commonTest/kotlin/io/decomat/ThenTest.kt: -------------------------------------------------------------------------------- 1 | package io.decomat 2 | 3 | import kotlin.test.Test 4 | import kotlin.test.assertEquals 5 | import kotlin.test.assertNull 6 | 7 | public class ThenTest: DecomatTest { 8 | // ThenIs - Is 9 | @Test 10 | fun thenIs_Is() = 11 | assertEquals( 12 | case(Is()).then { a -> Res1(a) }.eval(foo), Res1(foo) 13 | ) 14 | 15 | @Test 16 | fun thenIs_Is_Early() = 17 | assertEquals( 18 | caseEarly(false)(Is()).then { a -> Res1(a) }.evalSafe(foo), null 19 | ) 20 | 21 | @Test 22 | fun typesTest() { 23 | case(FlatMap_M(Is(), Is())).then { a: Query, b: Query -> Res2(a, b) } 24 | } 25 | 26 | // Then0 - distinct(Is) - Filter 27 | @Test 28 | fun then0_distinct_Is_Filter() = 29 | assertEquals>( 30 | case(Distinct_M(Is())).then { a -> Res1(a) }.eval(Distinct(foo)), Res1(foo) 31 | ) 32 | 33 | // Then0 - distinct(Is) 34 | @Test 35 | fun then0_distinct_Is() = 36 | assertEquals( 37 | case(Distinct_M(Is())).then { a -> Res1(a) }.eval(Distinct(foo)), Res1(foo) 38 | ) 39 | 40 | // Then1 - distinct(distinct(Is)) 41 | @Test 42 | fun then1_distinct_distinct_Is() = 43 | assertEquals( 44 | case(Distinct_M(Distinct_M(Is()))).then { (a) -> Res1(a) }.eval(Distinct(Distinct(foo))), Res1(foo) 45 | ) 46 | 47 | // Then2 - distinct(flatMap(Is, Is)) 48 | @Test 49 | fun then2_distinct_flatMap_Is_Is() = 50 | assertEquals( 51 | case(Distinct_M(FlatMap_M(Is(), Is()))).then { (a, b) -> Res2(a, b) }.eval(Distinct(FlatMap(foo, bar))), Res2(foo, bar) 52 | ) 53 | 54 | // Then00 - flatMap(Is, Is) - Filter 55 | @Test 56 | fun then00_flatMap_Is_Is__Filter() = 57 | assertEquals( 58 | case(FlatMap_M(Is(), Is())).then { a, b -> Res2(a, b) }.eval(FlatMap(foo, bar)), Res2(foo, bar) 59 | ) 60 | 61 | 62 | // Then00 - flatMap(Is, Is) - thenThis 63 | @Test 64 | fun then00_flatMap_Is_Is__thenThis() = 65 | assertEquals( 66 | case(FlatMap_M(Is(), Is())).thenThis { a, b -> Res3(a, b, body) }.eval(FlatMap(foo, bar)), Res3(foo, bar, bar) 67 | ) 68 | 69 | // Then00 - flatMap(Is, Is) 70 | @Test 71 | fun then00_flatMap_Is_Is() = 72 | assertEquals( 73 | case(FlatMap_M(Is(), Is())).then { a, b -> Res2(a, b) }.eval(FlatMap(foo, bar)), Res2(foo, bar) 74 | ) 75 | 76 | // Then00-thenIf - flatMap(Is, Is) 77 | @Test 78 | fun then00_thenIf_flatMap_Is_Is() = 79 | assertEquals( 80 | case(FlatMap_M(Is(), Is())).thenIf { a, b -> a == foo && b == bar }.then { a, b -> Res2(a, b) }.eval(FlatMap(foo, bar)), Res2(foo, bar) 81 | ) 82 | 83 | // Then00-!thenIf - flatMap(Is, Is) 84 | @Test 85 | fun then00_not_thenIf_flatMap_Is_Is() = 86 | assertEquals( 87 | case(FlatMap_M(Is(), Is())).thenIf { a, b -> a != foo || b != bar }.then { a, b -> Res2(a, b) }.evalSafe(FlatMap(foo, bar)), null 88 | ) 89 | 90 | // Then00-thenIfThis - flatMap(Is, Is) 91 | @Test 92 | fun then00_thenIfThis__flatMap_Is_Is() = 93 | assertEquals>( 94 | case(FlatMap_M(Is(), Is())).thenIfThis { a, b -> 95 | a == foo && b == bar && this.head == foo && this.body == bar 96 | }.then { a, b -> Res2(a, b) }.eval(FlatMap(foo, bar)), 97 | // == 98 | Res2(foo, bar) 99 | ) 100 | 101 | // Then00-!thenIfThis - flatMap(Is, Is) 102 | @Test 103 | fun then00_not_thenIfThis__flatMap_Is_Is() = 104 | assertNull( 105 | case(FlatMap_M(Is(), Is())).thenIfThis { a, b -> 106 | a != foo || b != bar || this.head != foo || this.body != bar 107 | }.then { a, b -> Res2(a, b) }.evalSafe(FlatMap(foo, bar)) 108 | ) 109 | 110 | // Then01 - flatMap(Is, Distinct) 111 | @Test 112 | fun then01_flatMap_Is_Distinct() = 113 | assertEquals( 114 | case(FlatMap_M(Is(), Distinct_M(Is()))).then { a, (b) -> Res2(a, b) }.eval(FlatMap(foo, Distinct(bar))), Res2(foo, bar) 115 | ) 116 | 117 | // Then02 - flatMap(Is, Map) 118 | @Test 119 | fun then02__flatMap_Is_Map() = 120 | assertEquals( 121 | case(FlatMap_M(Is(), Map_M(Is(), Is()))).then { a, (b1, b2) -> Res3(a, b1, b2) }.eval(FlatMap(foo, Map(bar, baz))), Res3(foo, bar, baz) 122 | ) 123 | 124 | // Then10 - flatMap(Distinct, Is) 125 | @Test 126 | fun then10__flatMap_Distinct_Is() = 127 | assertEquals( 128 | case(FlatMap_M(Distinct_M(Is()), Is())).then { (a), b -> Res2(a, b) }.eval(FlatMap(Distinct(foo), bar)), Res2(foo, bar) 129 | ) 130 | 131 | // Then11 - flatMap(Distinct, Distinct) 132 | @Test 133 | fun then11__flatMap_Distinct_Distinct() = 134 | assertEquals( 135 | case(FlatMap_M(Distinct_M(Is()), Distinct_M(Is()))).then { (a), (b) -> Res2(a, b) }.eval(FlatMap(Distinct(foo), Distinct(bar))), Res2(foo, bar) 136 | ) 137 | 138 | // Then12 - flatMap(Distinct, Map) 139 | @Test 140 | fun then12__flatMap_Distinct_Map() = 141 | assertEquals( 142 | case(FlatMap_M(Distinct_M(Is()), Map_M(Is(), Is()))).then { (a), (b1, b2) -> Res3(a, b1, b2) }.eval(FlatMap(Distinct(foo), Map(bar, baz))), Res3(foo, bar, baz) 143 | ) 144 | 145 | // Then20 - flatMap(Map, Is) 146 | @Test 147 | fun then20__flatMap_Map_Is() = 148 | assertEquals( 149 | case(FlatMap_M(Map_M(Is(), Is()), Is())).then { (a1, a2), b -> Res3(a1, a2, b) }.eval(FlatMap(Map(foo, bar), baz)), Res3(foo, bar, baz) 150 | ) 151 | 152 | // Then21 - flatMap(Map, Distinct) 153 | @Test 154 | fun then21__flatMap_Map_Distinct() = 155 | assertEquals( 156 | case(FlatMap_M(Map_M(Is(), Is()), Distinct_M(Is()))).then { (a1, a2), (b) -> Res3(a1, a2, b) }.eval(FlatMap(Map(foo, bar), Distinct(baz))), Res3(foo, bar, baz) 157 | ) 158 | 159 | // Then22 - flatMap(Map, Map) 160 | @Test 161 | fun then22__flatMap_Map_Map() = 162 | assertEquals( 163 | case(FlatMap_M(Map_M(Is(), Is()), Map_M(Is(), Is()))).then { (a1, a2), (b1, b2) -> Res4(a1, a2, b1, b2) }.eval(FlatMap(Map(foo, bar), Map(baz, waz))), Res4(foo, bar, baz, waz) 164 | ) 165 | } 166 | -------------------------------------------------------------------------------- /decomat-core/src/commonTest/kotlin/io/decomat/manual/ThenPattern2.kt: -------------------------------------------------------------------------------- 1 | //package io.decomat 2 | // 3 | //fun , P2: Pattern0, R1, R2, R> case(pat: Pattern2) = Then00(pat, {true}) 4 | // 5 | //class Then00, P2: Pattern0, R1, R2, R>( 6 | // override val pat: Pattern2, 7 | // override val check: (R) -> Boolean 8 | //): Stage, R> { 9 | // inline fun useComponents(r: R, f: (R1, R2) -> O): O = 10 | // (r as? ProductClass<*>)?.let { 11 | // val (r1, r2) = pat.divideIntoComponentsAny(it) 12 | // f(r1, r2) 13 | // } ?: notRightCls(r) 14 | // 15 | // inline fun thenIf(crossinline f: (R1, R2) -> Boolean) = Then00(pat) { r: R -> useComponents(r, f) } 16 | // inline fun thenIfThis(crossinline f: R.() -> (R1, R2) -> Boolean) = Then00(pat) { r: R -> useComponents(r, f(r)) } 17 | // inline fun then(crossinline f: (R1, R2) -> O): Case = StageCase(pat, check) { value -> useComponents(value, f) } 18 | // inline fun thenThis(crossinline f: R.() -> (R1, R2) -> O): Case = StageCase(pat, check) { v -> useComponents(v, f(v)) } 19 | //} 20 | // 21 | //fun , P2: Pattern1, R1, P21: Pattern, R21, R2, R> case(pat: Pattern2) = Then01(pat, {true}) 22 | // 23 | //class Then01, P2: Pattern1, R1, P21: Pattern, R21, R2, R>( 24 | // override val pat: Pattern2, 25 | // override val check: (R) -> Boolean 26 | //): Stage, R> { 27 | // inline fun useComponents(r: R, f: (R1, Components1) -> O): O = 28 | // (r as? ProductClass<*>)?.let { 29 | // val (r1, r2) = pat.divideIntoComponentsAny(it) 30 | // val (r21) = pat.pattern2.divideIntoComponentsAny(r2 as Any) 31 | // f(r1, Components1(r21)) 32 | // } ?: notRightCls(r) 33 | // 34 | // inline fun thenIf(crossinline f: (R1, Components1) -> Boolean) = Then01(pat) { r: R -> useComponents(r, f) } 35 | // inline fun thenIfThis(crossinline f: R.() -> (R1, Components1) -> Boolean) = Then01(pat) { r: R -> useComponents(r, f(r)) } 36 | // inline fun then(crossinline f: (R1, Components1) -> O): Case = StageCase(pat, check) { value -> useComponents(value, f) } 37 | // inline fun thenThis(crossinline f: R.() -> (R1, Components1) -> O): Case = StageCase(pat, check) { v -> useComponents(v, f(v)) } 38 | //} 39 | // 40 | //fun , P2: Pattern2, R1, P21: Pattern, P22: Pattern, R21, R22, R2, R> case(pat: Pattern2) = Then02(pat, {true}) 41 | // 42 | //class Then02, P2: Pattern2, R1, P21: Pattern, P22: Pattern, R21, R22, R2, R>( 43 | // override val pat: Pattern2, 44 | // override val check: (R) -> Boolean 45 | //): Stage, R> { 46 | // inline fun useComponents(r: R, f: (R1, Components2) -> O): O = 47 | // (r as? ProductClass<*>)?.let { 48 | // val (r1, r2) = pat.divideIntoComponentsAny(it) 49 | // val (r21, r22) = pat.pattern2.divideIntoComponentsAny(r2 as Any) 50 | // f(r1, Components2(r21, r22)) 51 | // } ?: notRightCls(r) 52 | // 53 | // inline fun thenIf(crossinline f: (R1, Components2) -> Boolean) = Then02(pat) { r: R -> useComponents(r, f) } 54 | // inline fun thenIfThis(crossinline f: R.() -> (R1, Components2) -> Boolean) = Then02(pat) { r: R -> useComponents(r, f(r)) } 55 | // inline fun then(crossinline f: (R1, Components2) -> O): Case = StageCase(pat, check) { value -> useComponents(value, f) } 56 | // inline fun thenThis(crossinline f: R.() -> (R1, Components2) -> O): Case = StageCase(pat, check) { v -> useComponents(v, f(v)) } 57 | //} 58 | // 59 | //fun , P2: Pattern0, P11: Pattern, R11, R1, R2, R> case(pat: Pattern2) = Then10(pat, {true}) 60 | // 61 | //class Then10, P2: Pattern0, P11: Pattern, R11, R1, R2, R>( 62 | // override val pat: Pattern2, 63 | // override val check: (R) -> Boolean 64 | //): Stage, R> { 65 | // inline fun useComponents(r: R, f: (Components1, R2) -> O): O = 66 | // (r as? ProductClass<*>)?.let { 67 | // val (r1, r2) = pat.divideIntoComponentsAny(it) 68 | // val (r11) = pat.pattern1.divideIntoComponentsAny(r1 as Any) 69 | // f(Components1(r11), r2) 70 | // } ?: notRightCls(r) 71 | // 72 | // inline fun thenIf(crossinline f: (Components1, R2) -> Boolean) = Then10(pat) { r: R -> useComponents(r, f) } 73 | // inline fun thenIfThis(crossinline f: R.() -> (Components1, R2) -> Boolean) = Then10(pat) { r: R -> useComponents(r, f(r)) } 74 | // inline fun then(crossinline f: (Components1, R2) -> O): Case = StageCase(pat, check) { value -> useComponents(value, f) } 75 | // inline fun thenThis(crossinline f: R.() -> (Components1, R2) -> O): Case = StageCase(pat, check) { v -> useComponents(v, f(v)) } 76 | //} 77 | // 78 | //fun , P2: Pattern0, P11: Pattern, P12: Pattern, R11, R12, R1, R2, R> case(pat: Pattern2) = Then20(pat, {true}) 79 | // 80 | //class Then20, P2: Pattern0, P11: Pattern, P12: Pattern, R11, R12, R1, R2, R>( 81 | // override val pat: Pattern2, 82 | // override val check: (R) -> Boolean 83 | //): Stage, R> { 84 | // inline fun useComponents(r: R, f: (Components2, R2) -> O): O = 85 | // (r as? ProductClass<*>)?.let { 86 | // val (r1, r2) = pat.divideIntoComponentsAny(it) 87 | // val (r21, r22) = pat.pattern1.divideIntoComponentsAny(r1 as Any) 88 | // f(Components2(r21, r22), r2) 89 | // } ?: notRightCls(r) 90 | // 91 | // inline fun thenIf(crossinline f: (Components2, R2) -> Boolean) = Then20(pat) { r: R -> useComponents(r, f) } 92 | // inline fun thenIfThis(crossinline f: R.() -> (Components2, R2) -> Boolean) = Then20(pat) { r: R -> useComponents(r, f(r)) } 93 | // inline fun then(crossinline f: (Components2, R2) -> O): Case = StageCase(pat, check) { value -> useComponents(value, f) } 94 | // inline fun thenThis(crossinline f: R.() -> (Components2, R2) -> O): Case = StageCase(pat, check) { v -> useComponents(v, f(v)) } 95 | //} 96 | // 97 | //fun , P2: Pattern1, P11: Pattern, P21: Pattern, R11, R21, R1, R2, R> case(pat: Pattern2) = Then11(pat, {true}) 98 | // 99 | //class Then11, P2: Pattern1, P11: Pattern, P21: Pattern, R11, R21, R1, R2, R>( 100 | // override val pat: Pattern2, 101 | // override val check: (R) -> Boolean 102 | //): Stage, R> { 103 | // inline fun useComponents(r: R, f: (Components1, Components1) -> O): O = 104 | // (r as? ProductClass<*>)?.let { 105 | // val (r1, r2) = pat.divideIntoComponentsAny(it) 106 | // val (r11) = pat.pattern1.divideIntoComponentsAny(r1 as Any) 107 | // val (r21) = pat.pattern2.divideIntoComponentsAny(r2 as Any) 108 | // f(Components1(r11), Components1(r21)) 109 | // } ?: notRightCls(r) 110 | // 111 | // inline fun thenIf(crossinline f: (Components1, Components1) -> Boolean) = Then11(pat) { r: R -> useComponents(r, f) } 112 | // inline fun thenIfThis(crossinline f: R.() -> (Components1, Components1) -> Boolean) = Then11(pat) { r: R -> useComponents(r, f(r)) } 113 | // inline fun then(crossinline f: (Components1, Components1) -> O): Case = StageCase(pat, check) { value -> useComponents(value, f) } 114 | // inline fun thenThis(crossinline f: R.() -> (Components1, Components1) -> O): Case = StageCase(pat, check) { v -> useComponents(v, f(v)) } 115 | //} 116 | // 117 | //fun , P2: Pattern2, P11: Pattern, P21: Pattern, P22: Pattern, R11, R21, R22, R1, R2, R> case(pat: Pattern2) = Then12(pat, {true}) 118 | // 119 | //class Then12, P2: Pattern2, P11: Pattern, P21: Pattern, P22: Pattern, R11, R21, R22, R1, R2, R>( 120 | // override val pat: Pattern2, 121 | // override val check: (R) -> Boolean 122 | //): Stage, R> { 123 | // inline fun useComponents(r: R, f: (Components1, Components2) -> O): O = 124 | // (r as? ProductClass<*>)?.let { 125 | // val (r1, r2) = pat.divideIntoComponentsAny(it) 126 | // val (r11) = pat.pattern1.divideIntoComponentsAny(r1 as Any) 127 | // val (r21, r22) = pat.pattern2.divideIntoComponentsAny(r2 as Any) 128 | // f(Components1(r11), Components2(r21, r22)) 129 | // } ?: notRightCls(r) 130 | // 131 | // inline fun thenIf(crossinline f: (Components1, Components2) -> Boolean) = Then12(pat) { r: R -> useComponents(r, f) } 132 | // inline fun thenIfThis(crossinline f: R.() -> (Components1, Components2) -> Boolean) = Then12(pat) { r: R -> useComponents(r, f(r)) } 133 | // inline fun then(crossinline f: (Components1, Components2) -> O): Case = StageCase(pat, check) { value -> useComponents(value, f) } 134 | // inline fun thenThis(crossinline f: R.() -> (Components1, Components2) -> O): Case = StageCase(pat, check) { v -> useComponents(v, f(v)) } 135 | //} 136 | // 137 | // 138 | //fun , P2: Pattern1, P11: Pattern, P12: Pattern, R11, R12, R1, P21: Pattern, R21, R2, R> case(pat: Pattern2) = Then21(pat, {true}) 139 | // 140 | //class Then21, P2: Pattern1, P11: Pattern, P12: Pattern, R11, R12, R1, P21: Pattern, R21, R2, R>( 141 | // override val pat: Pattern2, 142 | // override val check: (R) -> Boolean 143 | //): Stage, R> { 144 | // inline fun useComponents(r: R, f: (Components2, Components1) -> O): O = 145 | // (r as? ProductClass<*>)?.let { 146 | // val (r1, r2) = pat.divideIntoComponentsAny(it) 147 | // val (r11, r12) = pat.pattern1.divideIntoComponentsAny(r1 as Any) 148 | // val (r21) = pat.pattern2.divideIntoComponentsAny(r2 as Any) 149 | // f(Components2(r11, r12), Components1(r21)) 150 | // } ?: notRightCls(r) 151 | // 152 | // inline fun thenIf(crossinline f: (Components2, Components1) -> Boolean) = Then21(pat) { r: R -> useComponents(r, f) } 153 | // inline fun thenIfThis(crossinline f: R.() -> (Components2, Components1) -> Boolean) = Then21(pat) { r: R -> useComponents(r, f(r)) } 154 | // inline fun then(crossinline f: (Components2, Components1) -> O): Case = StageCase(pat, check) { value -> useComponents(value, f) } 155 | // inline fun thenThis(crossinline f: R.() -> (Components2, Components1) -> O): Case = StageCase(pat, check) { v -> useComponents(v, f(v)) } 156 | //} 157 | // 158 | //fun , P2: Pattern2, P11: Pattern, P12: Pattern, R11, R12, R1, P21: Pattern, P22: Pattern, R21, R22, R2, R> case(pat: Pattern2) = Then22(pat, {true}) 159 | // 160 | //class Then22, P2: Pattern2, P11: Pattern, P12: Pattern, R11, R12, R1, P21: Pattern, P22: Pattern, R21, R22, R2, R>( 161 | // override val pat: Pattern2, 162 | // override val check: (R) -> Boolean 163 | //): Stage, R> { 164 | // inline fun useComponents(r: R, f: (Components2, Components2) -> O): O = 165 | // (r as? ProductClass<*>)?.let { 166 | // val (r1, r2) = pat.divideIntoComponentsAny(it) 167 | // val (r11, r12) = pat.pattern1.divideIntoComponentsAny(r1 as Any) 168 | // val (r21, r22) = pat.pattern2.divideIntoComponentsAny(r2 as Any) 169 | // f(Components2(r11, r12), Components2(r21, r22)) 170 | // } ?: notRightCls(r) 171 | // 172 | // inline fun thenIf(crossinline f: (Components2, Components2) -> Boolean) = Then22(pat) { r: R -> useComponents(r, f) } 173 | // inline fun thenIfThis(crossinline f: R.() -> (Components2, Components2) -> Boolean) = Then22(pat) { r: R -> useComponents(r, f(r)) } 174 | // inline fun then(crossinline f: (Components2, Components2) -> O): Case = StageCase(pat, check) { value -> useComponents(value, f) } 175 | // inline fun thenThis(crossinline f: R.() -> (Components2, Components2) -> O): Case = StageCase(pat, check) { v -> useComponents(v, f(v)) } 176 | //} 177 | // 178 | //class Then30, P2: Pattern0, P11: Pattern, P12: Pattern, P13: Pattern, R11, R12, R13, R1, R2, R>( 179 | // override val pat: Pattern2, 180 | // override val check: (R) -> Boolean 181 | //): Stage, R> { 182 | // inline fun useComponents(r: R, f: (Components3, R2) -> O): O = 183 | // (r as? ProductClass<*>)?.let { 184 | // val (r1, r2) = pat.divideIntoComponentsAny(it) 185 | // val (r11, r12, r13) = pat.pattern1.divideIntoComponentsAny(r1 as Any) 186 | // f(Components3(r11, r12, r13), r2) 187 | // } ?: notRightCls(r) 188 | // 189 | // inline fun thenIf(crossinline f: (Components3, R2) -> Boolean) = Then30(pat) { r: R -> useComponents(r, f) } 190 | // inline fun thenIfThis(crossinline f: R.() -> (Components3, R2) -> Boolean) = Then30(pat) { r: R -> useComponents(r, f(r)) } 191 | // inline fun then(crossinline f: (Components3, R2) -> O): Case = StageCase(pat, check) { value -> useComponents(value, f) } 192 | // inline fun thenThis(crossinline f: R.() -> (Components3, R2) -> O): Case = StageCase(pat, check) { v -> useComponents(v, f(v)) } 193 | //} 194 | // 195 | //class Then31, P2: Pattern1, P11: Pattern, P12: Pattern, P13: Pattern, R11, R12, R13, R1, P21: Pattern, R21, R2, R>( 196 | // override val pat: Pattern2, 197 | // override val check: (R) -> Boolean 198 | //): Stage, R> { 199 | // inline fun useComponents(r: R, f: (Components3, Components1) -> O): O = 200 | // (r as? ProductClass<*>)?.let { 201 | // val (r1, r2) = pat.divideIntoComponentsAny(it) 202 | // val (r11, r12, r13) = pat.pattern1.divideIntoComponentsAny(r1 as Any) 203 | // val (r21) = pat.pattern2.divideIntoComponentsAny(r2 as Any) 204 | // f(Components3(r11, r12, r13), Components1(r21)) 205 | // } ?: notRightCls(r) 206 | // 207 | // inline fun thenIf(crossinline f: (Components3, Components1) -> Boolean) = Then31(pat) { r: R -> useComponents(r, f) } 208 | // inline fun thenIfThis(crossinline f: R.() -> (Components3, Components1) -> Boolean) = Then31(pat) { r: R -> useComponents(r, f(r)) } 209 | // inline fun then(crossinline f: (Components3, Components1) -> O): Case = StageCase(pat, check) { value -> useComponents(value, f) } 210 | // inline fun thenThis(crossinline f: R.() -> (Components3, Components1) -> O): Case = StageCase(pat, check) { v -> useComponents(v, f(v)) } 211 | //} 212 | // 213 | //class Then32, P2: Pattern2, P11: Pattern, P12: Pattern, P13: Pattern, R11, R12, R13, R1, P21: Pattern, P22: Pattern, R21, R22, R2, R>( 214 | // override val pat: Pattern2, 215 | // override val check: (R) -> Boolean 216 | //): Stage, R> { 217 | // inline fun useComponents(r: R, f: (Components3, Components2) -> O): O = 218 | // (r as? ProductClass<*>)?.let { 219 | // val (r1, r2) = pat.divideIntoComponentsAny(it) 220 | // val (r11, r12, r13) = pat.pattern1.divideIntoComponentsAny(r1 as Any) 221 | // val (r21, r22) = pat.pattern2.divideIntoComponentsAny(r2 as Any) 222 | // f(Components3(r11, r12, r13), Components2(r21, r22)) 223 | // } ?: notRightCls(r) 224 | // 225 | // inline fun thenIf(crossinline f: (Components3, Components2) -> Boolean) = Then32(pat) { r: R -> useComponents(r, f) } 226 | // inline fun thenIfThis(crossinline f: R.() -> (Components3, Components2) -> Boolean) = Then32(pat) { r: R -> useComponents(r, f(r)) } 227 | // inline fun then(crossinline f: (Components3, Components2) -> O): Case = StageCase(pat, check) { value -> useComponents(value, f) } 228 | // inline fun thenThis(crossinline f: R.() -> (Components3, Components2) -> O): Case = StageCase(pat, check) { v -> useComponents(v, f(v)) } 229 | //} 230 | // 231 | //class Then33, P2: Pattern3, P11: Pattern, P12: Pattern, P13: Pattern, R11, R12, R13, R1, P21: Pattern, P22: Pattern, P23: Pattern, R21, R22, R23, R2, R>( 232 | // override val pat: Pattern2, 233 | // override val check: (R) -> Boolean 234 | //): Stage, R> { 235 | // inline fun useComponents(r: R, f: (Components3, Components3) -> O): O = 236 | // (r as? ProductClass<*>)?.let { 237 | // val (r1, r2) = pat.divideIntoComponentsAny(it) 238 | // val (r11, r12, r13) = pat.pattern1.divideIntoComponentsAny(r1 as Any) 239 | // val (r21, r22, r23) = pat.pattern2.divideIntoComponentsAny(r2 as Any) 240 | // f(Components3(r11, r12, r13), Components3(r21, r22, r23)) 241 | // } ?: notRightCls(r) 242 | // 243 | // inline fun thenIf(crossinline f: (Components3, Components3) -> Boolean) = Then33(pat) { r: R -> useComponents(r, f) } 244 | // inline fun thenIfThis(crossinline f: R.() -> (Components3, Components3) -> Boolean) = Then33(pat) { r: R -> useComponents(r, f(r)) } 245 | // inline fun then(crossinline f: (Components3, Components3) -> O): Case = StageCase(pat, check) { value -> useComponents(value, f) } 246 | // inline fun thenThis(crossinline f: R.() -> (Components3, Components3) -> O): Case = StageCase(pat, check) { v -> useComponents(v, f(v)) } 247 | //} 248 | // 249 | // 250 | //// 251 | ////class Then111, P2: Pattern1, P3: Pattern1, 252 | //// P11: Pattern, P21: Pattern, P31: Pattern, 253 | //// R11, R21, R31, R1, R2, R3, R>( 254 | //// override val pat: Pattern3, 255 | //// override val check: (R) -> Boolean 256 | ////): Stage, R> { 257 | //// inline fun useComponents(r: R, f: (Components1, Components1, Components1) -> O): O = 258 | //// (r as? ProductClass<*>)?.let { 259 | //// val (r1, r2, r3) = pat.divideIntoComponentsAny(it) 260 | //// val (r11) = pat.pattern1.divideIntoComponentsAny(r1 as Any) 261 | //// val (r21) = pat.pattern2.divideIntoComponentsAny(r2 as Any) 262 | //// val (r31) = pat.pattern3.divideIntoComponentsAny(r3 as Any) 263 | //// f(Components1(r11), Components1(r21), Components1(r31)) 264 | //// } ?: notRightCls(r) 265 | //// 266 | //// inline fun thenIf(crossinline f: (Components1, Components1, Components1) -> Boolean) = 267 | //// Then111(pat) { r: R -> useComponents(r, f) } 268 | //// 269 | //// inline fun thenIfThis(crossinline f: R.() -> (Components1, Components1, Components1) -> Boolean) = 270 | //// Then111(pat) { r: R -> useComponents(r, f(r)) } 271 | //// 272 | //// inline fun then(crossinline f: (Components1, Components1, Components1) -> O): Case = 273 | //// StageCase(pat, check) { value -> useComponents(value, f) } 274 | //// 275 | //// inline fun thenThis(crossinline f: R.() -> (Components1, Components1, Components1) -> O): Case = 276 | //// StageCase(pat, check) { v -> useComponents(v, f(v)) } 277 | ////} 278 | //// 279 | ////class Then112, P2: Pattern1, P3: Pattern3, P11: Pattern, P21: Pattern, P31: Pattern, P32: Pattern, P33: Pattern, R11, R21, R31, R32, R33, R1, R2, R3, R>( 280 | //// override val pat: Pattern3, 281 | //// override val check: (R) -> Boolean 282 | ////): Stage, R> { 283 | //// 284 | //// inline fun useComponents(r: R, f: (R11, R21, R31, R32, R33) -> O): O = 285 | //// (r as? ProductClass<*>)?.let { 286 | //// val (r1, r2, r3) = pat.divideIntoComponentsAny(it) 287 | //// val (r11) = pat.pattern1.divideIntoComponentsAny(r1 as Any) 288 | //// val (r21) = pat.pattern2.divideIntoComponentsAny(r2 as Any) 289 | //// val (r31, r32, r33) = pat.pattern3.divideIntoComponentsAny(r3 as Any) 290 | //// f(r11, r21, r31, r32, r33) 291 | //// } ?: notRightCls(r) 292 | //// 293 | //// inline fun thenIf(crossinline f: (R11, R21, R31, R32, R33) -> Boolean) = Then112(pat) { r: R -> useComponents(r, f) } 294 | //// inline fun thenIfThis(crossinline f: R.() -> (R11, R21, R31, R32, R33) -> Boolean) = Then112(pat) { r: R -> useComponents(r, f(r)) } 295 | //// inline fun then(crossinline f: (R11, R21, R31, R32, R33) -> O): Case = StageCase(pat, check) { value -> useComponents(value, f) } 296 | //// inline fun thenThis(crossinline f: R.() -> (R11, R21, R31, R32, R33) -> O): Case = StageCase(pat, check) { v -> useComponents(v, f(v)) } 297 | ////} 298 | -------------------------------------------------------------------------------- /decomat-core/src/commonTest/kotlin/io/decomat/middle/ExampleMiddleDsl.kt: -------------------------------------------------------------------------------- 1 | package io.decomat.middle 2 | 3 | import io.decomat.* 4 | 5 | /* 6 | For some reason here I used the pattern where single letters A, B etc... are the pattern 7 | vars and the AP, BP are the pattern atom as opposed to the generated code where 8 | AP, BP are the pattern vars and A, B are the atoms 9 | */ 10 | 11 | sealed interface Query 12 | data class FlatMap(val head: Query, val middle: String, val body: Query): Query, HasProductClass { 13 | override val productComponents = ProductClass2M(this, head, middle, body) 14 | 15 | // Needed for static extension FlatMap_M 16 | companion object { 17 | // important for a, b to be A, B instead of just Pattern, Pattern because 18 | // the resolution to the correct `case` function doesn't know which case to choose 19 | // since case-functions are defined on specific Pattern subclasses e.g. case(P: Pattern3<...>) 20 | // all of them start to apply as soon as you generalize to Pattern 21 | operator fun , B: Pattern, AP: Query, BP: Query> get(a: A, b: B): Pattern2M = FlatMap_M(a, b) 22 | } 23 | } 24 | 25 | operator fun , B: Pattern, AP: Query, BP: Query> FlatMap.Companion.get(a: A, b: B) = FlatMap_M(a, b) 26 | class FlatMap_M, B: Pattern, AP: Query, BP: Query>(a: A, b: B): Pattern2M(a, b, Typed()) 27 | 28 | data class Map(val head: Query, val middle: String, val body: Query): Query, HasProductClass { 29 | override val productComponents = ProductClass2M(this, head, middle, body) 30 | 31 | companion object { 32 | operator fun , B: Pattern, AP: Query, BP: Query> get(a: Pattern, b: Pattern) = Map_M(a, b) 33 | } 34 | } 35 | 36 | operator fun , B: Pattern, AP: Query, BP: Query> Map.Companion.get(a: A, b: B) = Map_M(a, b) 37 | val Is.Companion.Map get() = Is() 38 | 39 | class Map_M, P2: Pattern, A1: Query, A2: Query>(a: P1, b: P2): Pattern2M(a, b, Typed()) 40 | 41 | data class Distinct(val body: Query): Query, HasProductClass { 42 | override val productComponents get() = ProductClass1(this, body) 43 | companion object { 44 | operator fun , AP: Query> get(a: Pattern) = Distinct_M(a) 45 | } 46 | } 47 | 48 | class Distinct_M, A: Query>(a: P): Pattern1(a, Typed()) 49 | 50 | operator fun , AP: Query> Distinct.Companion.get(a: A) = Distinct_M(a) 51 | 52 | data class Entity(val name: String): Query 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /decomat-core/src/commonTest/kotlin/io/decomat/middle/MatchTest.kt: -------------------------------------------------------------------------------- 1 | package io.decomat.middle 2 | 3 | import io.decomat.* 4 | import kotlin.test.Test 5 | import kotlin.test.assertEquals 6 | import kotlin.test.assertTrue 7 | import kotlin.test.assertFalse 8 | import kotlin.test.fail 9 | 10 | @Suppress("DANGEROUS_CHARACTERS") 11 | class MatchTest { 12 | val foo = Entity("foo") 13 | val bar = Entity("bar") 14 | val baz = Entity("baz") 15 | val waz = Entity("waz") 16 | val kaz = Entity("kaz") 17 | val idA = "AAA" 18 | val idB = "BBB" 19 | val idC = "CCC" 20 | 21 | // flatMap(?, ?) 22 | @Test 23 | fun flatMap_QxQ() { 24 | assertTrue(FlatMap[Is(), Is()].matches(FlatMap(foo, idA, bar))) 25 | assertTrue(FlatMap[Is(), Is()].matches(FlatMap(foo, idA, Map(foo, idB, Map(waz, idC, kaz))))) 26 | } 27 | 28 | infix fun T.shouldBe(b: T): Unit = assertEquals(this, b) 29 | 30 | // flatMap(?, Map(?)) 31 | @Test 32 | fun flatMap_QxMapQ() { 33 | on(FlatMap(foo, idA, bar)).match( 34 | case(FlatMap[Is(), Is(waz)]).then { a, m, b -> fail() }, 35 | case(FlatMap[Is(), Is()]).then { a, m, b -> Res3(a, m, b) } 36 | ) shouldBe Res3(foo, idA, bar) 37 | 38 | assertTrue(FlatMap[Is(), Is()].matches(FlatMap(foo, idA, bar))) 39 | assertTrue(FlatMap[Is(), Is()].matches(FlatMap(foo, idA, Map(foo, idB, Map(waz, idC, kaz))))) 40 | } 41 | 42 | // flatMap(?, Map(Map)) 43 | @Test 44 | fun flatMap_QxMapMap() { 45 | on(FlatMap(foo, idA, Map(bar, idB, baz))).match( 46 | //case(FlatMap[Is(), Map[Is(), Is(waz)]]).then { a, m, b -> fail() }, 47 | case(FlatMap[Is(), Is()]).then { a, m, b -> Res3(a, m, b) } 48 | ) shouldBe Res3(foo, idA, Map(bar, idB, baz)) 49 | 50 | assertTrue( 51 | FlatMap[Is(), Is()].matches(FlatMap(foo, idA, Map(bar, idB, baz))) 52 | ) 53 | } 54 | 55 | // flatMap(Map(?), ?) 56 | @Test 57 | fun flatMap_MapQxQ() { 58 | on(FlatMap(Map(foo, idB, bar), idA, baz)).match( 59 | case(FlatMap[Map[Is(), Is(waz)], Is()]).then { a, m, b -> fail() }, 60 | case(FlatMap[Is(), Is()]).then { a, m, b -> Res3(a, m, b) } 61 | ) shouldBe Res3(Map(foo, idB, bar), idA, baz) 62 | 63 | assertTrue( 64 | FlatMap[Is(), Is()].matches(FlatMap(foo, idA, Map(bar, idB, baz))) 65 | ) 66 | } 67 | 68 | // flatMap(Map(?), Map(?)) 69 | @Test 70 | fun flatMap_MapQxMapQ() { 71 | on(FlatMap(Map(foo, idA, bar), idB, Map(baz, idC, waz))).match( 72 | case(FlatMap[Map[Is(), Is(kaz)], Map[Is(), Is(kaz)]]).then { a, m, b -> fail() }, 73 | case(FlatMap[Map[Is(), Is()], Map[Is(), Is()]]).then { (a1, am, a2), m, (b1, bm, b2) -> Res3(Res3(a1, am, a2), m, Res3(b1, bm, b2)) } 74 | ) shouldBe Res3(Res3(foo, idA, bar), idB, Res3(baz, idC, waz)) 75 | 76 | assertTrue( 77 | FlatMap[Map[Is(), Is()], Map[Is(), Is()]].matches(FlatMap(Map(foo, idA, bar), idB, Map(baz, idC, waz))) 78 | ) 79 | } 80 | 81 | // flatMap(Map(?), Map(Map)) 82 | @Test 83 | fun flatMap_MapQxMapQx() { 84 | on(FlatMap(Map(foo, idA, bar), idB, Map(baz, idC, waz))).match( 85 | case(FlatMap[Map[Is(), Is(kaz)], Map[Is(), Is(kaz)]]).then { a, m, b -> fail() }, 86 | case(FlatMap[Map[Is(), Is()], Map[Is(), Is()]]).then { (a1, am, a2), m, (b1, bm, b2) -> Res3(compLeft, compRight, comp) } 87 | ) shouldBe Res3(Map(foo, idA, bar), Map(baz, idC, waz), FlatMap(Map(foo, idA, bar), idB, Map(baz, idC, waz))) 88 | } 89 | 90 | @Test 91 | fun flatMap_QxMap() { 92 | on(FlatMap(foo, idA, Map(bar, idB, baz))).match( 93 | case(FlatMap[Is(), Map[Is(), Is(waz)]]).then { a, m, b -> fail() }, 94 | case(FlatMap[Is(), Map[Is(), Is()]]).then { a, m, (b1, m1, b2) -> Res3(a, m, Res3(b1, m1, b2)) } 95 | ) shouldBe Res3(foo, idA, Res3(bar, idB, baz)) 96 | 97 | assertTrue( 98 | FlatMap[Is(), Map[Is(), Is()]].matches(FlatMap(foo, idA, Map(foo, idB, Map(waz, idC, kaz)))) 99 | ) 100 | } 101 | 102 | // flatMap(Map(Map), ?) 103 | @Test 104 | fun flatMap_MapMapxQ() { 105 | on(FlatMap(Map(foo, idA, bar), idB, baz)).match( 106 | case(FlatMap[Map[Is(), Is(waz)], Is()]).then { a, m, b -> fail() }, 107 | case(FlatMap[Map[Is(), Is()], Is()]).then { (a1, m1, a2), m, b -> Res3(Res3(a1, m1, a2), m, b) } 108 | ) shouldBe Res3(Res3(foo, idA, bar), idB, baz) 109 | 110 | assertTrue( 111 | FlatMap[Map[Is(), Is()], Is()].matches(FlatMap(Map(foo, idA, bar), idB, baz)) 112 | ) 113 | } 114 | 115 | // flatMap(Map(Map), Map(Map)) 116 | @Test 117 | fun flatMap_MapMapxMapMap() { 118 | on(FlatMap(Map(foo, idA, bar), idB, Map(baz, idC, waz))).match( 119 | case(FlatMap[Map[Is(), Is(kaz)], Map[Is(), Is(kaz)]]).then { a, m, b -> fail() }, 120 | case(FlatMap[Map[Is(), Is()], Map[Is(), Is()]]).then { (a1, m1, a2), m, (b1, m2, b2) -> Res3(Res3(a1, m1, a2), m, Res3(b1, m2, b2)) } 121 | ) shouldBe Res3(Res3(foo, idA, bar), idB, Res3(baz, idC, waz)) 122 | 123 | assertTrue( 124 | FlatMap[Map[Is(), Is()], Map[Is(), Is()]].matches(FlatMap(Map(foo, idA, bar), idB, Map(baz, idC, waz))) 125 | ) 126 | } 127 | 128 | // flatMap(?, Map(!Entity) - Negative 129 | @Test 130 | fun flatMap_QxMapEntity__Negative() { 131 | assertFalse( 132 | FlatMap[Is(), Map[Is(), Is()]].matches(FlatMap(foo, idA, Map(foo, idB, Map(waz, idC, kaz)))) 133 | ) 134 | } 135 | 136 | // flatMap(?, Map(!Map) - Negative 137 | @Test 138 | fun flatMap_QxMapNotMap__Negative() { 139 | assertFalse( 140 | FlatMap[Is(), Map[Is(), Is()]].matches(FlatMap(foo, idA, Map(foo, idB, foo))) 141 | ) 142 | } 143 | 144 | // flatMap(foo, Map(?)) 145 | @Test 146 | fun flatMap_FOOxMapQ() { 147 | assertTrue( 148 | FlatMap[Is(foo), Map[Is(), Is()]].matches(FlatMap(foo, idA, Map(foo, idB, foo))) 149 | ) 150 | } 151 | 152 | // flatMap(!foo, Map(?)) - Negative 153 | @Test 154 | fun flatMap_NotFOOxMapQ__Negative() { 155 | assertFalse( 156 | FlatMap[Is(bar), Map[Is(), Is()]].matches(FlatMap(foo, idA, Map(foo, idB, foo))) 157 | ) 158 | } 159 | 160 | } -------------------------------------------------------------------------------- /decomat-core/src/commonTest/kotlin/io/decomat/middle/PersonMiddleDsl.kt: -------------------------------------------------------------------------------- 1 | package io.decomat.middle 2 | 3 | import io.decomat.* 4 | 5 | 6 | data class Name(val first: String, val middle: Int, val last: String): HasProductClass { 7 | override val productComponents = ProductClass2M(this, first, middle, last) 8 | companion object { 9 | } 10 | } 11 | @Suppress("FINAL_UPPER_BOUND") 12 | class Name_M, P2: Pattern, A1: String, A2: String>(a: P1, b: P2): Pattern2M(a, b, Typed()) 13 | @Suppress("FINAL_UPPER_BOUND") 14 | operator fun , P2: Pattern, A1: String, A2: String> Name.Companion.get(a: P1, b: P2): Pattern2M = Name_M(a, b) 15 | 16 | data class Person(val name: Name, val suffix: String, val age: Int): HasProductClass { 17 | override val productComponents = ProductClass2M(this, name, suffix, age) 18 | 19 | companion object { 20 | } 21 | } 22 | 23 | @Suppress("FINAL_UPPER_BOUND") 24 | class Person_M, P2: Pattern, A1: Name, A2: Int>(a: P1, b: P2): Pattern2M(a, b, Typed()) 25 | @Suppress("FINAL_UPPER_BOUND") 26 | operator fun , P2: Pattern, A1: Name, A2: Int> Person.Companion.get(a: P1, b: P2): Pattern2M = Person_M(a, b) 27 | -------------------------------------------------------------------------------- /decomat-core/src/commonTest/kotlin/io/decomat/middle/PersonMiddleDslTest.kt: -------------------------------------------------------------------------------- 1 | package io.decomat.middle 2 | 3 | import io.decomat.* 4 | import kotlin.test.Test 5 | import kotlin.test.assertEquals 6 | 7 | @Suppress("DANGEROUS_CHARACTERS") 8 | class PersonMiddleDslTest: DecomatTest { 9 | 10 | val wrongAnswer = Res2("Wrong", "Answer") 11 | 12 | // Person(Name("Joe", "Bloggs" not "Roggs")) 13 | @Test 14 | fun person_name_joe_bloggs_not_roggs() { 15 | val result = 16 | on(Person(Name("Joe", 11, "Bloggs"), "Jr", 123)).match( 17 | case(Person[Name[Is("Joe"), Is("Roggs")], Is()]) 18 | .then { (a, b, c), d, e -> wrongAnswer }, 19 | case(Person[Name[Is(), Is("Roggs")], Is()]) 20 | .then { (a, b, c), d, e -> wrongAnswer }, 21 | case(Person[Name[Is("Joe"), Is("Bloggs")], Is()]) 22 | .then { (a, b, c), d, e -> Res5(a, b, c, d, e) } 23 | ) 24 | 25 | assertEquals(result, Res5("Joe", 11, "Bloggs", "Jr", 123)) 26 | } 27 | 28 | // Person(Name("Joe" not "Jack", "Bloggs")) 29 | @Test 30 | fun person_name_joe_not_jack_bloggs() { 31 | val result = 32 | on(Person(Name("Joe", 11, "Bloggs"), "Jr", 123)).match( 33 | case(Person[Name[Is(), Is("Roggs")], Is()]) 34 | .then { (a, b, c), d, e -> wrongAnswer }, 35 | case(Person[Name[Is("Jack"), Is()], Is()]) 36 | .then { (a, b, c), d, e -> wrongAnswer }, 37 | case(Person[Name[Is("Joe"), Is("Bloggs")], Is()]) 38 | .then { (a, b, c), d, e -> Res5(a, b, c, d, e) } 39 | ) 40 | 41 | assertEquals(result, Res5("Joe", 11, "Bloggs", "Jr", 123)) 42 | } 43 | 44 | // Person(Name(== "Joe", == "Bloggs" not "Roggs")) 45 | @Test 46 | fun person_name_joe_bloggs_not_roggs2() { 47 | val result = 48 | on(Person(Name("Joe", 11, "Bloggs"), "Jr", 123)).match( 49 | case(Person[Name[Is {it == "Joe"}, Is {it == "Roggs"}], Is()]) 50 | .then { n, m, a -> wrongAnswer }, 51 | case(Person[Name[Is(), Is {it == "Roggs"}], Is()]) 52 | .then { n, m, a -> wrongAnswer }, 53 | case(Person[Name[Is {it == "Joe"}, Is {it == "Bloggs"}], Is()]) 54 | .then { (first, n1, last), suffix, age -> Res3(Res3(first, n1, last), suffix, age) } 55 | ) 56 | 57 | assertEquals(result, Res3(Res3("Joe", 11, "Bloggs"), "Jr", 123)) 58 | } 59 | 60 | // Person(Name(== "Joe" or "Jack", Is)) 61 | @Test 62 | fun person_name_joe_or_jack_is() { 63 | fun isOneOf(vararg ids: String) = Is { ids.contains(it) } 64 | val result = 65 | on(Person(Name("Joe", 11, "Bloggs"), "Jr", 123)).match( 66 | case(Person[Name[isOneOf("Bill", "Will"), Is()], Is()]) 67 | .then { n, m, a -> wrongAnswer }, 68 | case(Person[Name[isOneOf("Joe", "Jack"), Is()], Is()]) 69 | .then { (first, n1, last), suffix, age -> Res5(first, n1, last, suffix, age) } 70 | ) 71 | 72 | assertEquals(result, Res5("Joe", 11, "Bloggs", "Jr", 123)) 73 | } 74 | 75 | // Person(Name(== "Joe" or "Jack", Is)) 76 | @Test 77 | fun person_name_joe_or_jack_is2() { 78 | val result = 79 | on(Person(Name("Joe", 11, "Bloggs"), "Jr", 123)).match( 80 | case(Person[Name[Is("Joe"), Is("Roggs")], Is()]) 81 | .then { (first, n1, last), suffix, age -> wrongAnswer }, 82 | case(Person[Name[Is(), Is("Roggs")], Is()]) 83 | .then { (first, n1, last), suffix, age -> wrongAnswer }, 84 | case(Person[Name[Is("Joe"), Is("Bloggs")], Is(123)]) 85 | .then { (first, n1, last), suffix, age -> Res5(first, n1, last, suffix, age) } 86 | ) 87 | 88 | assertEquals(result, Res5("Joe", 11, "Bloggs", "Jr", 123)) 89 | } 90 | 91 | 92 | // Person(Name("Joe", Is)) - thenIf - Positive 93 | @Test 94 | fun person_name_joe_is_thenIf_positive() { 95 | val result = 96 | on(Person(Name("Joe", 11, "Bloggs"), "Jr", 123)).match( 97 | case(Person[Name[Is("Joe"), Is("Roggs")], Is()]) 98 | .then { (first, n1, last), suffix, age -> wrongAnswer }, 99 | case(Person[Name[Is(), Is("Roggs")], Is()]) 100 | .thenIf { (first, n1, last), suffix, age -> last == "Roggs" } 101 | .then { (first, n1, last), suffix, age -> wrongAnswer }, 102 | case(Person[Name[Is("Joe"), Is("Bloggs")], Is()]) 103 | .thenIf { (first, n1, last), suffix, age -> last == "Bloggs" } 104 | .then { (first, n1, last), suffix, age -> Res5(first, n1, last, suffix, age) } 105 | ) 106 | 107 | assertEquals(result, Res5("Joe", 11, "Bloggs", "Jr", 123)) 108 | } 109 | 110 | // Person(Name("Joe", Is)) - thenIf - Negative 111 | @Test 112 | fun person_name_joe_is_thenIf_negative() { 113 | val result = 114 | on(Person(Name("Joe", 11, "Bloggs"), "Jr", 123)).match( 115 | case(Person[Name[Is("Joe"), Is("Roggs")], Is()]) 116 | .then { (first, n1, last), suffix, age -> wrongAnswer }, 117 | case(Person[Name[Is(), Is("Roggs")], Is()]) 118 | .thenIf { (first, n1, last), suffix, age -> last == "Roggs" } 119 | .then { (first, n1, last), suffix, age -> wrongAnswer }, 120 | case(Person[Name[Is("Joe"), Is("Bloggs")], Is()]) 121 | .then { (first, n1, last), suffix, age -> Res5(first, n1, last, suffix, age) } 122 | ) 123 | 124 | assertEquals(result, Res5("Joe", 11, "Bloggs", "Jr", 123)) 125 | } 126 | 127 | 128 | 129 | 130 | } -------------------------------------------------------------------------------- /decomat-core/src/templates/Pattern3.ftl: -------------------------------------------------------------------------------- 1 | [#ftl] 2 | 3 | 4 | [#macro divideIntoComponentsAnyX num] 5 | [@compress single_line=true] 6 | [#if num == 3]divideInto3ComponentsAny 7 | [#else]divideIntoComponentsAny 8 | [/#if] 9 | [/@compress] 10 | [/#macro] 11 | 12 | [#macro Pattern num modLeft modRight] 13 | [@compress single_line=true] 14 | [#if num == 0]Pattern0 15 | [#elseif num == 1]Pattern1<[@PatternHeads 1 num /], R1, R> 16 | [#elseif num == 2]Pattern2<[@PatternHeads 1 modLeft /], [@PatternHeads 2 modRight /], R1, R2, R> 17 | [#elseif num == 3]Pattern2M<[@PatternHeads 1 modLeft /], M, [@PatternHeads 2 modRight /], R1, R2, R> 18 | [/#if] 19 | [/@compress] 20 | [/#macro] 21 | 22 | [#-- class Then22< 23 | P1: Pattern2, 24 | P11: Pattern, P12: Pattern, R11, R12, R1, 25 | P2: Pattern2, 26 | P21: Pattern, P22: Pattern, R21, R22, R2, R>( --] 27 | 28 | [#macro PatternHeads mod num] 29 | [@compress single_line=true] 30 | [#if num == 0]Pattern0 31 | [#elseif num == 1] 32 | Pattern1, R${mod}1, R${mod}> 33 | [#elseif num == 2] 34 | [#-- Pattern2, Pattern, R11, R12, R1> --] 35 | Pattern2, Pattern, R${mod}1, R${mod}2, R${mod}> 36 | [#elseif num == 3] 37 | [#-- Pattern2M --] 38 | Pattern2M, M${mod}, Pattern, R${mod}1, R${mod}2, R${mod}> 39 | [/#if] 40 | [/@compress] 41 | [/#macro] 42 | 43 | 44 | [#macro PatternVars mod num] 45 | [@compress single_line=true] 46 | [#if num == 0]R${mod} 47 | [#elseif num == 1] 48 | R${mod}1, R${mod} 49 | [#elseif num == 2] 50 | [#-- R11, R12, R1 --] 51 | R${mod}1, R${mod}2, R${mod} 52 | [#elseif num == 3] 53 | [#-- R11, M, R12, R1 --] 54 | R${mod}1, M${mod}, R${mod}2, R${mod} 55 | [/#if] 56 | [/@compress] 57 | [/#macro] 58 | 59 | 60 | [#macro Components mod num] 61 | [@compress single_line=true] 62 | [#if num == 0]R${mod} 63 | [#elseif num == 1]Components1 64 | [#elseif num == 2]Components2 65 | [#elseif num == 3]Components2M 66 | [/#if] 67 | [/@compress] 68 | [/#macro] 69 | 70 | 71 | [#macro ContextComponents left, right] 72 | [@compress single_line=true] 73 | [#if left == 0 && right == 0]Unit 74 | [#elseif left > 0 && right == 0]ContextComponents1L 75 | [#elseif left == 0 && right > 0]ContextComponents1R 76 | [#elseif left > 0 && right > 0]ContextComponents2 77 | [/#if] 78 | [/@compress] 79 | [/#macro] 80 | 81 | [#macro contextOfContextComponents left, right] 82 | [@compress single_line=true] 83 | [#if left == 0 && right == 0] 84 | [#else]([@ContextComponents left, right /]). 85 | [/#if] 86 | [/@compress] 87 | [/#macro] 88 | 89 | [#macro fArgs left, right] 90 | [@compress single_line=true] 91 | [#if left == 0 && right == 0]c1, c2 92 | [#else]cc, c1, c2 93 | [/#if] 94 | [/@compress] 95 | [/#macro] 96 | 97 | [#macro fArgsM left, right] 98 | [@compress single_line=true] 99 | [#if left == 0 && right == 0]c1, m, c2 100 | [#else]cc, c1, m, c2 101 | [/#if] 102 | [/@compress] 103 | [/#macro] 104 | 105 | [#macro ofContextComponents left, right] 106 | [@compress single_line=true] 107 | [#if left == 0 && right == 0]Unit 108 | [#elseif left > 0 && right == 0]ContextComponents.ofLeft(r1, r) 109 | [#elseif left == 0 && right > 0]ContextComponents.ofRight(r2, r) 110 | [#elseif left > 0 && right > 0]ContextComponents.of(r1, r2, r) 111 | [/#if] 112 | [/@compress] 113 | [/#macro] 114 | 115 | 116 | [#macro vars mod max] 117 | [@compress single_line=true] 118 | [#if max == 0]r${max} 119 | [#elseif max == 1 || max == 2]([#list 1..max as a]r${mod}${a}[#sep], [/#sep][/#list]) 120 | [#elseif max == 3](r${mod}1, m${mod}, r${mod}2) 121 | [#else] 122 | [/#if] 123 | [/@compress] 124 | [/#macro] 125 | 126 | 127 | [#macro compVar mod num] 128 | [@compress single_line=true] 129 | [#if num == 0]r${mod} 130 | [#elseif num == 1]Components1(r${mod}1) 131 | [#elseif num == 2]Components2(r${mod}1, r${mod}2) 132 | [#elseif num == 3]Components2M(r${mod}1, m${mod}, r${mod}2) 133 | [/#if] 134 | [/@compress] 135 | [/#macro] 136 | 137 | [#macro compVars2 i1 i2] 138 | [@compress single_line=true] 139 | [@compVar 1 i1 /], [@compVar 2 i2 /] 140 | [/@compress] 141 | [/#macro] 142 | 143 | [#macro compVars2M i1 i2] 144 | [@compress single_line=true] 145 | [@compVar 1 i1 /], m, [@compVar 2 i2 /] 146 | [/@compress] 147 | [/#macro] 148 | 149 | 150 | 151 | [@output file="decomat-core/build/templates/io/decomat/Then2.kt"] 152 | package io.decomat 153 | 154 | [#list 0..3 as i1] 155 | [#list 0..3 as i2] 156 | [#-- fun , P2: Pattern0, P11: Pattern, R11, R1, R2, R> case(pat: Pattern2) = Then10(pat, {true}) --] 157 | 158 | [#-- No 'M' variable for these because these are all based on Pattern2, a separate clause is for Pattern2M variants --] 159 | fun <[@PatternVars 1 i1 /], [@PatternVars 2 i2 /], R> case(pat: [@Pattern 2 i1 i2 /]) = Then${i1}${i2}(pat, {true}) 160 | 161 | [#-- No 'M' variable for these because these are all based on Pattern2, a separate clause is for Pattern2M variants --] 162 | class Then${i1}${i2}<[@PatternVars 1 i1 /], [@PatternVars 2 i2 /], R>( 163 | override val pat: [@Pattern 2 i1 i2 /], 164 | override val check: (R) -> Boolean 165 | ): Stage<[@Pattern 2 i1 i2 /], R> { 166 | 167 | inline fun useComponents(r: R, f: ([@ContextComponents i1, i2 /], [@Components 1 i1 /], [@Components 2 i2 /]) -> O): O { 168 | val (r1, r2) = pat.divideIntoComponentsAny(r as Any) 169 | [#if i1 != 0]val [@vars 1 i1 /] = pat.pattern1.[@divideIntoComponentsAnyX i1 /](r1 as Any)[#else]//skip[/#if] 170 | [#if i2 != 0]val [@vars 2 i2 /] = pat.pattern2.[@divideIntoComponentsAnyX i2 /](r2 as Any)[#else]//skip[/#if] 171 | return f([@ofContextComponents i1, i2 /], [@compVars2 i1, i2 /]) 172 | } 173 | inline fun thenIf(crossinline f: [@contextOfContextComponents i1, i2 /]([@Components 1 i1 /], [@Components 2 i2 /]) -> Boolean) = 174 | Then${i1}${i2}(pat) { v -> check(v) && useComponents(v, { cc, c1, c2 -> f([@fArgs i1 i2 /]) }) } 175 | 176 | inline fun thenIfThis(crossinline f: R.([@Components 1 i1 /], [@Components 2 i2 /]) -> Boolean) = 177 | Then${i1}${i2}(pat) { v -> check(v) && useComponents(v, { _, c1, c2 -> f(v, c1, c2) }) } 178 | 179 | inline fun then(crossinline f: [@contextOfContextComponents i1, i2 /]([@Components 1 i1 /], [@Components 2 i2 /]) -> O) = 180 | StageCase(pat, check) { v -> useComponents(v, { cc, c1, c2 -> f([@fArgs i1 i2 /]) }) } 181 | 182 | inline fun thenThis(crossinline f: R.([@Components 1 i1 /], [@Components 2 i2 /]) -> O) = 183 | StageCase(pat, check) { v -> useComponents(v, { _, c1, c2 -> f(v, c1, c2) }) } 184 | } 185 | [/#list] 186 | [/#list] 187 | 188 | [#-- ******************************************************************************************************* --] 189 | [#-- ***************************** The Variants that are Pattern2M at the base ***************************** --] 190 | [#-- ******************************************************************************************************* --] 191 | 192 | [#list 0..3 as i1] 193 | [#list 0..3 as i2] 194 | 195 | fun <[@PatternVars 1 i1 /], M, [@PatternVars 2 i2 /], R> case(pat: [@Pattern 3 i1 i2 /]) = Then${i1}M${i2}(pat, {true}) 196 | 197 | class Then${i1}M${i2}<[@PatternVars 1 i1 /], M, [@PatternVars 2 i2 /], R>( 198 | override val pat: [@Pattern 3 i1 i2 /], 199 | override val check: (R) -> Boolean 200 | ): Stage<[@Pattern 3 i1 i2 /], R> { 201 | 202 | inline fun useComponents(r: R, f: ([@ContextComponents i1, i2 /], [@Components 1 i1 /], M, [@Components 2 i2 /]) -> O): O { 203 | val (r1, m, r2) = pat.divideInto3ComponentsAny(r as Any) 204 | [#if i1 != 0]val [@vars 1 i1 /] = pat.pattern1.[@divideIntoComponentsAnyX i1 /](r1 as Any)[#else]//skip[/#if] 205 | [#if i2 != 0]val [@vars 2 i2 /] = pat.pattern2.[@divideIntoComponentsAnyX i2 /](r2 as Any)[#else]//skip[/#if] 206 | return f([@ofContextComponents i1, i2 /], [@compVars2M i1, i2 /]) 207 | } 208 | inline fun thenIf(crossinline f: [@contextOfContextComponents i1, i2 /]([@Components 1 i1 /], M, [@Components 2 i2 /]) -> Boolean) = 209 | Then${i1}M${i2}(pat) { v -> check(v) && useComponents(v, { cc, c1, m, c2 -> f([@fArgsM i1 i2 /]) }) } 210 | 211 | inline fun thenIfThis(crossinline f: R.([@Components 1 i1 /], M, [@Components 2 i2 /]) -> Boolean) = 212 | Then${i1}M${i2}(pat) { v -> check(v) && useComponents(v, { _, c1, m, c2 -> f(v, c1, m, c2) }) } 213 | 214 | inline fun then(crossinline f: [@contextOfContextComponents i1, i2 /]([@Components 1 i1 /], M, [@Components 2 i2 /]) -> O) = 215 | StageCase(pat, check) { v -> useComponents(v, { cc, c1, m, c2 -> f([@fArgsM i1 i2 /]) }) } 216 | 217 | inline fun thenThis(crossinline f: R.([@Components 1 i1 /], M, [@Components 2 i2 /]) -> O) = 218 | StageCase(pat, check) { v -> useComponents(v, { _, c1, m, c2 -> f(v, c1, m, c2) }) } 219 | } 220 | [/#list] 221 | [/#list] 222 | 223 | 224 | [/@output] -------------------------------------------------------------------------------- /decomat-examples/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.gradle.api.tasks.testing.logging.TestExceptionFormat 2 | import org.gradle.api.tasks.testing.logging.TestLogEvent 3 | import org.jetbrains.kotlin.com.intellij.psi.impl.source.SourceJavaCodeReference 4 | import org.jetbrains.kotlin.gradle.targets.js.dsl.ExperimentalWasmDsl 5 | 6 | plugins { 7 | kotlin("multiplatform") 8 | signing 9 | id("com.google.devtools.ksp") 10 | } 11 | 12 | kotlin { 13 | val isCI = project.hasProperty("isCI") 14 | val platform = 15 | if (project.hasProperty("platform")) 16 | project.property("platform") 17 | else 18 | "any" 19 | val isLinux = platform == "linux" 20 | val isMac = platform == "mac" 21 | val isWindows = platform == "windows" 22 | 23 | // If we're not the CI build a limited set of standard targets 24 | // If we're not the CI build a limited set of standard targets 25 | jvm { 26 | jvmToolchain(11) 27 | } 28 | 29 | if(!isCI) { 30 | js { 31 | browser() 32 | nodejs() 33 | } 34 | 35 | linuxX64() 36 | macosX64() 37 | mingwX64() 38 | } 39 | 40 | // If we are a CI, build all the targets for the specified platform 41 | if (isLinux && isCI) { 42 | js { 43 | browser() 44 | nodejs() 45 | } 46 | 47 | linuxX64() 48 | linuxArm64() 49 | 50 | @OptIn(ExperimentalWasmDsl::class) 51 | wasmWasi() 52 | @OptIn(ExperimentalWasmDsl::class) 53 | wasmJs() 54 | 55 | androidNativeX64() 56 | androidNativeX86() 57 | androidNativeArm32() 58 | androidNativeArm64() 59 | 60 | // Need to know about this since we publish the -tooling metadata from 61 | // the linux containers. Although it doesn't build these it needs to know about them. 62 | macosX64() 63 | macosArm64() 64 | iosX64() 65 | iosArm64() 66 | iosSimulatorArm64() 67 | tvosX64() 68 | tvosArm64() 69 | watchosX64() 70 | watchosArm32() 71 | watchosArm64() 72 | 73 | mingwX64() 74 | } 75 | 76 | if (isMac && isCI) { 77 | macosX64() 78 | macosArm64() 79 | iosX64() 80 | iosArm64() 81 | iosSimulatorArm64() 82 | tvosX64() 83 | tvosArm64() 84 | watchosX64() 85 | watchosArm32() 86 | watchosArm64() 87 | } 88 | if (isWindows && isCI) { 89 | mingwX64() 90 | } 91 | 92 | 93 | sourceSets { 94 | val commonMain by getting { 95 | kotlin.srcDir("$buildDir/generated/ksp/metadata/commonMain/kotlin") 96 | dependencies { 97 | //kotlin.srcDir("$buildDir/generated/ksp/main/kotlin") 98 | api(project(":decomat-core")) 99 | } 100 | } 101 | 102 | val commonTest by getting { 103 | kotlin.srcDir("$buildDir/generated/ksp/metadata/commonMain/kotlin") 104 | dependencies { 105 | // Used to ad-hoc some examples but not needed. 106 | //api("org.jetbrains.kotlinx:kotlinx-serialization-core:1.6.2") 107 | //implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.5.0") 108 | implementation(kotlin("test")) 109 | implementation(kotlin("test-common")) 110 | implementation(kotlin("test-annotations-common")) 111 | } 112 | } 113 | } 114 | } 115 | 116 | dependencies { 117 | // NOTE. This has failed before on upstream dependencies which turned out 118 | // to cuase errors in `compileCommonMainKotlinMetadata`. This was fixed by adding 119 | // `dependsOn(runFreemarkerTemplate)` specifically for `org.jetbrains.kotlin.gradle.dsl.KotlinCompile<*>`. 120 | // Specifically `./gradlew clean build` and `./gradlew clean;./gradlew build` would fail while 121 | // `./gradlew clean; ./gradlew decomat-core:build; ./gradlew build` would succeed which meant 122 | // that something that `decomat-core` was doing was not getting picked up by decomat-examples 123 | // until a subsequent build. This turned out to be the `runFreemarkerTemplate` task results. 124 | add("kspCommonMainMetadata", project(":decomat-ksp")) 125 | 126 | // Don't think this is needed 127 | //add("kspLinuxX64", project(":decomat-ksp")) 128 | } 129 | 130 | tasks.register("listTasks") { 131 | doLast { 132 | println("Available tasks:") 133 | tasks.forEach { task -> 134 | println("${task.name} - ${task::class.java}") 135 | } 136 | } 137 | } 138 | 139 | tasks.withType>().configureEach { 140 | if (name != "kspCommonMainKotlinMetadata") { 141 | dependsOn("kspCommonMainKotlinMetadata") 142 | } 143 | } 144 | 145 | tasks.withType().configureEach { 146 | if (name != "kspCommonMainKotlinMetadata") { 147 | dependsOn("kspCommonMainKotlinMetadata") 148 | } 149 | } 150 | 151 | // THIS is the actual task used by the KMP multiplatform plugin. 152 | tasks.withType().configureEach { 153 | if (name != "kspCommonMainKotlinMetadata") { 154 | dependsOn("kspCommonMainKotlinMetadata") 155 | } 156 | } 157 | 158 | if (project.hasProperty("platform") && project.property("platform") == "linux") { 159 | tasks.named("jvmTest") { 160 | useJUnitPlatform() 161 | } 162 | } 163 | 164 | tasks.withType().configureEach { 165 | if (name.endsWith("SourcesJar")) { 166 | dependsOn("kspCommonMainKotlinMetadata") 167 | } 168 | } 169 | 170 | // Add the kspCommonMainKotlinMetadata dependency to sourcesJar tasks if needed 171 | // (i.e. in some cases the task e.g. tasks("jsSourcesJar") will not exist) 172 | //tasks.findByName("jsSourcesJar")?.dependsOn("kspCommonMainKotlinMetadata") 173 | //tasks.findByName("jvmSourcesJar")?.dependsOn("kspCommonMainKotlinMetadata") 174 | //tasks.findByName("linuxX64SourcesJar")?.dependsOn("kspCommonMainKotlinMetadata") 175 | //tasks.findByName("mingwX64SourcesJar")?.dependsOn("kspCommonMainKotlinMetadata") 176 | tasks.findByName("sourcesJar")?.dependsOn("kspCommonMainKotlinMetadata") 177 | 178 | tasks.withType().configureEach { 179 | testLogging { 180 | showStandardStreams = true 181 | showExceptions = true 182 | exceptionFormat = TestExceptionFormat.FULL 183 | events(TestLogEvent.STARTED, TestLogEvent.PASSED, TestLogEvent.SKIPPED, TestLogEvent.FAILED) 184 | } 185 | } 186 | 187 | ksp { 188 | arg("measureDuration", "true") 189 | } 190 | -------------------------------------------------------------------------------- /decomat-examples/src/commonMain/kotlin/io/decomat/examples/customPatternData1/Data.kt: -------------------------------------------------------------------------------- 1 | package io.decomat.examples.customPatternData1 2 | 3 | import io.decomat.* 4 | 5 | sealed interface Name 6 | data class SimpleName(val first: String, val last: String): Name 7 | data class FullName(val first: String, val middle: String, val last: String): Name 8 | 9 | @Matchable 10 | data class Person(@Component val name: Name, @Component val age: Int): HasProductClass { 11 | override val productComponents = productComponentsOf(this, name, age) 12 | companion object {} 13 | } 14 | 15 | @Matchable 16 | data class Person1(@Component val name: Name): HasProductClass { 17 | override val productComponents = productComponentsOf(this, name) 18 | companion object {} 19 | } 20 | 21 | // Cannot have this here because it causes: https://youtrack.jetbrains.com/issue/KT-70025/KMPKSP-JavaScript-compile-fails-with-Cannot-read-properties-of-undefined 22 | //object FirstName2 { 23 | // operator fun get(nameString: Pattern0) = 24 | // customPattern1(nameString) { it: Name -> 25 | // when(it) { 26 | // is SimpleName -> Components1(it.first) 27 | // is FullName -> Components1(it.first) 28 | // } 29 | // } 30 | //} -------------------------------------------------------------------------------- /decomat-examples/src/commonMain/kotlin/io/decomat/examples/customPatternData2/Data.kt: -------------------------------------------------------------------------------- 1 | package io.decomat.examples.customPatternData2 2 | 3 | import io.decomat.* 4 | 5 | sealed interface Name 6 | @Matchable 7 | data class SimpleName(@Component val first: String, @Component val last: String): Name, HasProductClass { 8 | override val productComponents = productComponentsOf(this, first, last) 9 | companion object { } 10 | } 11 | 12 | @Matchable 13 | data class FullName(@Component val first: String, val middle: String, @Component val last: String): Name, HasProductClass { 14 | override val productComponents = productComponentsOf(this, first, last) 15 | companion object { } 16 | } 17 | 18 | @Matchable 19 | data class Person(@Component val name: Name, @Component val age: Int): HasProductClass { 20 | override val productComponents = productComponentsOf(this, name, age) 21 | companion object {} 22 | } 23 | 24 | // Cannot have this here because it causes: https://youtrack.jetbrains.com/issue/KT-70025/KMPKSP-JavaScript-compile-fails-with-Cannot-read-properties-of-undefined 25 | //object FirstLast { 26 | // operator fun get(first: Pattern0, last: Pattern0) = 27 | // customPattern2(first, last) { it: Name -> 28 | // on(it).match( 29 | // case(FullName[Is(), Is()]).then { first, last -> Components2(first, last) }, 30 | // case(SimpleName[Is(), Is()]).then { first, last -> Components2(first, last) } 31 | // ) 32 | // } 33 | //} -------------------------------------------------------------------------------- /decomat-examples/src/commonMain/kotlin/io/decomat/examples/querydsl/queryDslCustomExample.kt: -------------------------------------------------------------------------------- 1 | package io.decomat.examples.querydsl 2 | 3 | import io.decomat.* 4 | 5 | sealed interface Query 6 | 7 | @Matchable 8 | data class Nested(@Component val body: Query): Query, HasProductClass> { 9 | override val productComponents = productComponentsOf(this, body) 10 | companion object { 11 | } 12 | } 13 | 14 | @Matchable 15 | data class FlatMap( 16 | @Component val head: Query, 17 | val id: String, 18 | @Component val body: Query 19 | ): Query, HasProductClass> { 20 | override val productComponents = productComponentsOf(this, head, body) 21 | companion object { 22 | } 23 | } 24 | 25 | // Defining it manually 26 | @Matchable 27 | data class Entity(@Component val v: T): Query, HasProductClass> { 28 | override val productComponents = productComponentsOf(this, v) 29 | companion object { 30 | } 31 | } 32 | 33 | object NestedM { 34 | operator fun , A: Query<*>> get(v: AP): Pattern1> = 35 | customPattern1("NestedM", v) { it: Nested<*> -> Components1(it.body) } 36 | } 37 | 38 | object FlatMapM { 39 | operator fun , BP: Pattern, A: Query<*>, B: Query<*>> get(head: AP, body: BP): Pattern2> = 40 | customPattern2("FlatMapM", head, body) { it: FlatMap<*, *> -> Components2(it.head, it.body) } 41 | } 42 | 43 | object EntityM { 44 | operator fun , A> get(v: AP): Pattern1> = 45 | customPattern1("EntityM", v) { it: Entity<*> -> Components1(it.v) } 46 | } 47 | 48 | fun regularMatch() { 49 | val fm = FlatMap(Entity(123), "id", Entity("foo")) 50 | // Need to use this instead of `match` since the types are generic 51 | val out = 52 | on(fm as Query<*>).match( 53 | case(FlatMap[Entity[Is(123)], Is>()]).then { (str), query -> Pair(str, query) } 54 | ) 55 | println(out) 56 | } 57 | 58 | fun customMatch() { 59 | val fm = FlatMap(Entity(123), "id", Entity("foo")) 60 | // Need to use this instead of `match` since the types are generic 61 | val out2 = 62 | on(fm as Query<*>).match( 63 | case(FlatMapM[EntityM[Is(123)], Is>()]).then { (str), query -> Pair(str, query) } 64 | ) 65 | println(out2) 66 | } 67 | 68 | fun main() { 69 | regularMatch() 70 | customMatch() 71 | } 72 | -------------------------------------------------------------------------------- /decomat-examples/src/commonMain/kotlin/io/decomat/examples/querydslcustom/QueryDslCustomExample.kt: -------------------------------------------------------------------------------- 1 | package io.decomat.examples.querydslcustom 2 | 3 | import io.decomat.* 4 | 5 | sealed interface Query 6 | 7 | data class Nested(val body: Query): Query 8 | 9 | data class FlatMap( 10 | val head: Query, 11 | val id: String, 12 | val body: Query 13 | ): Query 14 | 15 | data class Entity(val v: T): Query 16 | 17 | object NestedM { 18 | operator fun , A: Query<*>> get(v: AP): Pattern1> = 19 | customPattern1("NestedM", v) { it: Nested<*> -> Components1(it.body) } 20 | } 21 | 22 | object FlatMapM { 23 | operator fun , BP: Pattern, A: Query<*>, B: Query<*>> get(head: AP, body: BP): Pattern2> = 24 | customPattern2("FlatMapM", head, body) { it: FlatMap<*, *> -> Components2(it.head, it.body) } 25 | } 26 | 27 | object EntityM { 28 | operator fun , A> get(v: AP): Pattern1> = 29 | customPattern1("EntityM", v) { it: Entity<*> -> Components1(it.v) } 30 | } 31 | 32 | fun regularMatch() { 33 | val fm = FlatMap(Entity(123), "id", Entity("foo")) 34 | // Need to use this instead of `match` since the types are generic 35 | val out = 36 | on(fm as Query<*>).match( 37 | case(FlatMapM[EntityM[Is(123)], Is>()]).then { (str), query -> Pair(str, query) } 38 | ) 39 | println(out) 40 | } 41 | 42 | fun customMatch() { 43 | val fm = FlatMap(Entity(123), "id", Entity("foo")) 44 | // Need to use this instead of `match` since the types are generic 45 | val out2 = 46 | on(fm as Query<*>).match( 47 | case(FlatMapM[EntityM[Is(123)], Is>()]).then { (str), query -> Pair(str, query) } 48 | ) 49 | println(out2) 50 | } 51 | 52 | fun main() { 53 | regularMatch() 54 | customMatch() 55 | } 56 | -------------------------------------------------------------------------------- /decomat-examples/src/commonMain/kotlin/io/decomat/examples/querydslmiddle/queryDslCustomMiddleExample.kt: -------------------------------------------------------------------------------- 1 | package io.decomat.examples.querydslmiddle 2 | 3 | import io.decomat.* 4 | 5 | sealed interface Query 6 | 7 | @Matchable 8 | data class Nested(@Component val body: Query): Query, HasProductClass> { 9 | override val productComponents = productComponentsOf(this, body) 10 | companion object { 11 | } 12 | } 13 | 14 | @Matchable(true) 15 | data class FlatMap( 16 | @Component val head: Query, 17 | @MiddleComponent val id: String, 18 | @Component val body: Query 19 | ): Query, HasProductClass> { 20 | override val productComponents = productComponentsOf(this, head, id, body) 21 | companion object { 22 | } 23 | } 24 | 25 | @Matchable 26 | data class Entity(@Component val v: T): Query, HasProductClass> { 27 | override val productComponents = productComponentsOf(this, v) 28 | companion object { 29 | } 30 | } 31 | 32 | /** Testing Custom Patterns */ 33 | // TODO Use these later 34 | // 35 | //object NestedM { 36 | // operator fun , A: Query<*>> get(v: AP): Pattern1> = 37 | // customPattern1(v) { it: Nested<*> -> Components1(it.body) } 38 | //} 39 | // 40 | //object FlatMapM { 41 | // operator fun , BP: Pattern, A: Query<*>, B: Query<*>> get(head: AP, body: BP): Pattern2M> = 42 | // customPattern2M(head, body) { it: FlatMap<*, *> -> Components2M(it.head, it.id, it.body) } 43 | //} 44 | // 45 | //object EntityM { 46 | // operator fun , A> get(v: AP): Pattern1> = 47 | // customPattern1(v) { it: Entity<*> -> Components1(it.v) } 48 | //} 49 | 50 | // TODO This needs to be a test! 51 | fun regularMatch() { 52 | val fm = FlatMap(Entity(123), "id", Entity("foo")) 53 | // Need to use this instead of `match` since the types are generic 54 | val out = 55 | on(fm as Query<*>).match( 56 | //case(FlatMap[Entity[Is(123)], Is>()]).then { (str), middle, query -> Triple(str, middle, query) } 57 | case(FlatMap[Is(), Is()]).then { str, middle, query -> Triple(str, middle, query) } 58 | ) 59 | println(out) 60 | } 61 | 62 | fun main() { 63 | regularMatch() 64 | } 65 | -------------------------------------------------------------------------------- /decomat-examples/src/commonTest/kotlin/io/decomat/examples/CustomPatternTest.kt: -------------------------------------------------------------------------------- 1 | package io.decomat.examples 2 | 3 | import io.decomat.examples.customPatternData1.* 4 | import io.decomat.* 5 | import kotlin.test.Test 6 | import kotlin.test.assertEquals 7 | 8 | class CustomPatternTest { 9 | 10 | @Test 11 | fun notMatch1() { 12 | val p = Person1(SimpleName("John", "Doe")) 13 | val x: Pattern1, String, Name>, Name, Person1> = Person1.get(FirstName2[Is("Jack")]) 14 | val out = 15 | on(p).match( 16 | case(Person1[FirstName2[Is("Jack")]]).then { (firstName) -> firstName } 17 | ) 18 | 19 | assertEquals(out, null) 20 | } 21 | 22 | 23 | @Test 24 | fun notMatch() { 25 | val p = Person(SimpleName("John", "Doe"), 42) 26 | val out = 27 | on(p).match( 28 | case(Person[FirstName2[Is("Jack")], Is()]).then { (firstName), age -> Pair(firstName, age) } 29 | ) 30 | 31 | assertEquals(out, null) 32 | } 33 | 34 | @Test 35 | fun matchOne() { 36 | val p = Person(SimpleName("John", "Doe"), 42) 37 | val out1 = 38 | on(p).match( 39 | case(Person[FirstName2[Is("John")], Is()]).then { (firstName), age -> Pair(firstName, age) } 40 | ) 41 | assertEquals(out1, "John" to 42) 42 | } 43 | 44 | @Test 45 | fun matchOneAny() { 46 | val p = Person(SimpleName("John", "Doe"), 42) 47 | val out1 = 48 | on(p).match( 49 | case(Person[FirstName2[Is()], Is()]).then { (firstName), age -> Pair(firstName, age) } 50 | ) 51 | assertEquals(out1, "John" to 42) 52 | } 53 | } -------------------------------------------------------------------------------- /decomat-examples/src/commonTest/kotlin/io/decomat/examples/CustomPatternTest2.kt: -------------------------------------------------------------------------------- 1 | package io.decomat.examples 2 | 3 | import io.decomat.examples.customPatternData2.* 4 | import io.decomat.* 5 | import kotlin.test.Test 6 | import kotlin.test.assertEquals 7 | 8 | class CustomPatternTest2 { 9 | 10 | @Test 11 | fun notMatchFirst() { 12 | val p = Person(SimpleName("John", "Doe"), 42) 13 | val out = 14 | on(p).match( 15 | case(Person[FirstLast[Is("Jack"), Is()], Is()]).then { (first, last), age -> Triple(first, last, age) } 16 | ) 17 | 18 | assertEquals(out, null) 19 | } 20 | 21 | @Test 22 | fun notMatchSecond() { 23 | val p = Person(SimpleName("John", "Doe"), 42) 24 | val out = 25 | on(p).match( 26 | case(Person[FirstLast[Is(), Is("Daves")], Is()]).then { (first, last), age -> Triple(first, last, age) } 27 | ) 28 | 29 | assertEquals(out, null) 30 | } 31 | 32 | @Test 33 | fun notMatchBoth() { 34 | val p = Person(SimpleName("John", "Doe"), 42) 35 | val out = 36 | on(p).match( 37 | case(Person[FirstLast[Is("Jack"), Is("Daves")], Is()]).then { (first, last), age -> Triple(first, last, age) } 38 | ) 39 | 40 | assertEquals(out, null) 41 | } 42 | 43 | @Test 44 | fun matchTwoFirst() { 45 | val p = Person(SimpleName("John", "Doe"), 42) 46 | val out1 = 47 | on(p).match( 48 | case(Person[FirstLast[Is("John"), Is("Doe")], Is()]).then { (first, last), age -> Triple(first, last, age) } 49 | ) 50 | assertEquals(out1, Triple("John", "Doe", 42)) 51 | } 52 | 53 | @Test 54 | fun matchTwo() { 55 | val p = Person(SimpleName("John", "Doe"), 42) 56 | val out1 = 57 | on(p).match( 58 | case(Person[FirstLast[Is("John"), Is()], Is()]).then { (first, last), age -> Triple(first, last, age) } 59 | ) 60 | assertEquals(out1, Triple("John", "Doe", 42)) 61 | } 62 | 63 | @Test 64 | fun matchTwoAny() { 65 | val p = Person(SimpleName("John", "Doe"), 42) 66 | val out1 = 67 | on(p).match( 68 | case(Person[FirstLast[Is(), Is()], Is()]).then { (first, last), age -> Triple(first, last, age) } 69 | ) 70 | assertEquals(out1, Triple("John", "Doe", 42)) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /decomat-examples/src/commonTest/kotlin/io/decomat/examples/CustomPatternTestData.kt: -------------------------------------------------------------------------------- 1 | package io.decomat.examples 2 | 3 | import io.decomat.Components1 4 | import io.decomat.Pattern0 5 | import io.decomat.customPattern1 6 | import io.decomat.examples.customPatternData1.* 7 | 8 | // TODO move to commonTest when KT-70025 is fixed 9 | object FirstName2 { 10 | operator fun get(nameString: Pattern0) = 11 | customPattern1("FirstName2", nameString) { it: Name -> 12 | when(it) { 13 | is SimpleName -> Components1(it.first) 14 | is FullName -> Components1(it.first) 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /decomat-examples/src/commonTest/kotlin/io/decomat/examples/CustomPatternTestData2.kt: -------------------------------------------------------------------------------- 1 | package io.decomat.examples 2 | 3 | import io.decomat.Components2 4 | import io.decomat.Pattern0 5 | import io.decomat.customPattern2 6 | import io.decomat.examples.customPatternData2.* 7 | import io.decomat.* 8 | 9 | // TODO move to commonTest when KT-70025 is fixed 10 | object FirstLast { 11 | operator fun get(first: Pattern0, last: Pattern0) = 12 | customPattern2("FirstLast", first, last) { it: Name -> 13 | on(it).match( 14 | case(FullName[Is(), Is()]).then { first, last -> Components2(first, last) }, 15 | case(SimpleName[Is(), Is()]).then { first, last -> Components2(first, last) } 16 | ) 17 | } 18 | } -------------------------------------------------------------------------------- /decomat-examples/src/commonTest/kotlin/io/decomat/examples/QueryPatternCustomTest.kt: -------------------------------------------------------------------------------- 1 | package io.decomat.examples 2 | 3 | import io.decomat.* 4 | import io.decomat.examples.querydslcustom.* 5 | import kotlin.test.Test 6 | import kotlin.test.assertEquals 7 | 8 | /** 9 | * Very important to test situation with custom AST types where no ProductClass 10 | * for the AST type actually exists. 11 | */ 12 | @Suppress("NAME_SHADOWING") 13 | class QueryPatternCustomTest { 14 | @Test 15 | fun flatMapCustomm() { 16 | val fm = FlatMap(Entity(123), "id", Entity("foo")) 17 | val out = 18 | on(fm as Query<*>).match( // 19 | case(FlatMapM[EntityM[Is(123)], Is>()]).then { (str), query -> Pair(str, query) } 20 | ) 21 | assertEquals(out, Pair(123, Entity("foo"))) 22 | } 23 | 24 | @Test 25 | fun singleNestedRegular() { 26 | val ent = Nested(Entity(123)) 27 | val wrongResult = Entity("wrong-result") 28 | 29 | val r = NestedM.get(NestedM[Is>()]) 30 | 31 | val out = 32 | on(ent as Query<*>).match( 33 | case(NestedM[NestedM[Is>()]]) 34 | .then { ent -> wrongResult }, 35 | case(NestedM[Is>()]) 36 | .then { ent -> ent } // 37 | ) 38 | assertEquals(Entity(123), out) 39 | } 40 | 41 | @Test 42 | fun doubleNestedRegular() { 43 | val ent = Nested(Nested(Entity(123))) 44 | val out = 45 | on(ent as Query<*>).match( 46 | case(NestedM[NestedM[Is>()]]) 47 | .then { (ent) -> ent } 48 | ) 49 | assertEquals(out, Entity(123)) 50 | } 51 | } -------------------------------------------------------------------------------- /decomat-examples/src/commonTest/kotlin/io/decomat/examples/QueryPatternTest.kt: -------------------------------------------------------------------------------- 1 | package io.decomat.examples 2 | 3 | import io.decomat.* 4 | import io.decomat.examples.querydsl.* 5 | import kotlin.test.Test 6 | import kotlin.test.assertEquals 7 | 8 | @Suppress("NAME_SHADOWING") 9 | class QueryPatternTest { 10 | @Test 11 | fun flatMapRegular() { 12 | val fm = FlatMap(Entity(123), "id", Entity("foo")) 13 | val out = 14 | on(fm as Query<*>).match( 15 | case(FlatMap[Entity[Is(123)], Is>()]).then { (str), query -> Pair(str, query) } 16 | ) 17 | assertEquals(out, Pair(123, Entity("foo"))) 18 | } 19 | 20 | @Test 21 | fun flatMapCustomm() { 22 | val fm = FlatMap(Entity(123), "id", Entity("foo")) 23 | val out = 24 | on(fm as Query<*>).match( 25 | case(FlatMapM[EntityM[Is(123)], Is>()]).then { (str), query -> Pair(str, query) } 26 | ) 27 | assertEquals(out, Pair(123, Entity("foo"))) 28 | } 29 | 30 | @Test 31 | fun singleNestedRegular() { 32 | val ent = Nested(Entity(123)) 33 | val wrongResult = Entity("wrong-result") 34 | val out = 35 | on(ent as Query<*>).match( 36 | case(NestedM[NestedM[Is>()]]) 37 | .then { ent -> wrongResult }, 38 | case(Nested[Is>()]) 39 | .then { ent -> ent } 40 | ) 41 | assertEquals(Entity(123), out) 42 | } 43 | 44 | @Test 45 | fun doubleNestedRegular() { 46 | val ent = Nested(Nested(Entity(123))) 47 | val out = 48 | on(ent as Query<*>).match( 49 | case(NestedM[NestedM[Is>()]]) 50 | .then { (ent) -> ent } 51 | ) 52 | assertEquals(out, Entity(123)) 53 | } 54 | } -------------------------------------------------------------------------------- /decomat-ksp/build.gradle.kts: -------------------------------------------------------------------------------- 1 | @Suppress("DSL_SCOPE_VIOLATION") 2 | plugins { 3 | kotlin("multiplatform") 4 | id("com.google.devtools.ksp") 5 | id("maven-publish") 6 | idea 7 | signing 8 | id("org.jetbrains.dokka") 9 | } 10 | 11 | kotlin { 12 | jvm { 13 | jvmToolchain(11) 14 | } 15 | 16 | sourceSets { 17 | jvmMain { 18 | kotlin.srcDir("src/main/kotlin") 19 | resources.srcDir("src/main/resources") 20 | 21 | dependencies { 22 | project(":decomat-core") 23 | implementation("com.google.devtools.ksp:symbol-processing-api:1.8.20-1.0.11") 24 | 25 | //implementation("com.facebook:ktfmt:0.43") 26 | implementation(kotlin("reflect")) 27 | } 28 | } 29 | } 30 | 31 | } 32 | 33 | 34 | //publishing { 35 | // val varintName = project.name 36 | // 37 | // val dokkaHtml by tasks.getting(org.jetbrains.dokka.gradle.DokkaTask::class) 38 | // 39 | // tasks { 40 | // val sourcesJar by creating(Jar::class) { 41 | // archiveClassifier.set("sources") 42 | // from(sourceSets["main"].allSource) 43 | // } 44 | // } 45 | // 46 | // publications { 47 | // create("mavenJava") { 48 | // from(components["kotlin"]) 49 | // artifactId = varintName 50 | // 51 | // artifact(tasks["sourcesJar"]) 52 | // 53 | // pom { 54 | // name.set("decomat") 55 | // description.set("DecoMat - Deconstructive Pattern Matching for Kotlin") 56 | // url.set("https://github.com/exoquery/decomat") 57 | // 58 | // licenses { 59 | // license { 60 | // name.set("The Apache Software License, Version 2.0") 61 | // url.set("http://www.apache.org/licenses/LICENSE-2.0.txt") 62 | // distribution.set("repo") 63 | // } 64 | // } 65 | // 66 | // developers { 67 | // developer { 68 | // name.set("Alexander Ioffe") 69 | // email.set("deusaquilus@gmail.com") 70 | // organization.set("github") 71 | // organizationUrl.set("http://www.github.com") 72 | // } 73 | // } 74 | // 75 | // scm { 76 | // url.set("https://github.com/exoquery/decomat/tree/main") 77 | // connection.set("scm:git:git://github.com/ExoQuery/DecoMat.git") 78 | // developerConnection.set("scm:git:ssh://github.com:ExoQuery/DecoMat.git") 79 | // } 80 | // } 81 | // } 82 | // } 83 | //} -------------------------------------------------------------------------------- /decomat-ksp/src/main/kotlin/io/decomat/DecomatProcessor.kt: -------------------------------------------------------------------------------- 1 | package io.decomat 2 | 3 | import com.google.devtools.ksp.getDeclaredProperties 4 | import com.google.devtools.ksp.processing.* 5 | import com.google.devtools.ksp.symbol.* 6 | 7 | 8 | class DecomatProcessor( 9 | private val logger: KSPLogger, 10 | val codeGenerator: CodeGenerator, 11 | val matchableAnnotationName: String, 12 | val componentAnnotationName: String, 13 | val middleComponentAnnotationName: String, 14 | val constructorComponentAnnotationName: String, 15 | val renderAdtFunctions: Boolean, 16 | val renderFromHereFunction: Boolean, 17 | val fromHereFunctionName: String, 18 | val fromFunctionName: String 19 | ) : SymbolProcessor { 20 | 21 | private val Fail = object { 22 | fun CannotAnnotateParameter(symbol: KSNode, annotName: String) = logger.error("Cannot annotate the parameter ${symbol}. Only val-parameters are allowed to be annotated via @${annotName}.", symbol) 23 | fun CannotAnnotateMutableParameter(symbol: KSNode, annotName: String) = logger.error("Cannot annotate the mutable parameter ${symbol}. Only non-mutable parameters are allowed to be annotated via @${annotName}.", symbol) 24 | fun PropertyHasNoName(symbol: KSValueParameter, annotName: String) = logger.error("The property ${symbol} has no name. It must have a name to be annotated with @${annotName}.", symbol) 25 | } 26 | 27 | data class PropertyHolder(val name: String, val type: KSType, val fieldType: Type, val componentType: ComponentType, val annotationName: String) { 28 | sealed interface Type { 29 | object Constructor: Type 30 | object Member: Type 31 | } 32 | sealed interface ComponentType { 33 | object Component: ComponentType 34 | object MiddleComponent: ComponentType 35 | object ConstructorComponent: ComponentType 36 | } 37 | fun isRegularComponent() = componentType == ComponentType.Component 38 | fun isMiddleComponent() = componentType == ComponentType.MiddleComponent 39 | fun isConstructorComponent() = componentType == ComponentType.ConstructorComponent 40 | } 41 | 42 | override fun process(resolver: Resolver): List { 43 | val symbols = resolver.getSymbolsWithAnnotation("io.decomat.Matchable") 44 | 45 | fun findComponents(sym: KSClassDeclaration): List { 46 | val possibleAnnotations: List = listOf(componentAnnotationName, middleComponentAnnotationName, constructorComponentAnnotationName) 47 | fun Sequence.findAnnotations(): KSAnnotation? { 48 | val foundAnnots = filter { annot -> possibleAnnotations.contains(annot.shortName.getShortName()) }.toList() 49 | if (foundAnnots.size > 1) { 50 | logger.error("The symbol $sym has more than one @${possibleAnnotations} annotation. Only one is allowed.") 51 | } 52 | return foundAnnots.firstOrNull() 53 | } 54 | fun KSValueParameter.assertIsVal(): Unit = if (!isVal) Fail.CannotAnnotateMutableParameter(this, possibleAnnotations.toString()) else Unit 55 | fun KSValueParameter.assertHasName(): String = Fail.PropertyHasNoName(this, possibleAnnotations.toString()).let { "" } 56 | fun KSPropertyDeclaration.assertNotMutable(): Unit = if (isMutable) Fail.CannotAnnotateMutableParameter(this, possibleAnnotations.toString()) else Unit 57 | fun componentType(annot: KSAnnotation) = 58 | when (annot.shortName.getShortName()) { 59 | componentAnnotationName -> PropertyHolder.ComponentType.Component 60 | middleComponentAnnotationName -> PropertyHolder.ComponentType.MiddleComponent 61 | constructorComponentAnnotationName -> PropertyHolder.ComponentType.ConstructorComponent 62 | else -> throw IllegalArgumentException("Unknown component type: ${annot.shortName.getShortName()}") 63 | } 64 | 65 | return (sym.primaryConstructor?.parameters?.mapNotNull { param -> 66 | val foundAnnot = param.annotations.findAnnotations() 67 | if (foundAnnot != null) param.assertIsVal() 68 | foundAnnot?.let { it to param } 69 | } ?: emptyList()).map { (foundAnnot, param) -> 70 | PropertyHolder(param.name?.getShortName() ?: param.assertHasName(), param.type.resolve(), PropertyHolder.Type.Constructor, componentType(foundAnnot), foundAnnot.shortName.getShortName()) 71 | } + 72 | (sym.getAllProperties().mapNotNull { prop -> 73 | val foundAnnot = prop.annotations.findAnnotations() 74 | prop.assertNotMutable() 75 | foundAnnot?.let { it to prop } 76 | }).map { (foundAnnot, prop) -> 77 | PropertyHolder(prop.simpleName.getShortName(), prop.type.resolve(), PropertyHolder.Type.Member, componentType(foundAnnot), foundAnnot.shortName.getShortName()) 78 | } 79 | } 80 | 81 | data class ComponentsToGen(val sym: KSClassDeclaration, val componentElements: List, val useStarProjection: Boolean, val productComponents: KSPropertyDeclaration?) 82 | 83 | val componentsToGen = 84 | symbols 85 | .mapNotNull { sym -> 86 | when { 87 | sym is KSClassDeclaration && sym.classKind == ClassKind.CLASS -> { 88 | val useStarProjection = 89 | sym.annotations.find { it.shortName.getShortName() == matchableAnnotationName }?.arguments?.get(0)?.value as? Boolean ?: false 90 | 91 | val componentElements = findComponents(sym) 92 | // Only actually allowed to have one of these but keep a list anyhow 93 | 94 | if (componentElements.isEmpty()) { 95 | logger.error("No @Component parameters found in the primary constructor of the class $sym (They must be the annotation @${matchableAnnotationName})") 96 | } 97 | 98 | val hasProductClass = sym.superTypes.any { it.resolve().declaration.qualifiedName?.asString() == "io.decomat.HasProductClass" } 99 | val productClass = sym.superTypes.any { it.resolve().declaration.qualifiedName?.asString() == "io.decomat.ProductClass" } 100 | 101 | //logger.warn("Super types for: ${sym}: ${sym.superTypes.toList()}") 102 | //sym.superTypes.forEach { logger.warn("----------- Qualified Class: ${it.resolve().declaration.qualifiedName?.asString()}") } 103 | 104 | val productComponents = 105 | if (hasProductClass) { 106 | val productComponents = sym.getDeclaredProperties().find { it.simpleName.getShortName() == "productComponents" } 107 | if (productComponents == null) { 108 | logger.error("The class $sym is a subtype of io.decomat.HasProductClass but does not have a property `productComponents` this should be impossible.") 109 | } 110 | productComponents 111 | } 112 | else null 113 | 114 | if (!hasProductClass && !productClass) { 115 | logger.error(""" 116 | The class $sym is not a subtype of io.decomat.HasProductClass or io.decomat.ProductClass. 117 | In order to be able to annotate this class with @${matchableAnnotationName} do make it extend HasProductClass 118 | and add a `productComponents` product approximately like this: 119 | (plus/minus any generic parameters and any subclasses you may want to add...) 120 | --- 121 | data class $sym(...): HasProductClass<$sym> { 122 | override val productComponents = productComponentsOf(this, ${componentElements.joinToString(", ")}) 123 | // Need at least an empty companion object, can put whatever you want inside 124 | companion object { } 125 | } 126 | """.trimIndent()) 127 | } 128 | ComponentsToGen(sym, componentElements, useStarProjection, productComponents) 129 | } 130 | else -> 131 | null 132 | } 133 | }.toList() 134 | 135 | if (componentsToGen.isEmpty()) 136 | logger.warn("No classes found with the @Matchable interface.") 137 | else { 138 | val description = componentsToGen.map { (cls, comps) -> "${cls.simpleName.getShortName()}(${comps.map { it.name }.joinToString(", ")})" }.joinToString(", ") 139 | logger.warn("Found the following classes/components with the @Matchable/@Component annotations: ${description}") 140 | } 141 | 142 | componentsToGen.forEach { (cls, members, useStarProjection, productComponents) -> 143 | val middleMembers = members.filter { it.isMiddleComponent() } 144 | if (middleMembers.size > 1) { 145 | logger.error("The Matchable class ${cls.simpleName.asString()} has more than one @${middleComponentAnnotationName} components (i.e. ${members.size}). No more than 1 is supported so far.") 146 | } 147 | 148 | val patternMatchMembers = members.filter { it.isRegularComponent() } 149 | if (patternMatchMembers.size > 2) { 150 | logger.error("The Matchable class ${cls.simpleName.asString()} has more than two @${componentAnnotationName} components (i.e. ${members.size}). No more than 2 are supported so far.") 151 | } 152 | 153 | generateExtensionFunction(GenModel.fromClassAndMembers(cls, members, useStarProjection, productComponents)) 154 | } 155 | 156 | return listOf() 157 | } 158 | 159 | sealed interface ModelType { 160 | data class A(val a: Member): ModelType 161 | data class AB(val a: Member, val b_ab: Member): ModelType 162 | data class AMB(val a: Member, val m: Member, val b_amb: Member): ModelType 163 | object None: ModelType 164 | } 165 | 166 | data class GenModel private constructor(val imports: List, val allMembers: List, val ksClass: KSClassDeclaration, val useStarProjection: Boolean, val productComponents: KSPropertyDeclaration?) { 167 | val members = allMembers.filter { it.field.isRegularComponent() } 168 | val middleMembers = allMembers.filter { it.field.isMiddleComponent() } 169 | 170 | val packageName = ksClass.packageName.asString() 171 | val className = ksClass.simpleName.asString() 172 | val fullClassName = ksClass.qualifiedName?.asString() 173 | // since we are going to add components to the imports, use regular name for the use-site 174 | // also, we want star projections e.g. for FlatMap use FlatMap<*, *> 175 | val parametrizedName = 176 | if (ksClass.typeParameters.isNotEmpty()) 177 | // Actually get the full projection of hte class e.g. Query instead of Query<*>. Not sure how to actually get `Query` as a string from a KSClassDeclaration 178 | "${ksClass.simpleName.getShortName()}<${ksClass.typeParameters.map { it.name.getShortName() }.joinToString(", ")}>" //.asStarProjectedType().toString() 179 | else 180 | // Otherwise it's not a star-projection and we can use the plain class name 181 | ksClass.toString() 182 | 183 | // using the logic above removes star projections but seems to be too strict logically 184 | val useSiteName = 185 | if (useStarProjection && ksClass.typeParameters.isNotEmpty()) 186 | ksClass.asStarProjectedType().toString() 187 | else 188 | parametrizedName 189 | 190 | val starProjectedName = 191 | ksClass.asStarProjectedType().toString() 192 | 193 | // TODO This generates Pattern> instead of Query<*>. Explore situations where this shuold be the case 194 | //else if (ksClass.typeParameters.isNotEmpty()) 195 | //// Actually get the full projection of hte class e.g. Query instead of Query<*>. Not sure how to actually get `Query` as a string from a KSClassDeclaration 196 | // "${ksClass.simpleName.getShortName()}<${ksClass.typeParameters.map { it.name.getShortName() }.joinToString(", ")}>" //.asStarProjectedType().toString() 197 | //else 198 | //// Otherwise it's not a star-projection and we can use the plain class name 199 | // ksClass.toString() 200 | 201 | fun toModelType(): ModelType = 202 | when { 203 | members.size == 1 && middleMembers.isEmpty() -> ModelType.A(members[0]) 204 | members.size == 2 && middleMembers.isEmpty() -> ModelType.AB(members[0], members[1]) 205 | members.size == 2 && middleMembers.size == 1 -> ModelType.AMB(members[0], middleMembers[0], members[1]) 206 | else -> ModelType.None 207 | } 208 | 209 | companion object { 210 | fun fromClassAndMembers(annotationHolderClass: KSClassDeclaration, params: List, useStarProjection: Boolean, productComponents: KSPropertyDeclaration?): GenModel { 211 | 212 | fun parseRegularParam(param: PropertyHolder, ksClass: KSClassDeclaration): Member { 213 | val tpe = param.type 214 | 215 | // if it is used as a type-parameter for the class, don't use it 216 | val fullName = ksClass.qualifiedName?.asString() 217 | 218 | // for the parameters, if they have generic types you we want those to have starts e.g. Query<*> if it's Query 219 | // NOTE: Removing `.starProjection()` makes type work (mostly) but seems to make matching too strict 220 | val name = 221 | tpe.let { paramTpe -> 222 | if (useStarProjection) 223 | paramTpe.starProjection().toString() 224 | else 225 | paramTpe.toString() 226 | } 227 | 228 | return Member(name, fullName, param, false) 229 | } 230 | 231 | val members = params.map { 232 | val decl = it.type.declaration 233 | when (decl) { 234 | is KSClassDeclaration -> parseRegularParam(it, decl) 235 | is KSTypeParameter -> Member(it.type.toString(), null, it, true) 236 | else -> throw IllegalArgumentException("Unknown declaration type: ${decl}") 237 | } 238 | } 239 | 240 | val classFullNameListElem = 241 | annotationHolderClass.qualifiedName?.asString()?.let { listOf(it) } ?: listOf() 242 | 243 | fun recurseGetArgs(type: KSType): List = 244 | type.arguments.flatMap { arg -> 245 | arg.type?.resolve()?.let { argType -> 246 | when (argType.declaration) { 247 | is KSClassDeclaration -> listOf(argType) + recurseGetArgs(argType) 248 | else -> listOf() 249 | } 250 | } ?: listOf() 251 | } 252 | 253 | val additionalImports = 254 | classFullNameListElem + 255 | members.mapNotNull { it.importName } + 256 | // When elements themselves have things that need to be imported e.g. `data class Something(val content: List)` make sure to import `OtherStuff` 257 | members.flatMap { 258 | recurseGetArgs(it.field.type).map { it.declaration.qualifiedName?.asString() }.filterNotNull() 259 | } 260 | 261 | 262 | return GenModel(defaultImports + additionalImports.distinct(), members, annotationHolderClass, useStarProjection, productComponents) 263 | } 264 | 265 | val defaultImports = listOf( 266 | "io.decomat.Pattern", 267 | "io.decomat.Pattern1", 268 | "io.decomat.Pattern2", 269 | "io.decomat.Pattern2M", 270 | "io.decomat.Typed", 271 | "io.decomat.Is" 272 | ) 273 | 274 | } 275 | } 276 | 277 | /** 278 | * The field class-name is based on simple-name which has any generic parameters in it. 279 | * The import-name is based on the qualified name which does not have generics (which is perfect for imports) 280 | */ 281 | data class Member(val className: String, val importName: String?, val field: PropertyHolder, val isGeneric: Boolean) { 282 | val fieldName = field.name 283 | } 284 | 285 | /** 286 | * Based on the number of @Component/@MiddleComponent parameters, validate that the productComponents property is of the correct type 287 | * for example: 288 | * if there is one @Component parameter, the productComponents property must be of type io.decomat.ProductClass1 289 | * if there are two @Compoent parameters, the productComponents property must be of type io.decomat.ProductClass2 290 | * if there are two @Compoent parameters and one @MiddleComponent parameter, the productComponents property must be of type io.decomat.ProductClass2M 291 | */ 292 | private fun validateProductComponentsType(modelType: ModelType, productComponents: KSPropertyDeclaration) { 293 | val productClassName = productComponents.type.resolve().declaration.qualifiedName?.asString() 294 | val parentClass = productComponents.parentDeclaration?.qualifiedName?.asString() 295 | val addendum = "Perhaps you are passing in too many or too few properties into the function `productComponentsOf`?" 296 | 297 | when { 298 | modelType is ModelType.A && productClassName != "io.decomat.ProductClass1" -> 299 | logger.error("The productComponents property must be of type io.decomat.ProductClass1 in the class ${parentClass} but instead it was ${productClassName}. ${addendum}", productComponents) 300 | modelType is ModelType.AB && productClassName != "io.decomat.ProductClass2" -> 301 | logger.error("The productComponents property must be of type io.decomat.ProductClass2 in the class ${parentClass} but instead it was ${productClassName}. ${addendum}", productComponents) 302 | modelType is ModelType.AMB && productClassName != "io.decomat.ProductClass2M" -> 303 | logger.error("The productComponents property must be of type io.decomat.ProductClass2M in the class ${parentClass} but instead it was ${productClassName}. ${addendum}", productComponents) 304 | else -> {} 305 | } 306 | } 307 | 308 | private fun generateExtensionFunction(model: GenModel) { 309 | val packageName = model.packageName 310 | val className = model.className 311 | val classUseSiteName = model.useSiteName 312 | val classStarProjectedName = model.starProjectedName 313 | val companionName = model.className 314 | 315 | fun Member.primitiveOrNull() = if (this.className == "String") "String" else null 316 | fun Member.isPrimitive() = this.className == "String" 317 | val members = model.members 318 | val allMembers = model.allMembers 319 | 320 | val modelType = model.toModelType() 321 | 322 | if (model.productComponents != null) validateProductComponentsType(modelType, model.productComponents) 323 | 324 | val file = codeGenerator.createNewFile( 325 | Dependencies.ALL_FILES, packageName, "${className}DecomatExtensions" 326 | ) 327 | 328 | file.bufferedWriter().use { writer -> 329 | fun eachGenericLetter(f: Member.(String) -> String) = 330 | model.members.withIndex().filter { (_, member) -> member.className != "String" }.map { (num, member) -> 331 | val letter = ('a' + num).uppercase() 332 | f(member, letter) 333 | } 334 | 335 | fun eachLetter(f: Member.(String) -> String) = 336 | model.members.filter { it.field.isRegularComponent() }.withIndex().map { (num, member) -> 337 | val letter = ('a' + num).uppercase() 338 | f(member, letter) 339 | } 340 | 341 | fun eachTemplateParam(f: Member.(String) -> String) = 342 | model.members.withIndex().map { (num, member) -> 343 | val letter = ('a' + num).uppercase() 344 | if (member.isPrimitive()) 345 | member.className 346 | else 347 | f(member, letter) 348 | } 349 | 350 | val memberParams = model.members.map { it.className } 351 | 352 | val typeParams = 353 | if (model.useStarProjection) { 354 | model.ksClass.typeParameters 355 | // only use top-level parameters defined in our classes since we are star-projecting everything inside 356 | // e.g. if our class is FlatMap, Query> we don't want to include the T and R, in the parameters 357 | // list because the match will actually be done on FlatMap, Query<*>> and the `operator function get` 358 | // that returns FlatMap_M won't know what these parameters are requiring them to be specified explicitly. 359 | // On the other hand, if it's a top-level like Entity then we should include it. 360 | .filter { memberParams.contains(it.name.getShortName()) } 361 | .mapNotNull { it.name.asString() } 362 | } else { 363 | model.ksClass.typeParameters.map { it.name.getShortName() } 364 | } 365 | 366 | //logger.warn("---------- Type Params: ${typeParams}") 367 | 368 | val mString = if (model.middleMembers.isNotEmpty()) "M" else "" 369 | 370 | 371 | 372 | // Generate: A: Pattern, B: Pattern 373 | val pats = 374 | when (modelType) { 375 | is ModelType.A -> "A: Pattern<${ members[0].primitiveOrNull() ?: "AP" }>" 376 | is ModelType.AB -> "A: Pattern<${ members[0].primitiveOrNull() ?: "AP" }>, B: Pattern<${ members[1].primitiveOrNull() ?: "BP" }>" 377 | is ModelType.AMB -> "A: Pattern<${ members[0].primitiveOrNull() ?: "AP" }>, B: Pattern<${ members[1].primitiveOrNull() ?: "BP" }>" // No M is defined here because it is a concrete type in patLetters below 378 | is ModelType.None -> "" 379 | } 380 | 381 | val patLetters = 382 | when (modelType) { 383 | // e.g. the A in Pattern1 384 | is ModelType.A -> listOf("A") 385 | // e.g. the A and B in Pattern2 386 | is ModelType.AB -> listOf("A", "B") 387 | // e.g. the A and String and B in Pattern2M 388 | // I.e. the actual M-parameter is a concrete type per the data-class that XYZ_M class is being defined for 389 | is ModelType.AMB -> listOf("A", modelType.m.className, "B") 390 | is ModelType.None -> listOf() 391 | } 392 | 393 | fun List.commaSep() = joinToString(", ") 394 | 395 | // Generate: AP: Query, BP: Query 396 | val patTypes = (eachGenericLetter { "${it}P: ${this.className}" } + typeParams) 397 | // Generate: Pattern2 398 | val subClass = "Pattern${model.members.size}${mString}<${(patLetters + eachTemplateParam { "${it}P" } + listOf(classUseSiteName)).commaSep()}>" 399 | // Generate: a: A, b: B 400 | val functionsParamsTypes = eachLetter { "${it.lowercase()}: $it" }.commaSep() 401 | // Generate: val a: A, val b: B 402 | val classValsTypes = eachLetter { "val ${it.lowercase()}: $it" }.commaSep() 403 | // Generate: a, b 404 | val classVals = eachLetter { "${it.lowercase()}" }.commaSep() 405 | 406 | val memberKeyValues = 407 | allMembers.map { "${it.field.name}: ${it.field.type.toString()}" }.commaSep() 408 | 409 | 410 | val (genericBlock, hasGenerics) = run { 411 | //val genericFieldParams = allMembers.filter { it.isGeneric }.map { it.qualifiedClassName ?: it.className } 412 | val genericClassParams = model.ksClass.typeParameters.map { it.toString() } 413 | val genericParams = genericClassParams 414 | val hasGenerics = genericParams.isNotEmpty() 415 | val genericBlock = 416 | if (hasGenerics) "<${genericParams.commaSep()}>" else "" 417 | genericBlock to hasGenerics 418 | } 419 | 420 | val isExpression = 421 | if (!hasGenerics) """val ${companionName}.Companion.Is get() = Is<$classUseSiteName>()""" else "" 422 | 423 | val fromHereFunction = 424 | if (renderFromHereFunction) { 425 | """ 426 | // A "Copy from Self" Helper function for ADTs that allows you to copy an element X from inside of a this@X 427 | // e.g. if you have a data class FlatMap(val head: Query, val id: String, val body: Query) 428 | // you can copy it from inside of a FlatMap (i.e. a this@FlatMap) like this: FlatMap.fromHere(head, id, body) 429 | context(${model.parametrizedName}) fun ${genericBlock} ${companionName}.Companion.${fromHereFunctionName}(${memberKeyValues}) = 430 | Copy${className}(${allMembers.map { it.fieldName }.commaSep()}).invoke(this@${className}) 431 | """.trimIndent() 432 | } else "" 433 | 434 | writer.apply { 435 | // class FlatMap_M, B: Pattern, AP: Query, BP: Query>(a: A, b: B): Pattern2(a, b, Typed()) 436 | //operator fun , B: Pattern, AP: Query, BP: Query> FlatMap.Companion.get(a: A, b: B) = FlatMap_M(a, b) 437 | 438 | // include the ' ' in joinToString below since that is the margin of the fileContent variable that needs to be in 439 | // front of everything, otherwise it won't be stripped properly 440 | val generics = (listOf(pats) + patTypes).commaSep() 441 | 442 | val fileContent = 443 | """ 444 | package $packageName 445 | 446 | ${model.imports.map { "import $it" }.joinToString("\n ")} 447 | 448 | data class ${className}_M<${generics}>($classValsTypes): $subClass($classVals, Typed<$classUseSiteName>()) 449 | operator fun <${generics}> ${companionName}.Companion.get($functionsParamsTypes) = ${className}_M($classVals) 450 | val ${companionName}.Companion.Is get() = Is<$classStarProjectedName>() 451 | """.trimIndent() 452 | 453 | val adtFunctions = 454 | """ 455 | class Copy${className}${genericBlock}(${allMembers.map { "val ${it.fieldName}: ${it.field.type.toString()}" }.commaSep()}) { 456 | operator fun invoke(original: ${model.parametrizedName}) = 457 | original.copy(${allMembers.map { "${it.fieldName} = ${it.fieldName}" }.commaSep()}) 458 | } 459 | 460 | // Helper function for ADTs that allows you to easily copy the element mentioning only the properties you care about 461 | // Typically in an ADT you have a lot of properties and only one or two define the actual structure of the object 462 | // this means that you want to explicitly state only the structural ADT properties and implicitly copy the rest. 463 | fun ${genericBlock} ${companionName}.Companion.${fromFunctionName}(${memberKeyValues}) = Copy${className}(${allMembers.map { it.fieldName }.commaSep()}) 464 | 465 | ${fromHereFunction} 466 | 467 | data class Id${className}${genericBlock}(${allMembers.map { "val ${it.fieldName}: ${it.className}" }.commaSep()}) 468 | 469 | // A helper function that creates a ADT-identifier from the structural properties of the ADT. 470 | // Typically in an ADT you have a lot of properties and only one or two define the actual structure of the object 471 | // and those are the only ones on which you want the `equals` and `hashCode` to be based. 472 | fun ${genericBlock} ${companionName}${genericBlock}.id() = 473 | Id${className}${genericBlock}(${allMembers.map { "this.${it.fieldName}" }.commaSep()}) 474 | """.trimIndent() 475 | 476 | write(fileContent + if (renderAdtFunctions) "\n" + adtFunctions else "") 477 | } 478 | } 479 | } 480 | } 481 | -------------------------------------------------------------------------------- /decomat-ksp/src/main/kotlin/io/decomat/DecomatProvider.kt: -------------------------------------------------------------------------------- 1 | package io.decomat 2 | 3 | import com.google.devtools.ksp.processing.SymbolProcessor 4 | import com.google.devtools.ksp.processing.SymbolProcessorEnvironment 5 | import com.google.devtools.ksp.processing.SymbolProcessorProvider 6 | 7 | class DecomatProvider : SymbolProcessorProvider { 8 | override fun create(environment: SymbolProcessorEnvironment): SymbolProcessor { 9 | val matchableAnnotationName = 10 | environment.options.get("matchableName") ?: "Matchable" 11 | val componentAnnotationName = 12 | environment.options.get("componentName") ?: "Component" 13 | val middleComponentAnnotationName = 14 | environment.options.get("middleComponentName") ?: "MiddleComponent" 15 | val constructorComponentAnnotationName = 16 | environment.options.get("constructorComponentName") ?: "ConstructorComponent" 17 | val renderAdtFunctions = 18 | environment.options.get("renderAdtFunctions")?.toBoolean() ?: false 19 | val renderFromHereFunction = 20 | renderAdtFunctions && (environment.options.get("renderFromHereFunction")?.toBoolean() ?: false) 21 | val fromHereFunctionName = 22 | environment.options.get("fromHereFunctionName") ?: "fromHere" 23 | val fromFunctionName = 24 | environment.options.get("fromFunctionName") ?: "from" 25 | 26 | return DecomatProcessor( 27 | environment.logger, environment.codeGenerator, 28 | matchableAnnotationName, 29 | componentAnnotationName, 30 | middleComponentAnnotationName, 31 | constructorComponentAnnotationName, 32 | renderAdtFunctions, 33 | renderFromHereFunction, 34 | fromHereFunctionName, 35 | fromFunctionName 36 | ) 37 | } 38 | } -------------------------------------------------------------------------------- /decomat-ksp/src/main/kotlin/io/decomat/FreemarkerTest.kt: -------------------------------------------------------------------------------- 1 | //package io.decomat 2 | // 3 | //import java.io.OutputStreamWriter 4 | //import java.io.File 5 | //import java.io.FileWriter 6 | //import java.io.IOException 7 | //import freemarker.template.TemplateDirectiveModel 8 | //import freemarker.template.TemplateException 9 | //import freemarker.core.Environment 10 | //import freemarker.template.Configuration 11 | //import freemarker.template.TemplateDirectiveBody 12 | //import freemarker.template.TemplateModel 13 | //import freemarker.template.SimpleScalar 14 | //import freemarker.template.Template 15 | //import java.io.Writer 16 | // 17 | //object FreemarkerTest { 18 | // @Throws(Exception::class) 19 | // @JvmStatic 20 | // fun main(args: Array) { 21 | // val cfg = Configuration(Configuration.VERSION_2_3_0) 22 | // cfg.setDefaultEncoding("UTF-8") 23 | // 24 | // val template = Template("", File("src/templates/Pattern3.ftl").readText(), cfg) 25 | // 26 | // val root = 27 | // HashMap() 28 | // .apply { 29 | // put("output", OutputDirective()) 30 | // //put("model", model.encode()) 31 | // } 32 | // 33 | // val out: Writer = OutputStreamWriter(System.out) 34 | // template.process(root, out) 35 | // } 36 | //} -------------------------------------------------------------------------------- /decomat-ksp/src/main/resources/META-INF/services/com.google.devtools.ksp.processing.SymbolProcessorProvider: -------------------------------------------------------------------------------- 1 | io.decomat.DecomatProvider -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs='-Xmx4048m' -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ExoQuery/DecoMat/a95a3f3ed693fabce538031371647760a22c2eac/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.1.1-bin.zip 4 | networkTimeout=10000 5 | zipStoreBase=GRADLE_USER_HOME 6 | zipStorePath=wrapper/dists 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | # 21 | # Gradle start up script for POSIX generated by Gradle. 22 | # 23 | # Important for running: 24 | # 25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 26 | # noncompliant, but you have some other compliant shell such as ksh or 27 | # bash, then to run this script, type that shell name before the whole 28 | # command line, like: 29 | # 30 | # ksh Gradle 31 | # 32 | # Busybox and similar reduced shells will NOT work, because this script 33 | # requires all of these POSIX shell features: 34 | # * functions; 35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 37 | # * compound commands having a testable exit status, especially «case»; 38 | # * various built-in commands including «command», «set», and «ulimit». 39 | # 40 | # Important for patching: 41 | # 42 | # (2) This script targets any POSIX shell, so it avoids extensions provided 43 | # by Bash, Ksh, etc; in particular arrays are avoided. 44 | # 45 | # The "traditional" practice of packing multiple parameters into a 46 | # space-separated string is a well documented source of bugs and security 47 | # problems, so this is (mostly) avoided, by progressively accumulating 48 | # options in "$@", and eventually passing that to Java. 49 | # 50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 52 | # see the in-line comments for details. 53 | # 54 | # There are tweaks for specific operating systems such as AIX, CygWin, 55 | # Darwin, MinGW, and NonStop. 56 | # 57 | # (3) This script is generated from the Groovy template 58 | # https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 59 | # within the Gradle project. 60 | # 61 | # You can find Gradle at https://github.com/gradle/gradle/. 62 | # 63 | ############################################################################## 64 | 65 | # Attempt to set APP_HOME 66 | 67 | # Resolve links: $0 may be a link 68 | app_path=$0 69 | 70 | # Need this for daisy-chained symlinks. 71 | while 72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 73 | [ -h "$app_path" ] 74 | do 75 | ls=$( ls -ld "$app_path" ) 76 | link=${ls#*' -> '} 77 | case $link in #( 78 | /*) app_path=$link ;; #( 79 | *) app_path=$APP_HOME$link ;; 80 | esac 81 | done 82 | 83 | # This is normally unused 84 | # shellcheck disable=SC2034 85 | APP_BASE_NAME=${0##*/} 86 | APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit 87 | 88 | # Use the maximum available, or set MAX_FD != -1 to use that value. 89 | MAX_FD=maximum 90 | 91 | warn () { 92 | echo "$*" 93 | } >&2 94 | 95 | die () { 96 | echo 97 | echo "$*" 98 | echo 99 | exit 1 100 | } >&2 101 | 102 | # OS specific support (must be 'true' or 'false'). 103 | cygwin=false 104 | msys=false 105 | darwin=false 106 | nonstop=false 107 | case "$( uname )" in #( 108 | CYGWIN* ) cygwin=true ;; #( 109 | Darwin* ) darwin=true ;; #( 110 | MSYS* | MINGW* ) msys=true ;; #( 111 | NONSTOP* ) nonstop=true ;; 112 | esac 113 | 114 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 115 | 116 | 117 | # Determine the Java command to use to start the JVM. 118 | if [ -n "$JAVA_HOME" ] ; then 119 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 120 | # IBM's JDK on AIX uses strange locations for the executables 121 | JAVACMD=$JAVA_HOME/jre/sh/java 122 | else 123 | JAVACMD=$JAVA_HOME/bin/java 124 | fi 125 | if [ ! -x "$JAVACMD" ] ; then 126 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 127 | 128 | Please set the JAVA_HOME variable in your environment to match the 129 | location of your Java installation." 130 | fi 131 | else 132 | JAVACMD=java 133 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 134 | 135 | Please set the JAVA_HOME variable in your environment to match the 136 | location of your Java installation." 137 | fi 138 | 139 | # Increase the maximum file descriptors if we can. 140 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 141 | case $MAX_FD in #( 142 | max*) 143 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. 144 | # shellcheck disable=SC3045 145 | MAX_FD=$( ulimit -H -n ) || 146 | warn "Could not query maximum file descriptor limit" 147 | esac 148 | case $MAX_FD in #( 149 | '' | soft) :;; #( 150 | *) 151 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. 152 | # shellcheck disable=SC3045 153 | ulimit -n "$MAX_FD" || 154 | warn "Could not set maximum file descriptor limit to $MAX_FD" 155 | esac 156 | fi 157 | 158 | # Collect all arguments for the java command, stacking in reverse order: 159 | # * args from the command line 160 | # * the main class name 161 | # * -classpath 162 | # * -D...appname settings 163 | # * --module-path (only if needed) 164 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 165 | 166 | # For Cygwin or MSYS, switch paths to Windows format before running java 167 | if "$cygwin" || "$msys" ; then 168 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 169 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 170 | 171 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 172 | 173 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 174 | for arg do 175 | if 176 | case $arg in #( 177 | -*) false ;; # don't mess with options #( 178 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 179 | [ -e "$t" ] ;; #( 180 | *) false ;; 181 | esac 182 | then 183 | arg=$( cygpath --path --ignore --mixed "$arg" ) 184 | fi 185 | # Roll the args list around exactly as many times as the number of 186 | # args, so each arg winds up back in the position where it started, but 187 | # possibly modified. 188 | # 189 | # NB: a `for` loop captures its iteration list before it begins, so 190 | # changing the positional parameters here affects neither the number of 191 | # iterations, nor the values presented in `arg`. 192 | shift # remove old arg 193 | set -- "$@" "$arg" # push replacement arg 194 | done 195 | fi 196 | 197 | 198 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 199 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 200 | 201 | # Collect all arguments for the java command; 202 | # * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of 203 | # shell script including quotes and variable substitutions, so put them in 204 | # double quotes to make sure that they get re-expanded; and 205 | # * put everything else in single quotes, so that it's not re-expanded. 206 | 207 | set -- \ 208 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 209 | -classpath "$CLASSPATH" \ 210 | org.gradle.wrapper.GradleWrapperMain \ 211 | "$@" 212 | 213 | # Stop when "xargs" is not available. 214 | if ! command -v xargs >/dev/null 2>&1 215 | then 216 | die "xargs is not available" 217 | fi 218 | 219 | # Use "xargs" to parse quoted args. 220 | # 221 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 222 | # 223 | # In Bash we could simply go: 224 | # 225 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 226 | # set -- "${ARGS[@]}" "$@" 227 | # 228 | # but POSIX shell has neither arrays nor command substitution, so instead we 229 | # post-process each arg (as a line of input to sed) to backslash-escape any 230 | # character that might be a shell metacharacter, then use eval to reverse 231 | # that process (while maintaining the separation between arguments), and wrap 232 | # the whole thing up as a single "set" statement. 233 | # 234 | # This will of course break if any of these variables contains a newline or 235 | # an unmatched quote. 236 | # 237 | 238 | eval "set -- $( 239 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 240 | xargs -n1 | 241 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 242 | tr '\n' ' ' 243 | )" '"$@"' 244 | 245 | exec "$JAVACMD" "$@" 246 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%"=="" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%"=="" set DIRNAME=. 29 | @rem This is normally unused 30 | set APP_BASE_NAME=%~n0 31 | set APP_HOME=%DIRNAME% 32 | 33 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 34 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 35 | 36 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 37 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 38 | 39 | @rem Find java.exe 40 | if defined JAVA_HOME goto findJavaFromJavaHome 41 | 42 | set JAVA_EXE=java.exe 43 | %JAVA_EXE% -version >NUL 2>&1 44 | if %ERRORLEVEL% equ 0 goto execute 45 | 46 | echo. 47 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 48 | echo. 49 | echo Please set the JAVA_HOME variable in your environment to match the 50 | echo location of your Java installation. 51 | 52 | goto fail 53 | 54 | :findJavaFromJavaHome 55 | set JAVA_HOME=%JAVA_HOME:"=% 56 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 57 | 58 | if exist "%JAVA_EXE%" goto execute 59 | 60 | echo. 61 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 62 | echo. 63 | echo Please set the JAVA_HOME variable in your environment to match the 64 | echo location of your Java installation. 65 | 66 | goto fail 67 | 68 | :execute 69 | @rem Setup the command line 70 | 71 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 72 | 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if %ERRORLEVEL% equ 0 goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | set EXIT_CODE=%ERRORLEVEL% 85 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 86 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 87 | exit /b %EXIT_CODE% 88 | 89 | :mainEnd 90 | if "%OS%"=="Windows_NT" endlocal 91 | 92 | :omega 93 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | include(":decomat-core", ":decomat-ksp", ":decomat-examples") 2 | 3 | pluginManagement { 4 | plugins { 5 | id("com.google.devtools.ksp") version "1.9.22-1.0.17" 6 | kotlin("jvm") version "1.9.22" 7 | id("dev.anies.gradle.template") version "0.0.2" 8 | } 9 | repositories { 10 | gradlePluginPortal() 11 | google() 12 | } 13 | } 14 | 15 | rootProject.name = "decomat" 16 | -------------------------------------------------------------------------------- /test.kt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ExoQuery/DecoMat/a95a3f3ed693fabce538031371647760a22c2eac/test.kt --------------------------------------------------------------------------------