├── .git-blame-ignore-revs ├── .github ├── dependabot.yaml ├── release-drafter.yml └── workflows │ ├── ci.yml │ └── scala-steward.yml ├── .gitignore ├── .mergify.yml ├── .scala-steward.conf ├── .scalafmt.conf ├── LICENSE.txt ├── README.md ├── banner.png ├── build.sbt ├── project ├── build.properties └── plugins.sbt └── quicklens └── src ├── main ├── scala-2.13+ │ └── com.softwaremill.quicklens │ │ ├── LowPriorityImplicits.scala │ │ └── package.scala ├── scala-2.13- │ └── com.softwaremill.quicklens │ │ ├── LowPriorityImplicits.scala │ │ └── package.scala ├── scala-2 │ └── com │ │ └── softwaremill │ │ └── quicklens │ │ └── QuicklensMacros.scala └── scala-3 │ └── com │ └── softwaremill │ └── quicklens │ ├── QuicklensMacros.scala │ └── package.scala └── test ├── scala-2 └── com.softwaremill.quicklens │ └── QuicklensMapAtFunctorTest.scala ├── scala-3 └── com │ └── softwaremill │ └── quicklens │ └── test │ ├── CompileTimeTest.scala │ ├── CustomModifyProxyTest.scala │ ├── ExplicitCopyTest.scala │ ├── ExtensionCopyTest.scala │ ├── LiteralTypeTest.scala │ ├── ModifyAllOptimizedTest.scala │ ├── ModifyAllOrderTest.scala │ ├── ModifyAndTypeTest.scala │ ├── ModifyEnumTest.scala │ └── ModitySealedAbstractClass.scala └── scala └── com └── softwaremill └── quicklens ├── EnormousModifyAllTest.scala ├── HugeModifyTest.scala ├── LensLazyTest.scala ├── LensTest.scala ├── ModifyAliasTest.scala ├── ModifyArrayIndexTest.scala ├── ModifyAtTest.scala ├── ModifyEachTest.scala ├── ModifyEachWhereTest.scala ├── ModifyEitherTest.scala ├── ModifyIndexTest.scala ├── ModifyIndexedSeqIndexTest.scala ├── ModifyLazyTest.scala ├── ModifyMapAtTest.scala ├── ModifyMapIndexTest.scala ├── ModifyOptionAtOrElseTest.scala ├── ModifyOptionAtTest.scala ├── ModifyOptionIndexTest.scala ├── ModifyPimpTest.scala ├── ModifySelfThisTest.scala ├── ModifySeqIndexTest.scala ├── ModifySimpleTest.scala ├── ModifyWhenTest.scala ├── RepeatedModifyAllTest.scala ├── RepeatedModifyTest.scala ├── SealedTest.scala ├── SecondParamListTest.scala ├── SetToSimpleTest.scala ├── TestData.scala └── TupleModifyTest.scala /.git-blame-ignore-revs: -------------------------------------------------------------------------------- 1 | # Scala Steward: Reformat with scalafmt 3.2.2 2 | 1c7df8c740764927fd3962f34081dc1d0840e43f 3 | 4 | # Scala Steward: Reformat with scalafmt 3.7.17 5 | f3e9d962fa8ceb0db3cec08bac9e4d05635f0908 6 | 7 | # Scala Steward: Reformat with scalafmt 3.8.3 8 | 3f51dc7447fca7c4a49087ff4255387e31ad3b0d 9 | -------------------------------------------------------------------------------- /.github/dependabot.yaml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | # Maintain dependencies for GitHub Actions 4 | - package-ecosystem: "github-actions" 5 | directory: "/" 6 | schedule: 7 | interval: "daily" -------------------------------------------------------------------------------- /.github/release-drafter.yml: -------------------------------------------------------------------------------- 1 | template: | 2 | ## What’s Changed 3 | 4 | $CHANGES -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | pull_request: 4 | branches: ['**'] 5 | push: 6 | branches: ['**'] 7 | tags: [v*] 8 | jobs: 9 | ci: 10 | # run on external PRs, but not on internal PRs since those will be run by push to branch 11 | if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name != github.repository 12 | runs-on: ubuntu-22.04 13 | env: 14 | JAVA_OPTS: -Xmx4G 15 | steps: 16 | - name: Checkout 17 | uses: actions/checkout@v3 18 | - name: Set up JDK 19 | uses: actions/setup-java@v3 20 | with: 21 | distribution: 'temurin' 22 | java-version: '11' 23 | cache: 'sbt' 24 | - name: Compile 25 | run: sbt -v compile 26 | - name: Test 27 | run: sbt -v test 28 | 29 | publish: 30 | name: Publish release 31 | needs: [ci] 32 | if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v')) 33 | runs-on: ubuntu-22.04 34 | env: 35 | STTP_NATIVE: 1 36 | JAVA_OPTS: -Xmx4G 37 | steps: 38 | - name: Checkout 39 | uses: actions/checkout@v3 40 | - name: Set up JDK 41 | uses: actions/setup-java@v3 42 | with: 43 | distribution: 'temurin' 44 | java-version: '11' 45 | cache: 'sbt' 46 | - name: Compile 47 | run: sbt compile 48 | - name: Publish artifacts 49 | run: sbt ci-release 50 | env: 51 | PGP_PASSPHRASE: ${{ secrets.PGP_PASSPHRASE }} 52 | PGP_SECRET: ${{ secrets.PGP_SECRET }} 53 | SONATYPE_USERNAME: ${{ secrets.SONATYPE_USERNAME }} 54 | SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }} 55 | - name: Extract version from commit message 56 | run: | 57 | version=${GITHUB_REF/refs\/tags\/v/} 58 | echo "VERSION=$version" >> $GITHUB_ENV 59 | env: 60 | COMMIT_MSG: ${{ github.event.head_commit.message }} 61 | - name: Publish release notes 62 | uses: release-drafter/release-drafter@v5 63 | with: 64 | config-name: release-drafter.yml 65 | publish: true 66 | name: "v${{ env.VERSION }}" 67 | tag: "v${{ env.VERSION }}" 68 | version: "v${{ env.VERSION }}" 69 | env: 70 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 71 | -------------------------------------------------------------------------------- /.github/workflows/scala-steward.yml: -------------------------------------------------------------------------------- 1 | name: Scala Steward 2 | 3 | # This workflow will launch at 00:00 every day 4 | on: 5 | schedule: 6 | - cron: '0 0 * * *' 7 | workflow_dispatch: 8 | 9 | jobs: 10 | scala-steward: 11 | runs-on: ubuntu-22.04 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@v3 15 | - name: Set up JDK 11 16 | uses: actions/setup-java@v3 17 | with: 18 | distribution: 'temurin' 19 | java-version: 11 20 | cache: 'sbt' 21 | - name: Launch Scala Steward 22 | uses: scala-steward-org/scala-steward-action@v2 23 | with: 24 | author-name: scala-steward 25 | author-email: scala-steward 26 | github-token: ${{ secrets.REPO_GITHUB_TOKEN }} 27 | repo-config: .scala-steward.conf 28 | ignore-opts-files: false 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | *.iml 3 | *.ipr 4 | target/ 5 | project/project 6 | out 7 | .js 8 | .jvm 9 | .native 10 | .metals/ 11 | .bloop/ 12 | project/**/metals.sbt 13 | .vscode/ 14 | .bsp 15 | .scala-build -------------------------------------------------------------------------------- /.mergify.yml: -------------------------------------------------------------------------------- 1 | pull_request_rules: 2 | - name: delete head branch after merge 3 | conditions: [] 4 | actions: 5 | delete_head_branch: {} 6 | - name: automatic merge for scala-steward pull requests affecting build.sbt 7 | conditions: 8 | - author=softwaremill-ci 9 | - status-success=ci 10 | - "#files=1" 11 | - files=build.sbt 12 | actions: 13 | merge: 14 | method: merge 15 | - name: automatic merge for scala-steward pull requests affecting project plugins.sbt 16 | conditions: 17 | - author=softwaremill-ci 18 | - status-success=ci 19 | - "#files=1" 20 | - files=project/plugins.sbt 21 | actions: 22 | merge: 23 | method: merge 24 | - name: semi-automatic merge for scala-steward pull requests 25 | conditions: 26 | - author=softwaremill-ci 27 | - status-success=ci 28 | - "#approved-reviews-by>=1" 29 | actions: 30 | merge: 31 | method: merge 32 | - name: automatic merge for scala-steward pull requests affecting project build.properties 33 | conditions: 34 | - author=softwaremill-ci 35 | - status-success=ci 36 | - "#files=1" 37 | - files=project/build.properties 38 | actions: 39 | merge: 40 | method: merge 41 | - name: automatic merge for scala-steward pull requests affecting .scalafmt.conf 42 | conditions: 43 | - author=softwaremill-ci 44 | - status-success=ci 45 | - "#files=1" 46 | - files=.scalafmt.conf 47 | actions: 48 | merge: 49 | method: merge 50 | -------------------------------------------------------------------------------- /.scala-steward.conf: -------------------------------------------------------------------------------- 1 | updates.ignore = [ 2 | {groupId = "org.scala-lang", artifactId = "scala-compiler", version = "2.12."}, 3 | {groupId = "org.scala-lang", artifactId = "scala-compiler", version = "2.13."}, 4 | ] 5 | updates.pin = [ 6 | {groupId = "org.scala-lang", artifactId = "scala3-library", version = "3.3."} 7 | ] 8 | -------------------------------------------------------------------------------- /.scalafmt.conf: -------------------------------------------------------------------------------- 1 | version = 3.8.3 2 | maxColumn = 120 3 | runner.dialect = scala3 4 | fileOverride { 5 | "glob:**/scala-2/**" { 6 | runner.dialect = scala213 7 | } 8 | "glob:**/scala-2.13+/**" { 9 | runner.dialect = scala213 10 | } 11 | "glob:**/scala-2.13-/**" { 12 | runner.dialect = scala213 13 | } 14 | } -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright 2013-2016 Adam Warski 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Quicklens](https://github.com/softwaremill/quicklens/raw/master/banner.png) 2 | 3 | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/com.softwaremill.quicklens/quicklens_2.13/badge.svg)](https://maven-badges.herokuapp.com/maven-central/com.softwaremill.quicklens/quicklens_2.13) 4 | [![CI](https://github.com/softwaremill/quicklens/workflows/CI/badge.svg)](https://github.com/softwaremill/quicklens/actions?query=workflow%3A%22CI%22) 5 | 6 | **Modify deeply nested fields in case classes:** 7 | 8 | ````scala 9 | import com.softwaremill.quicklens._ 10 | 11 | case class Street(name: String) 12 | case class Address(street: Street) 13 | case class Person(address: Address, age: Int) 14 | 15 | val person = Person(Address(Street("1 Functional Rd.")), 35) 16 | 17 | val p2 = person.modify(_.address.street.name).using(_.toUpperCase) 18 | val p3 = person.modify(_.address.street.name).setTo("3 OO Ln.") 19 | 20 | // or 21 | 22 | val p4 = modify(person)(_.address.street.name).using(_.toUpperCase) 23 | val p5 = modify(person)(_.address.street.name).setTo("3 OO Ln.") 24 | ```` 25 | 26 | **Chain modifications:** 27 | 28 | ````scala 29 | person 30 | .modify(_.address.street.name).using(_.toUpperCase) 31 | .modify(_.age).using(_ - 1) 32 | ```` 33 | 34 | **Modify conditionally:** 35 | 36 | ````scala 37 | person.modify(_.address.street.name).setToIfDefined(Some("3 00 Ln.")) 38 | person.modify(_.address.street.name).setToIf(shouldChangeAddress)("3 00 Ln.") 39 | ```` 40 | 41 | **Modify several fields in one go:** 42 | 43 | ````scala 44 | import com.softwaremill.quicklens._ 45 | 46 | case class Person(firstName: String, middleName: Option[String], lastName: String) 47 | 48 | val person = Person("john", Some("steve"), "smith") 49 | 50 | person.modifyAll(_.firstName, _.middleName.each, _.lastName).using(_.capitalize) 51 | ```` 52 | 53 | **Traverse options/lists/maps using `.each`:** 54 | 55 | ````scala 56 | import com.softwaremill.quicklens._ 57 | 58 | case class Street(name: String) 59 | case class Address(street: Option[Street]) 60 | case class Person(addresses: List[Address]) 61 | 62 | val person = Person(List( 63 | Address(Some(Street("1 Functional Rd."))), 64 | Address(Some(Street("2 Imperative Dr."))) 65 | )) 66 | 67 | val p2 = person.modify(_.addresses.each.street.each.name).using(_.toUpperCase) 68 | ```` 69 | 70 | `.each` can only be used inside a `modify` and "unwraps" the container (currently supports `Seq`s, `Option`s and 71 | `Maps`s - only values are unwrapped for maps). 72 | You can add support for your own containers by providing an implicit `QuicklensFunctor[C]` with the appropriate 73 | `C` type parameter. 74 | 75 | **Traverse selected elements using `.eachWhere`:** 76 | 77 | Similarly to `.each`, you can use `.eachWhere(p)` where `p` is a predicate to modify only the elements which satisfy 78 | the condition. All other elements remain unchanged. 79 | 80 | ````scala 81 | def filterAddress: Address => Boolean = ??? 82 | person 83 | .modify(_.addresses.eachWhere(filterAddress) 84 | .street.eachWhere(_.name.startsWith("1")).name) 85 | .using(_.toUpperCase) 86 | ```` 87 | 88 | **Modify specific elements in an option/sequence/map using `.at`:** 89 | 90 | ````scala 91 | person.modify(_.addresses.at(2).street.at.name).using(_.toUpperCase) 92 | ```` 93 | 94 | Similarly to `.each`, `.at` modifies only the element at the given index/key. If there's no element at that index, 95 | an `IndexOutOfBoundsException` is thrown. In the above example, `.at(2)` selects an element in `addresses: List[Address]` 96 | and `.at` selects the lone possible element in `street: Option[Street]`. If `street` is `None`, a 97 | `NoSuchElementException` is thrown. 98 | 99 | `.at` works for map keys as well: 100 | 101 | ````scala 102 | case class Property(value: String) 103 | 104 | case class PersonWithProps(name: String, props: Map[String, Property]) 105 | 106 | val personWithProps = PersonWithProps( 107 | "Joe", 108 | Map("Role" -> Property("Programmmer"), "Age" -> Property("45")) 109 | ) 110 | 111 | personWithProps.modify(_.props.at("Age").value).setTo("45") 112 | ```` 113 | 114 | Similarly to `.each`, `.at` modifies only the element with the given key. If there's no such element, 115 | an `NoSuchElementException` is thrown. 116 | 117 | **Modify specific elements in an option/sequence/map using `.index`:** 118 | 119 | ````scala 120 | person.modify(_.addresses.index(2).street.index.name).using(_.toUpperCase) 121 | ```` 122 | 123 | Similarly to `.at`, `.index` modifies only the element at the given index/key. If there's no element at that index, 124 | no modification is made. In the above example, `.index(2)` selects an element in `addresses: List[Address]` 125 | and `.index` selects the lone possible element in `street: Option[Street]`. If `street` is `None`, no modification 126 | is made. 127 | 128 | `.index` works for map keys as well: 129 | 130 | ````scala 131 | case class Property(value: String) 132 | 133 | case class PersonWithProps(name: String, props: Map[String, Property]) 134 | 135 | val personWithProps = PersonWithProps( 136 | "Joe", 137 | Map("Role" -> Property("Programmmer"), "Age" -> Property("45")) 138 | ) 139 | 140 | personWithProps.modify(_.props.index("Age").value).setTo("45") 141 | ```` 142 | 143 | Similarly to `.at`, `.index` modifies only the element with the given key. If there's no such element, 144 | no modification is made. 145 | 146 | **Modify specific elements in an option or map with a fallback using `.atOrElse`:** 147 | 148 | ````scala 149 | personWithProps.modify(_.props.atOrElse("NumReports", Property("0")).value).setTo("5") 150 | ```` 151 | 152 | If `props` contains an entry for `"NumReports"`, then `.atOrElse` behaves the same as `.at` and the second 153 | parameter is never evaluated. If there is no entry, then `.atOrElse` will make one using the second parameter 154 | and perform subsequent modifications on the newly instantiated default. 155 | 156 | For Options, `.atOrElse` takes no arguments and acts similarly. 157 | 158 | ````scala 159 | person.modify(_.addresses.at(2).street.atOrElse(Street("main street")).name).using(_.toUpperCase) 160 | ```` 161 | 162 | `.atOrElse` is currently not available for sequences because quicklens might need to insert many 163 | elements in the list in order to ensure that one is available at a particular position, and it's not 164 | clear that providing one default for all keys is the right behavior. 165 | 166 | **Modify Either fields using `.eachLeft` and `.eachRight`:** 167 | 168 | ````scala 169 | case class AuthContext(token: String) 170 | case class AuthRequest(url: String) 171 | case class Resource(auth: Either[AuthContext, AuthRequest]) 172 | 173 | val devResource = Resource(auth = Left(AuthContext("fake")) 174 | 175 | val prodResource = devResource.modify(_.auth.eachLeft.token).setTo("real") 176 | 177 | ```` 178 | 179 | **Modify fields when they are of a certain subtype:** 180 | 181 | ```scala 182 | trait Animal 183 | case class Dog(age: Int) extends Animal 184 | case class Cat(ages: Seq[Int]) extends Animal 185 | 186 | case class Zoo(animals: Seq[Animal]) 187 | 188 | val zoo = Zoo(List(Dog(4), Cat(List(3, 12, 13)))) 189 | 190 | val olderZoo = zoo.modifyAll( 191 | _.animals.each.when[Dog].age, 192 | _.animals.each.when[Cat].ages.at(0) 193 | ).using(_ + 1) 194 | ``` 195 | 196 | This is also known as a *prism*, see e.g. [here](https://www.optics.dev/Monocle/docs/optics/prism). 197 | 198 | **Re-usable modifications (lenses):** 199 | 200 | ````scala 201 | import com.softwaremill.quicklens._ 202 | 203 | val modifyStreetName = modify(_: Person)(_.address.street.name) 204 | 205 | val p3 = modifyStreetName(person).using(_.toUpperCase) 206 | val p4 = modifyStreetName(anotherPerson).using(_.toLowerCase) 207 | 208 | // 209 | 210 | val upperCaseStreetName = modify(_: Person)(_.address.street.name).using(_.toUpperCase) 211 | 212 | val p5 = upperCaseStreetName(person) 213 | ```` 214 | Alternate syntax: 215 | ````scala 216 | import com.softwaremill.quicklens._ 217 | 218 | val modifyStreetName = modifyLens[Person](_.address.street.name) 219 | 220 | val p3 = modifyStreetName.using(_.toUpperCase)(person) 221 | val p4 = modifyStreetName.using(_.toLowerCase)(anotherPerson) 222 | 223 | // 224 | 225 | val upperCaseStreetName = modifyLens[Person](_.address.street.name).using(_.toUpperCase) 226 | 227 | val p5 = upperCaseStreetName(person) 228 | ```` 229 | 230 | **Composing lenses:** 231 | 232 | ````scala 233 | import com.softwaremill.quicklens._ 234 | 235 | val modifyAddress = modify(_: Person)(_.address) 236 | val modifyStreetName = modify(_: Address)(_.street.name) 237 | 238 | val p6 = (modifyAddress andThenModify modifyStreetName)(person).using(_.toUpperCase) 239 | ```` 240 | or, with alternate syntax: 241 | ````scala 242 | import com.softwaremill.quicklens._ 243 | 244 | val modifyAddress = modifyLens[Person](_.address) 245 | val modifyStreetName = modifyLens[Address](_.street.name) 246 | 247 | val p6 = (modifyAddress andThenModify modifyStreetName).using(_.toUpperCase)(person) 248 | ```` 249 | 250 | 251 | **Modify nested sealed hierarchies & enums:** 252 | 253 | ````scala 254 | import com.softwaremill.quicklens._ 255 | 256 | sealed trait Pet { def name: String } 257 | case class Fish(name: String) extends Pet 258 | sealed trait LeggedPet extends Pet 259 | case class Cat(name: String) extends LeggedPet 260 | case class Dog(name: String) extends LeggedPet 261 | 262 | val pets = List[Pet]( 263 | Fish("Finn"), Cat("Catia"), Dog("Douglas") 264 | ) 265 | 266 | val juniorPets = pets.modify(_.each.name).using(_ + ", Jr.") 267 | ```` 268 | 269 | --- 270 | 271 | Also check out [Monocle](https://github.com/julien-truffaut/Monocle), for a more advanced [lens](http://eed3si9n.com/learning-scalaz/Lens.html) library. 272 | 273 | Read [the blog](http://www.warski.org/blog/2015/02/quicklens-modify-deeply-nested-case-class-fields/) for more info. 274 | 275 | Available in Maven Central: 276 | 277 | ````scala 278 | val quicklens = "com.softwaremill.quicklens" %% "quicklens" % "1.9.12" 279 | ```` 280 | 281 | Available for Scala 2.11, 2.12, 2.13, [3](https://dotty.epfl.ch), [Scala.js](http://www.scala-js.org) and [Scala Native](http://www.scala-native.org)! 282 | 283 | ## Commercial Support 284 | 285 | We offer commercial support for Quicklens and related technologies, as well as development services. [Contact us](https://softwaremill.com) to learn more about our offer! 286 | 287 | ## Copyright 288 | 289 | Copyright (C) 2015-2021 SoftwareMill [https://softwaremill.com](https://softwaremill.com). 290 | -------------------------------------------------------------------------------- /banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/softwaremill/quicklens/964ff968bdb424f27ff7de2fbcd0b3b4cf582a7b/banner.png -------------------------------------------------------------------------------- /build.sbt: -------------------------------------------------------------------------------- 1 | import com.softwaremill.SbtSoftwareMillCommon.commonSmlBuildSettings 2 | import com.softwaremill.Publish.{updateDocs, ossPublishSettings} 3 | import com.softwaremill.UpdateVersionInDocs 4 | 5 | val scala211 = "2.11.12" 6 | val scala212 = "2.12.20" 7 | val scala213 = "2.13.15" 8 | val scala3 = "3.3.4" 9 | 10 | val scalaIdeaVersion = scala3 // the version for which to import sources into intellij 11 | 12 | excludeLintKeys in Global ++= Set(ideSkipProject) 13 | 14 | val commonSettings = commonSmlBuildSettings ++ ossPublishSettings ++ Seq( 15 | organization := "com.softwaremill.quicklens", 16 | updateDocs := UpdateVersionInDocs(sLog.value, organization.value, version.value, List(file("README.md"))), 17 | scalacOptions ++= Seq("-deprecation", "-feature", "-unchecked"), // useful for debugging macros: "-Ycheck:all", "-Xcheck-macros" 18 | ideSkipProject := (scalaVersion.value != scalaIdeaVersion) 19 | ) 20 | 21 | lazy val root = 22 | project 23 | .in(file(".")) 24 | .settings(commonSettings) 25 | .settings(publishArtifact := false) 26 | .settings(scalaVersion := scalaIdeaVersion) 27 | .aggregate(quicklens.projectRefs: _*) 28 | 29 | val versionSpecificScalaSources = { 30 | Compile / unmanagedSourceDirectories := { 31 | val current = (Compile / unmanagedSourceDirectories).value 32 | val sv = (Compile / scalaVersion).value 33 | val baseDirectory = (Compile / scalaSource).value 34 | val suffixes = CrossVersion.partialVersion(sv) match { 35 | case Some((2, 13)) => List("2", "2.13+") 36 | case Some((2, _)) => List("2", "2.13-") 37 | case Some((3, _)) => List("3") 38 | case _ => Nil 39 | } 40 | val versionSpecificSources = suffixes.map(s => new File(baseDirectory.getAbsolutePath + "-" + s)) 41 | versionSpecificSources ++ current 42 | } 43 | } 44 | 45 | def compilerLibrary(scalaVersion: String) = { 46 | if (scalaVersion == scala3) { 47 | Seq.empty 48 | } else { 49 | Seq("org.scala-lang" % "scala-compiler" % scalaVersion % Test) 50 | } 51 | } 52 | 53 | def reflectLibrary(scalaVersion: String) = { 54 | if (scalaVersion == scala3) { 55 | Seq.empty 56 | } else { 57 | Seq("org.scala-lang" % "scala-reflect" % scalaVersion % Provided) 58 | } 59 | } 60 | 61 | lazy val quicklens = (projectMatrix in file("quicklens")) 62 | .settings(commonSettings) 63 | .settings( 64 | name := "quicklens", 65 | libraryDependencies ++= reflectLibrary(scalaVersion.value), 66 | Test / publishArtifact := false, 67 | libraryDependencies ++= compilerLibrary(scalaVersion.value), 68 | versionSpecificScalaSources, 69 | libraryDependencies ++= Seq("flatspec", "shouldmatchers").map(m => 70 | "org.scalatest" %%% s"scalatest-$m" % "3.2.18" % Test 71 | ) 72 | ) 73 | .jvmPlatform( 74 | scalaVersions = List(scala211, scala212, scala213, scala3) 75 | ) 76 | .jsPlatform( 77 | scalaVersions = List(scala212, scala213, scala3) 78 | ) 79 | .nativePlatform( 80 | scalaVersions = List(scala212, scala213, scala3), 81 | ) 82 | -------------------------------------------------------------------------------- /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=1.10.6 2 | -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | val scalaJSVersion = Option(System.getenv("SCALAJS_VERSION")).getOrElse("1.17.0") 2 | val scalaNativeVersion = Option(System.getenv("SCALANATIVE_VERSION")).getOrElse("0.5.6") 3 | 4 | addSbtPlugin("org.scala-js" % "sbt-scalajs" % scalaJSVersion) 5 | addSbtPlugin("org.scala-native" % "sbt-scala-native" % scalaNativeVersion) 6 | addSbtPlugin("com.eed3si9n" % "sbt-projectmatrix" % "0.10.1") 7 | addSbtPlugin("org.jetbrains.scala" % "sbt-ide-settings" % "1.1.2") 8 | 9 | val sbtSoftwareMillVersion = "2.0.20" 10 | addSbtPlugin("com.softwaremill.sbt-softwaremill" % "sbt-softwaremill-common" % sbtSoftwareMillVersion) 11 | addSbtPlugin("com.softwaremill.sbt-softwaremill" % "sbt-softwaremill-publish" % sbtSoftwareMillVersion) 12 | -------------------------------------------------------------------------------- /quicklens/src/main/scala-2.13+/com.softwaremill.quicklens/LowPriorityImplicits.scala: -------------------------------------------------------------------------------- 1 | package com.softwaremill.quicklens 2 | 3 | import scala.annotation.compileTimeOnly 4 | 5 | private[quicklens] trait LowPriorityImplicits { 6 | 7 | /** `QuicklensEach` is in `LowPriorityImplicits` to not conflict with the `QuicklensMapAtFunctor` on `each` calls. 8 | */ 9 | implicit class QuicklensEach[F[_], T](t: F[T])(implicit f: QuicklensFunctor[F, T]) { 10 | @compileTimeOnly(canOnlyBeUsedInsideModify("each")) 11 | def each: T = sys.error("") 12 | 13 | @compileTimeOnly(canOnlyBeUsedInsideModify("eachWhere")) 14 | def eachWhere(p: T => Boolean): T = sys.error("") 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /quicklens/src/main/scala-2.13+/com.softwaremill.quicklens/package.scala: -------------------------------------------------------------------------------- 1 | package com.softwaremill 2 | 3 | import scala.annotation.compileTimeOnly 4 | import scala.collection.Factory 5 | import scala.collection.SeqLike 6 | import scala.language.experimental.macros 7 | import scala.language.higherKinds 8 | 9 | package object quicklens extends LowPriorityImplicits { 10 | 11 | private[softwaremill] def canOnlyBeUsedInsideModify(method: String) = 12 | s"$method can only be used inside modify" 13 | 14 | /** Create an object allowing modifying the given (deeply nested) field accessible in a `case class` hierarchy via 15 | * `path` on the given `obj`. 16 | * 17 | * All modifications are side-effect free and create copies of the original objects. 18 | * 19 | * You can use `.each` to traverse options, lists, etc. 20 | */ 21 | def modify[T, U](obj: T)(path: T => U): PathModify[T, U] = macro QuicklensMacros.modify_impl[T, U] 22 | 23 | /** Create an object allowing modifying the given (deeply nested) fields accessible in a `case class` hierarchy via 24 | * `paths` on the given `obj`. 25 | * 26 | * All modifications are side-effect free and create copies of the original objects. 27 | * 28 | * You can use `.each` to traverse options, lists, etc. 29 | */ 30 | def modifyAll[T, U](obj: T)(path1: T => U, paths: (T => U)*): PathModify[T, U] = 31 | macro QuicklensMacros.modifyAll_impl[T, U] 32 | 33 | implicit class ModifyPimp[T](t: T) { 34 | 35 | /** Create an object allowing modifying the given (deeply nested) field accessible in a `case class` hierarchy via 36 | * `path` on the given `obj`. 37 | * 38 | * All modifications are side-effect free and create copies of the original objects. 39 | * 40 | * You can use `.each` to traverse options, lists, etc. 41 | */ 42 | def modify[U](path: T => U): PathModify[T, U] = macro QuicklensMacros.modifyPimp_impl[T, U] 43 | 44 | /** Create an object allowing modifying the given (deeply nested) fields accessible in a `case class` hierarchy via 45 | * `paths` on the given `obj`. 46 | * 47 | * All modifications are side-effect free and create copies of the original objects. 48 | * 49 | * You can use `.each` to traverse options, lists, etc. 50 | */ 51 | def modifyAll[U](path1: T => U, paths: (T => U)*): PathModify[T, U] = macro QuicklensMacros.modifyAllPimp_impl[T, U] 52 | } 53 | 54 | case class PathModify[T, U](obj: T, doModify: (T, U => U) => T) { 55 | 56 | /** Transform the value of the field(s) using the given function. 57 | * 58 | * @return 59 | * A copy of the root object with the (deeply nested) field(s) modified. 60 | */ 61 | def using(mod: U => U): T = doModify(obj, mod) 62 | 63 | /** An alias for [[using]]. Explicit calls to [[using]] are preferred over this alias, but quicklens provides this 64 | * option because code auto-formatters (like scalafmt) will generally not keep [[modify]]/[[using]] pairs on the 65 | * same line, leading to code like 66 | * {{{ 67 | * x 68 | * .modify(_.foo) 69 | * .using(newFoo :: _) 70 | * .modify(_.bar) 71 | * .using(_ + newBar) 72 | * }}} 73 | * When using [[apply]], scalafmt will allow 74 | * {{{ 75 | * x 76 | * .modify(_.foo)(newFoo :: _) 77 | * .modify(_.bar)(_ + newBar) 78 | * }}} 79 | */ 80 | final def apply(mod: U => U): T = using(mod) 81 | 82 | /** Transform the value of the field(s) using the given function, if the condition is true. Otherwise, returns the 83 | * original object unchanged. 84 | * 85 | * @return 86 | * A copy of the root object with the (deeply nested) field(s) modified, if `condition` is true. 87 | */ 88 | def usingIf(condition: Boolean)(mod: U => U): T = if (condition) doModify(obj, mod) else obj 89 | 90 | /** Set the value of the field(s) to a new value. 91 | * 92 | * @return 93 | * A copy of the root object with the (deeply nested) field(s) set to the new value. 94 | */ 95 | def setTo(v: U): T = doModify(obj, _ => v) 96 | 97 | /** Set the value of the field(s) to a new value, if it is defined. Otherwise, returns the original object 98 | * unchanged. 99 | * 100 | * @return 101 | * A copy of the root object with the (deeply nested) field(s) set to the new value, if it is defined. 102 | */ 103 | def setToIfDefined(v: Option[U]): T = v.fold(obj)(setTo) 104 | 105 | /** Set the value of the field(s) to a new value, if the condition is true. Otherwise, returns the original object 106 | * unchanged. 107 | * 108 | * @return 109 | * A copy of the root object with the (deeply nested) field(s) set to the new value, if `condition` is true. 110 | */ 111 | def setToIf(condition: Boolean)(v: => U): T = if (condition) setTo(v) else obj 112 | } 113 | 114 | implicit class AbstractPathModifyPimp[T, U](f1: T => PathModify[T, U]) { 115 | def andThenModify[V](f2: U => PathModify[U, V]): T => PathModify[T, V] = { t: T => 116 | PathModify[T, V](t, (t, vv) => f1(t).doModify(t, u => f2(u).doModify(u, vv))) 117 | } 118 | } 119 | 120 | def modifyLens[T]: LensHelper[T] = LensHelper[T]() 121 | 122 | def modifyAllLens[T]: MultiLensHelper[T] = MultiLensHelper[T]() 123 | 124 | case class LensHelper[T] private () { 125 | 126 | def apply[U](path: T => U): PathLazyModify[T, U] = macro QuicklensMacros.modifyLazy_impl[T, U] 127 | } 128 | 129 | case class MultiLensHelper[T] private () { 130 | 131 | def apply[U](path1: T => U, paths: (T => U)*): PathLazyModify[T, U] = macro QuicklensMacros.modifyLazyAll_impl[T, U] 132 | } 133 | 134 | case class PathLazyModify[T, U](doModify: (T, U => U) => T) { 135 | 136 | self => 137 | 138 | /** see [[PathModify.using]] 139 | */ 140 | def using(mod: U => U): T => T = obj => doModify(obj, mod) 141 | 142 | /** see [[PathModify.usingIf]] 143 | */ 144 | def usingIf(condition: Boolean)(mod: U => U): T => T = 145 | obj => 146 | if (condition) doModify(obj, mod) 147 | else obj 148 | 149 | /** see [[PathModify.setTo]] 150 | */ 151 | def setTo(v: U): T => T = obj => doModify(obj, _ => v) 152 | 153 | /** see [[PathModify.setToIfDefined]] 154 | */ 155 | def setToIfDefined(v: Option[U]): T => T = v.fold((obj: T) => obj)(setTo) 156 | 157 | /** see [[PathModify.setToIf]] 158 | */ 159 | def setToIf(condition: Boolean)(v: => U): T => T = 160 | if (condition) setTo(v) 161 | else obj => obj 162 | 163 | /** see [[AbstractPathModifyPimp]] 164 | */ 165 | def andThenModify[V](f2: PathLazyModify[U, V]): PathLazyModify[T, V] = 166 | PathLazyModify[T, V]((t, vv) => self.doModify(t, u => f2.doModify(u, vv))) 167 | } 168 | 169 | trait QuicklensFunctor[F[_], A] { 170 | def map(fa: F[A])(f: A => A): F[A] 171 | def each(fa: F[A])(f: A => A): F[A] = map(fa)(f) 172 | def eachWhere(fa: F[A], p: A => Boolean)(f: A => A): F[A] = map(fa) { a => 173 | if (p(a)) f(a) else a 174 | } 175 | } 176 | 177 | implicit def optionQuicklensFunctor[A]: QuicklensFunctor[Option, A] with QuicklensSingleAtFunctor[Option, A] = 178 | new QuicklensFunctor[Option, A] with QuicklensSingleAtFunctor[Option, A] { 179 | override def map(fa: Option[A])(f: A => A): Option[A] = fa.map(f) 180 | override def at(fa: Option[A])(f: A => A): Option[A] = Some(fa.map(f).get) 181 | override def atOrElse(fa: Option[A], default: => A)(f: A => A): Option[A] = fa.orElse(Some(default)).map(f) 182 | override def index(fa: Option[A])(f: A => A): Option[A] = fa.map(f) 183 | } 184 | 185 | // Currently only used for [[Option]], but could be used for [[Right]]-biased [[Either]]s. 186 | trait QuicklensSingleAtFunctor[F[_], T] { 187 | def at(fa: F[T])(f: T => T): F[T] 188 | def atOrElse(fa: F[T], default: => T)(f: T => T): F[T] 189 | def index(fa: F[T])(f: T => T): F[T] 190 | } 191 | 192 | implicit class QuicklensSingleAt[F[_], T](t: F[T])(implicit f: QuicklensSingleAtFunctor[F, T]) { 193 | @compileTimeOnly(canOnlyBeUsedInsideModify("at")) 194 | def at: T = sys.error("") 195 | 196 | @compileTimeOnly(canOnlyBeUsedInsideModify("at")) 197 | def atOrElse(default: => T): T = sys.error("") 198 | 199 | @compileTimeOnly(canOnlyBeUsedInsideModify("index")) 200 | def index: T = sys.error("") 201 | } 202 | 203 | implicit def traversableQuicklensFunctor[F[_], A](implicit 204 | fac: Factory[A, F[A]], 205 | ev: F[A] => Iterable[A] 206 | ): QuicklensFunctor[F, A] = 207 | new QuicklensFunctor[F, A] { 208 | override def map(fa: F[A])(f: A => A) = ev(fa).map(f).to(fac) 209 | } 210 | 211 | implicit class QuicklensAt[F[_], T](t: F[T])(implicit f: QuicklensAtFunctor[F, T]) { 212 | @compileTimeOnly(canOnlyBeUsedInsideModify("at")) 213 | def at(idx: Int): T = sys.error("") 214 | @compileTimeOnly(canOnlyBeUsedInsideModify("index")) 215 | def index(idx: Int): T = sys.error("") 216 | } 217 | 218 | trait QuicklensAtFunctor[F[_], T] { 219 | def at(fa: F[T], idx: Int)(f: T => T): F[T] 220 | def index(fa: F[T], idx: Int)(f: T => T): F[T] 221 | } 222 | 223 | implicit class QuicklensMapAt[M[KT, TT], K, T](t: M[K, T])(implicit 224 | f: QuicklensMapAtFunctor[M, K, T] 225 | ) { 226 | @compileTimeOnly(canOnlyBeUsedInsideModify("at")) 227 | def at(idx: K): T = sys.error("") 228 | 229 | @compileTimeOnly(canOnlyBeUsedInsideModify("atOrElse")) 230 | def atOrElse(idx: K, default: => T): T = sys.error("") 231 | 232 | @compileTimeOnly(canOnlyBeUsedInsideModify("index")) 233 | def index(idx: K): T = sys.error("") 234 | 235 | @compileTimeOnly(canOnlyBeUsedInsideModify("each")) 236 | def each: T = sys.error("") 237 | } 238 | 239 | trait QuicklensMapAtFunctor[F[_, _], K, T] { 240 | def at(fa: F[K, T], idx: K)(f: T => T): F[K, T] 241 | def atOrElse(fa: F[K, T], idx: K, default: => T)(f: T => T): F[K, T] 242 | def index(fa: F[K, T], idx: K)(f: T => T): F[K, T] 243 | def each(fa: F[K, T])(f: T => T): F[K, T] 244 | } 245 | 246 | implicit def mapQuicklensFunctor[M[KT, TT] <: Map[KT, TT], K, T](implicit 247 | fac: Factory[(K, T), M[K, T]] 248 | ): QuicklensMapAtFunctor[M, K, T] = new QuicklensMapAtFunctor[M, K, T] { 249 | override def at(fa: M[K, T], key: K)(f: T => T): M[K, T] = 250 | fa.updated(key, f(fa(key))).to(fac) 251 | override def atOrElse(fa: M[K, T], key: K, default: => T)(f: T => T): M[K, T] = 252 | fa.updated(key, f(fa.getOrElse(key, default))).to(fac) 253 | override def index(fa: M[K, T], key: K)(f: T => T): M[K, T] = 254 | fa.get(key).map(f).fold(fa)(t => fa.updated(key, t).to(fac)) 255 | override def each(fa: M[K, T])(f: (T) => T): M[K, T] = { 256 | fa.view.mapValues(f).to(fac) 257 | } 258 | } 259 | 260 | implicit def seqQuicklensAtFunctor[F[_], T](implicit 261 | fac: Factory[T, F[T]], 262 | ev: F[T] => Seq[T] 263 | ): QuicklensAtFunctor[F, T] = 264 | new QuicklensAtFunctor[F, T] { 265 | override def at(fa: F[T], idx: Int)(f: T => T): F[T] = 266 | fa.updated(idx, f(fa(idx))).to(fac) 267 | override def index(fa: F[T], idx: Int)(f: T => T): F[T] = 268 | if (idx < fa.size) fa.updated(idx, f(fa(idx))).to(fac) else fa 269 | } 270 | 271 | implicit class QuicklensWhen[A](value: A) { 272 | @compileTimeOnly(canOnlyBeUsedInsideModify("when")) 273 | def when[B <: A]: B = sys.error("") 274 | } 275 | 276 | implicit class QuicklensEither[T[_, _], L, R](e: T[L, R])(implicit f: QuicklensEitherFunctor[T, L, R]) { 277 | @compileTimeOnly(canOnlyBeUsedInsideModify("eachLeft")) 278 | def eachLeft: L = sys.error("") 279 | 280 | @compileTimeOnly(canOnlyBeUsedInsideModify("eachRight")) 281 | def eachRight: R = sys.error("") 282 | } 283 | 284 | trait QuicklensEitherFunctor[T[_, _], L, R] { 285 | def eachLeft(e: T[L, R])(f: L => L): T[L, R] 286 | def eachRight(e: T[L, R])(f: R => R): T[L, R] 287 | } 288 | 289 | implicit def eitherQuicklensFunctor[T[_, _], L, R]: QuicklensEitherFunctor[Either, L, R] = 290 | new QuicklensEitherFunctor[Either, L, R] { 291 | override def eachLeft(e: Either[L, R])(f: (L) => L) = e.left.map(f) 292 | override def eachRight(e: Either[L, R])(f: (R) => R) = e.map(f) 293 | } 294 | } 295 | -------------------------------------------------------------------------------- /quicklens/src/main/scala-2.13-/com.softwaremill.quicklens/LowPriorityImplicits.scala: -------------------------------------------------------------------------------- 1 | package com.softwaremill.quicklens 2 | 3 | import scala.annotation.compileTimeOnly 4 | 5 | private[quicklens] trait LowPriorityImplicits { 6 | 7 | /** `QuicklensEach` is in `LowPriorityImplicits` to not conflict with the `QuicklensMapAtFunctor` on `each` calls. 8 | */ 9 | implicit class QuicklensEach[F[_], T](t: F[T])(implicit f: QuicklensFunctor[F, T]) { 10 | @compileTimeOnly(canOnlyBeUsedInsideModify("each")) 11 | def each: T = sys.error("") 12 | 13 | @compileTimeOnly(canOnlyBeUsedInsideModify("eachWhere")) 14 | def eachWhere(p: T => Boolean): T = sys.error("") 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /quicklens/src/main/scala-2.13-/com.softwaremill.quicklens/package.scala: -------------------------------------------------------------------------------- 1 | package com.softwaremill 2 | 3 | import scala.annotation.compileTimeOnly 4 | import scala.collection.TraversableLike 5 | import scala.collection.SeqLike 6 | import scala.collection.generic.CanBuildFrom 7 | import scala.language.experimental.macros 8 | import scala.language.higherKinds 9 | 10 | package object quicklens extends LowPriorityImplicits { 11 | 12 | private[softwaremill] def canOnlyBeUsedInsideModify(method: String) = 13 | s"$method can only be used inside modify" 14 | 15 | /** Create an object allowing modifying the given (deeply nested) field accessible in a `case class` hierarchy via 16 | * `path` on the given `obj`. 17 | * 18 | * All modifications are side-effect free and create copies of the original objects. 19 | * 20 | * You can use `.each` to traverse options, lists, etc. 21 | */ 22 | def modify[T, U](obj: T)(path: T => U): PathModify[T, U] = macro QuicklensMacros.modify_impl[T, U] 23 | 24 | /** Create an object allowing modifying the given (deeply nested) fields accessible in a `case class` hierarchy via 25 | * `paths` on the given `obj`. 26 | * 27 | * All modifications are side-effect free and create copies of the original objects. 28 | * 29 | * You can use `.each` to traverse options, lists, etc. 30 | */ 31 | def modifyAll[T, U](obj: T)(path1: T => U, paths: (T => U)*): PathModify[T, U] = 32 | macro QuicklensMacros.modifyAll_impl[T, U] 33 | 34 | implicit class ModifyPimp[T](t: T) { 35 | 36 | /** Create an object allowing modifying the given (deeply nested) field accessible in a `case class` hierarchy via 37 | * `path` on the given `obj`. 38 | * 39 | * All modifications are side-effect free and create copies of the original objects. 40 | * 41 | * You can use `.each` to traverse options, lists, etc. 42 | */ 43 | def modify[U](path: T => U): PathModify[T, U] = macro QuicklensMacros.modifyPimp_impl[T, U] 44 | 45 | /** Create an object allowing modifying the given (deeply nested) fields accessible in a `case class` hierarchy via 46 | * `paths` on the given `obj`. 47 | * 48 | * All modifications are side-effect free and create copies of the original objects. 49 | * 50 | * You can use `.each` to traverse options, lists, etc. 51 | */ 52 | def modifyAll[U](path1: T => U, paths: (T => U)*): PathModify[T, U] = macro QuicklensMacros.modifyAllPimp_impl[T, U] 53 | } 54 | 55 | case class PathModify[T, U](obj: T, doModify: (T, U => U) => T) { 56 | 57 | /** Transform the value of the field(s) using the given function. 58 | * 59 | * @return 60 | * A copy of the root object with the (deeply nested) field(s) modified. 61 | */ 62 | def using(mod: U => U): T = doModify(obj, mod) 63 | 64 | /** An alias for [[using]]. Explicit calls to [[using]] are preferred over this alias, but quicklens provides this 65 | * option because code auto-formatters (like scalafmt) will generally not keep [[modify]]/[[using]] pairs on the 66 | * same line, leading to code like 67 | * {{{ 68 | * x 69 | * .modify(_.foo) 70 | * .using(newFoo :: _) 71 | * .modify(_.bar) 72 | * .using(_ + newBar) 73 | * }}} 74 | * When using [[apply]], scalafmt will allow 75 | * {{{ 76 | * x 77 | * .modify(_.foo)(newFoo :: _) 78 | * .modify(_.bar)(_ + newBar) 79 | * }}} 80 | */ 81 | final def apply(mod: U => U): T = using(mod) 82 | 83 | /** Transform the value of the field(s) using the given function, if the condition is true. Otherwise, returns the 84 | * original object unchanged. 85 | * 86 | * @return 87 | * A copy of the root object with the (deeply nested) field(s) modified, if `condition` is true. 88 | */ 89 | def usingIf(condition: Boolean)(mod: U => U): T = if (condition) doModify(obj, mod) else obj 90 | 91 | /** Set the value of the field(s) to a new value. 92 | * 93 | * @return 94 | * A copy of the root object with the (deeply nested) field(s) set to the new value. 95 | */ 96 | def setTo(v: U): T = doModify(obj, _ => v) 97 | 98 | /** Set the value of the field(s) to a new value, if it is defined. Otherwise, returns the original object 99 | * unchanged. 100 | * 101 | * @return 102 | * A copy of the root object with the (deeply nested) field(s) set to the new value, if it is defined. 103 | */ 104 | def setToIfDefined(v: Option[U]): T = v.fold(obj)(setTo) 105 | 106 | /** Set the value of the field(s) to a new value, if the condition is true. Otherwise, returns the original object 107 | * unchanged. 108 | * 109 | * @return 110 | * A copy of the root object with the (deeply nested) field(s) set to the new value, if `condition` is true. 111 | */ 112 | def setToIf(condition: Boolean)(v: => U): T = if (condition) setTo(v) else obj 113 | } 114 | 115 | implicit class AbstractPathModifyPimp[T, U](f1: T => PathModify[T, U]) { 116 | def andThenModify[V](f2: U => PathModify[U, V]): T => PathModify[T, V] = { t: T => 117 | PathModify[T, V](t, (t, vv) => f1(t).doModify(t, u => f2(u).doModify(u, vv))) 118 | } 119 | } 120 | 121 | def modifyLens[T]: LensHelper[T] = LensHelper[T]() 122 | 123 | def modifyAllLens[T]: MultiLensHelper[T] = MultiLensHelper[T]() 124 | 125 | case class LensHelper[T] private () { 126 | 127 | def apply[U](path: T => U): PathLazyModify[T, U] = macro QuicklensMacros.modifyLazy_impl[T, U] 128 | } 129 | 130 | case class MultiLensHelper[T] private () { 131 | 132 | def apply[U](path1: T => U, paths: (T => U)*): PathLazyModify[T, U] = macro QuicklensMacros.modifyLazyAll_impl[T, U] 133 | } 134 | 135 | case class PathLazyModify[T, U](doModify: (T, U => U) => T) { 136 | 137 | self => 138 | 139 | /** see [[PathModify.using]] 140 | */ 141 | def using(mod: U => U): T => T = obj => doModify(obj, mod) 142 | 143 | /** see [[PathModify.usingIf]] 144 | */ 145 | def usingIf(condition: Boolean)(mod: U => U): T => T = 146 | obj => 147 | if (condition) doModify(obj, mod) 148 | else obj 149 | 150 | /** see [[PathModify.setTo]] 151 | */ 152 | def setTo(v: U): T => T = obj => doModify(obj, _ => v) 153 | 154 | /** see [[PathModify.setToIfDefined]] 155 | */ 156 | def setToIfDefined(v: Option[U]): T => T = v.fold((obj: T) => obj)(setTo) 157 | 158 | /** see [[PathModify.setToIf]] 159 | */ 160 | def setToIf(condition: Boolean)(v: => U): T => T = 161 | if (condition) setTo(v) 162 | else obj => obj 163 | 164 | /** see [[AbstractPathModifyPimp]] 165 | */ 166 | def andThenModify[V](f2: PathLazyModify[U, V]): PathLazyModify[T, V] = 167 | PathLazyModify[T, V]((t, vv) => self.doModify(t, u => f2.doModify(u, vv))) 168 | } 169 | 170 | trait QuicklensFunctor[F[_], A] { 171 | def map(fa: F[A])(f: A => A): F[A] 172 | def each(fa: F[A])(f: A => A): F[A] = map(fa)(f) 173 | def eachWhere(fa: F[A], p: A => Boolean)(f: A => A): F[A] = map(fa) { a => 174 | if (p(a)) f(a) else a 175 | } 176 | } 177 | 178 | implicit def optionQuicklensFunctor[A]: QuicklensFunctor[Option, A] with QuicklensSingleAtFunctor[Option, A] = 179 | new QuicklensFunctor[Option, A] with QuicklensSingleAtFunctor[Option, A] { 180 | override def map(fa: Option[A])(f: A => A): Option[A] = fa.map(f) 181 | override def at(fa: Option[A])(f: A => A): Option[A] = Some(fa.map(f).get) 182 | override def atOrElse(fa: Option[A], default: => A)(f: A => A): Option[A] = fa.orElse(Some(default)).map(f) 183 | override def index(fa: Option[A])(f: A => A): Option[A] = fa.map(f) 184 | } 185 | 186 | // Currently only used for [[Option]], but could be used for [[Right]]-biased [[Either]]s. 187 | trait QuicklensSingleAtFunctor[F[_], T] { 188 | def at(fa: F[T])(f: T => T): F[T] 189 | def atOrElse(fa: F[T], default: => T)(f: T => T): F[T] 190 | def index(fa: F[T])(f: T => T): F[T] 191 | } 192 | 193 | implicit class QuicklensSingleAt[F[_], T](t: F[T])(implicit f: QuicklensSingleAtFunctor[F, T]) { 194 | @compileTimeOnly(canOnlyBeUsedInsideModify("at")) 195 | def at: T = sys.error("") 196 | 197 | @compileTimeOnly(canOnlyBeUsedInsideModify("atOrElse")) 198 | def atOrElse(default: => T): T = sys.error("") 199 | 200 | @compileTimeOnly(canOnlyBeUsedInsideModify("index")) 201 | def index: T = sys.error("") 202 | } 203 | 204 | implicit def traversableQuicklensFunctor[F[_], A](implicit 205 | cbf: CanBuildFrom[F[A], A, F[A]], 206 | ev: F[A] => TraversableLike[A, F[A]] 207 | ): QuicklensFunctor[F, A] = 208 | new QuicklensFunctor[F, A] { 209 | override def map(fa: F[A])(f: A => A) = fa.map(f) 210 | } 211 | 212 | implicit class QuicklensAt[F[_], T](t: F[T])(implicit f: QuicklensAtFunctor[F, T]) { 213 | @compileTimeOnly(canOnlyBeUsedInsideModify("at")) 214 | def at(idx: Int): T = sys.error("") 215 | 216 | @compileTimeOnly(canOnlyBeUsedInsideModify("index")) 217 | def index(idx: Int): T = sys.error("") 218 | } 219 | 220 | trait QuicklensAtFunctor[F[_], T] { 221 | def at(fa: F[T], idx: Int)(f: T => T): F[T] 222 | def index(fa: F[T], idx: Int)(f: T => T): F[T] 223 | } 224 | 225 | implicit class QuicklensMapAt[M[KT, TT], K, T](t: M[K, T])(implicit 226 | f: QuicklensMapAtFunctor[M, K, T] 227 | ) { 228 | @compileTimeOnly(canOnlyBeUsedInsideModify("at")) 229 | def at(idx: K): T = sys.error("") 230 | 231 | @compileTimeOnly(canOnlyBeUsedInsideModify("atOrElse")) 232 | def atOrElse(idx: K, default: => T): T = sys.error("") 233 | 234 | @compileTimeOnly(canOnlyBeUsedInsideModify("index")) 235 | def index(idx: K): T = sys.error("") 236 | 237 | @compileTimeOnly(canOnlyBeUsedInsideModify("each")) 238 | def each: T = sys.error("") 239 | } 240 | 241 | trait QuicklensMapAtFunctor[F[_, _], K, T] { 242 | def at(fa: F[K, T], idx: K)(f: T => T): F[K, T] 243 | def atOrElse(fa: F[K, T], idx: K, default: => T)(f: T => T): F[K, T] 244 | def index(fa: F[K, T], idx: K)(f: T => T): F[K, T] 245 | def each(fa: F[K, T])(f: T => T): F[K, T] 246 | } 247 | 248 | implicit def mapQuicklensFunctor[M[KT, TT] <: Map[KT, TT], K, T](implicit 249 | cbf: CanBuildFrom[M[K, T], (K, T), M[K, T]] 250 | ): QuicklensMapAtFunctor[M, K, T] = new QuicklensMapAtFunctor[M, K, T] { 251 | override def at(fa: M[K, T], key: K)(f: T => T): M[K, T] = 252 | fa.updated(key, f(fa(key))).asInstanceOf[M[K, T]] 253 | override def atOrElse(fa: M[K, T], key: K, default: => T)(f: T => T): M[K, T] = 254 | fa.updated(key, f(fa.getOrElse(key, default))).asInstanceOf[M[K, T]] 255 | override def index(fa: M[K, T], key: K)(f: T => T): M[K, T] = { 256 | fa.get(key).map(f).fold(fa)(t => fa.updated(key, t).asInstanceOf[M[K, T]]) 257 | } 258 | override def each(fa: M[K, T])(f: (T) => T): M[K, T] = { 259 | val builder = cbf(fa) 260 | fa.foreach { case (k, t) => builder += k -> f(t) } 261 | builder.result 262 | } 263 | } 264 | 265 | implicit def seqQuicklensAtFunctor[F[_], T](implicit 266 | cbf: CanBuildFrom[F[T], T, F[T]], 267 | ev: F[T] => SeqLike[T, F[T]] 268 | ): QuicklensAtFunctor[F, T] = 269 | new QuicklensAtFunctor[F, T] { 270 | override def at(fa: F[T], idx: Int)(f: T => T): F[T] = 271 | fa.updated(idx, f(fa(idx))) 272 | override def index(fa: F[T], idx: Int)(f: T => T): F[T] = 273 | if (idx < fa.size) fa.updated(idx, f(fa(idx))) else fa 274 | } 275 | 276 | implicit class QuicklensWhen[A](value: A) { 277 | @compileTimeOnly(canOnlyBeUsedInsideModify("when")) 278 | def when[B <: A]: B = sys.error("") 279 | } 280 | 281 | implicit class QuicklensEither[T[_, _], L, R](e: T[L, R])(implicit f: QuicklensEitherFunctor[T, L, R]) { 282 | @compileTimeOnly(canOnlyBeUsedInsideModify("eachLeft")) 283 | def eachLeft: L = sys.error("") 284 | 285 | @compileTimeOnly(canOnlyBeUsedInsideModify("eachRight")) 286 | def eachRight: R = sys.error("") 287 | } 288 | 289 | trait QuicklensEitherFunctor[T[_, _], L, R] { 290 | def eachLeft(e: T[L, R])(f: L => L): T[L, R] 291 | def eachRight(e: T[L, R])(f: R => R): T[L, R] 292 | } 293 | 294 | implicit def eitherQuicklensFunctor[T[_, _], L, R]: QuicklensEitherFunctor[Either, L, R] = 295 | new QuicklensEitherFunctor[Either, L, R] { 296 | override def eachLeft(e: Either[L, R])(f: (L) => L) = e.left.map(f) 297 | override def eachRight(e: Either[L, R])(f: (R) => R) = e.right.map(f) 298 | } 299 | } 300 | -------------------------------------------------------------------------------- /quicklens/src/main/scala-2/com/softwaremill/quicklens/QuicklensMacros.scala: -------------------------------------------------------------------------------- 1 | package com.softwaremill.quicklens 2 | 3 | import scala.annotation.tailrec 4 | import scala.reflect.macros.blackbox 5 | 6 | object QuicklensMacros { 7 | private val ShapeInfo = "Path must have shape: _.field1.field2.each.field3.(...)" 8 | 9 | /** modify(a)(_.b.c) => new PathMod(a, (A, F) => A.copy(b = A.b.copy(c = F(A.b.c)))) 10 | */ 11 | def modify_impl[T: c.WeakTypeTag, U: c.WeakTypeTag](c: blackbox.Context)(obj: c.Expr[T])( 12 | path: c.Expr[T => U] 13 | ): c.Tree = modifyUnwrapped(c)(obj, modificationForPath(c)(path)) 14 | 15 | /** modifyAll(a)(_.b.c, _.d.e) => new PathMod(a, << chained modifications >>) 16 | */ 17 | def modifyAll_impl[T: c.WeakTypeTag, U: c.WeakTypeTag](c: blackbox.Context)(obj: c.Expr[T])( 18 | path1: c.Expr[T => U], 19 | paths: c.Expr[T => U]* 20 | ): c.Tree = modifyUnwrapped(c)(obj, modificationsForPaths(c)(path1, paths)) 21 | 22 | /** A helper method for modify_impl and modifyAll_impl. 23 | */ 24 | private def modifyUnwrapped[T: c.WeakTypeTag, U: c.WeakTypeTag](c: blackbox.Context)( 25 | obj: c.Expr[T], 26 | modifications: c.Tree 27 | ): c.Tree = { 28 | import c.universe._ 29 | q"_root_.com.softwaremill.quicklens.PathModify($obj, $modifications)" 30 | } 31 | 32 | /** modify[A](_.b.c) => a => new PathMod(a, (A, F) => A.copy(b = A.b.copy(c = F(A.b.c)))) 33 | */ 34 | def modifyLazy_impl[T: c.WeakTypeTag, U: c.WeakTypeTag](c: blackbox.Context)( 35 | path: c.Expr[T => U] 36 | ): c.Tree = modifyLazyUnwrapped(c)(modificationForPath(c)(path)) 37 | 38 | /** modifyAll[A](_.b.c, _.d.e) => a => new PathMod(a, << chained modifications >>) 39 | */ 40 | def modifyLazyAll_impl[T: c.WeakTypeTag, U: c.WeakTypeTag](c: blackbox.Context)( 41 | path1: c.Expr[T => U], 42 | paths: c.Expr[T => U]* 43 | ): c.Tree = modifyLazyUnwrapped(c)(modificationsForPaths(c)(path1, paths)) 44 | 45 | /** A helper method for modify_impl and modifyAll_impl. 46 | */ 47 | private def modifyLazyUnwrapped[T: c.WeakTypeTag, U: c.WeakTypeTag](c: blackbox.Context)( 48 | modifications: c.Tree 49 | ): c.Tree = { 50 | import c.universe._ 51 | q"_root_.com.softwaremill.quicklens.PathLazyModify($modifications)" 52 | } 53 | 54 | /** a.modify(_.b.c) => new PathMod(a, (A, F) => A.copy(b = A.b.copy(c = F(A.b.c)))) 55 | */ 56 | def modifyPimp_impl[T: c.WeakTypeTag, U: c.WeakTypeTag](c: blackbox.Context)( 57 | path: c.Expr[T => U] 58 | ): c.Tree = modifyWrapped(c)(modificationForPath(c)(path)) 59 | 60 | /** a.modify(_.b.c, _.d.e) => new PathMod(a, << chained modifications >>) 61 | */ 62 | def modifyAllPimp_impl[T: c.WeakTypeTag, U: c.WeakTypeTag](c: blackbox.Context)( 63 | path1: c.Expr[T => U], 64 | paths: c.Expr[T => U]* 65 | ): c.Tree = modifyWrapped(c)(modificationsForPaths(c)(path1, paths)) 66 | 67 | /** A helper method for modifyPimp_impl and modifyAllPimp_impl. 68 | */ 69 | def modifyWrapped[T: c.WeakTypeTag, U: c.WeakTypeTag](c: blackbox.Context)( 70 | modifications: c.Tree 71 | ): c.Tree = { 72 | import c.universe._ 73 | 74 | val wrappedValue = c.macroApplication match { 75 | case Apply(TypeApply(Select(Apply(_, List(w)), _), _), _) => w 76 | case _ => c.abort(c.enclosingPosition, s"Unknown usage of ModifyPimp. Please file a bug.") 77 | } 78 | 79 | val valueAlias = TermName(c.freshName()) 80 | 81 | q"""{ 82 | val $valueAlias = $wrappedValue; 83 | _root_.com.softwaremill.quicklens.PathModify(${Ident(valueAlias)}, $modifications) 84 | }""" 85 | } 86 | 87 | /** Compose modifications generated for each path, from left to right. 88 | */ 89 | private def modificationsForPaths[T: c.WeakTypeTag, U: c.WeakTypeTag](c: blackbox.Context)( 90 | path1: c.Expr[T => U], 91 | paths: Seq[c.Expr[T => U]] 92 | ): c.Tree = { 93 | import c.universe._ 94 | 95 | val valueName = TermName(c.freshName()) 96 | val modifierName = TermName(c.freshName()) 97 | 98 | val modification1 = q"${modificationForPath(c)(path1)}($valueName, $modifierName)" 99 | val chained = paths.foldLeft(modification1) { case (tree, path) => 100 | val modification = modificationForPath(c)(path) 101 | q"$modification($tree, $modifierName)" 102 | } 103 | 104 | val valueArg = q"val $valueName: ${weakTypeOf[T]}" 105 | val modifierArg = q"val $modifierName: (${weakTypeOf[U]} => ${weakTypeOf[U]})" 106 | q"($valueArg, $modifierArg) => $chained" 107 | } 108 | 109 | /** Produce a modification for a single path. 110 | */ 111 | private def modificationForPath[T: c.WeakTypeTag, U: c.WeakTypeTag](c: blackbox.Context)( 112 | path: c.Expr[T => U] 113 | ): c.Tree = { 114 | import c.universe._ 115 | 116 | sealed trait PathAccess 117 | case object DirectPathAccess extends PathAccess 118 | case class SealedPathAccess(types: Set[Symbol]) extends PathAccess 119 | 120 | sealed trait PathElement 121 | case class TermPathElement(term: c.TermName, access: PathAccess, xargs: c.Tree*) extends PathElement 122 | case class SubtypePathElement(subtype: c.Type) extends PathElement 123 | case class FunctorPathElement(functor: c.Tree, method: c.TermName, xargs: c.Tree*) extends PathElement 124 | 125 | /** Determine if the `.copy` method should be applied directly or through a match across all subclasses (for sealed 126 | * traits). 127 | */ 128 | def determinePathAccess(typeSymbol: Symbol) = { 129 | def ifEmpty[A](set: Set[A], empty: => Set[A]) = 130 | if (set.isEmpty) empty else set 131 | 132 | def knownDirectSubclasses(sealedSymbol: ClassSymbol) = ifEmpty( 133 | sealedSymbol.knownDirectSubclasses, 134 | c.abort( 135 | c.enclosingPosition, 136 | s"""Could not find subclasses of sealed trait $sealedSymbol. 137 | |You might need to ensure that it gets compiled before this invocation. 138 | |See also: .""".stripMargin 139 | ) 140 | ) 141 | 142 | def expand(symbol: Symbol): Set[Symbol] = 143 | Set(symbol) 144 | .filter(_.isClass) 145 | .map(_.asClass) 146 | .map { s => 147 | s.typeSignature; s 148 | } // see 149 | .filter(_.isSealed) 150 | .flatMap(s => knownDirectSubclasses(s)) 151 | .flatMap(s => ifEmpty(expand(s), Set(s))) 152 | 153 | val subclasses = expand(typeSymbol) 154 | if (subclasses.isEmpty) DirectPathAccess else SealedPathAccess(subclasses) 155 | } 156 | 157 | /** _.a.b.each.c => List(TPE(a), TPE(b), FPE(functor, each/at/eachWhere, xargs), TPE(c)) 158 | */ 159 | @tailrec 160 | def collectPathElements(tree: c.Tree, acc: List[PathElement]): List[PathElement] = { 161 | def methodSupported(method: TermName) = 162 | Seq("at", "eachWhere", "atOrElse", "index").contains(method.toString) 163 | def typeSupported(quicklensType: c.Tree) = 164 | Seq("QuicklensEach", "QuicklensAt", "QuicklensMapAt", "QuicklensWhen", "QuicklensEither", "QuicklensSingleAt") 165 | .exists(quicklensType.toString.endsWith) 166 | tree match { 167 | case q"$parent.$child" => 168 | val access = determinePathAccess(parent.tpe.typeSymbol) 169 | collectPathElements(parent, TermPathElement(child, access) :: acc) 170 | case q"$tpname[..$_]($parent).when[$tp]" if typeSupported(tpname) => 171 | collectPathElements(parent, SubtypePathElement(tp.tpe) :: acc) 172 | case q"$parent.$method(..$xargs)" if methodSupported(method) => 173 | collectPathElements(parent, TermPathElement(method, DirectPathAccess, xargs: _*) :: acc) 174 | case q"$tpname[..$_]($t)($f)" if typeSupported(tpname) => 175 | val newAcc = (acc: @unchecked) match { 176 | // replace the term controlled by quicklens 177 | case TermPathElement(term, _, xargs @ _*) :: rest => FunctorPathElement(f, term, xargs: _*) :: rest 178 | case pathEl :: _ => 179 | c.abort(c.enclosingPosition, s"Invalid use of path element $pathEl. $ShapeInfo, got: ${path.tree}") 180 | } 181 | collectPathElements(t, newAcc) 182 | case t: Ident => acc 183 | case _ => c.abort(c.enclosingPosition, s"Unsupported path element. $ShapeInfo, got: $tree") 184 | } 185 | } 186 | 187 | /** (x, List(TPE(c), TPE(b), FPE(functor, method, xargs), TPE(a))) => x.b.c 188 | */ 189 | def generateSelects(rootPathEl: c.TermName, reversePathEls: List[PathElement]): c.Tree = { 190 | @tailrec 191 | def terms(els: List[PathElement], result: List[c.TermName]): List[c.TermName] = 192 | (els: @unchecked) match { 193 | case Nil => result 194 | case TermPathElement(term, _) :: tail => terms(tail, term :: result) 195 | case SubtypePathElement(_) :: _ => result 196 | case FunctorPathElement(_, _, _*) :: _ => result 197 | } 198 | 199 | @tailrec 200 | def go(els: List[c.TermName], result: c.Tree): c.Tree = 201 | els match { 202 | case Nil => result 203 | case pathEl :: tail => 204 | val select = q"$result.$pathEl" 205 | go(tail, select) 206 | } 207 | 208 | go(terms(reversePathEls, Nil), Ident(rootPathEl)) 209 | } 210 | 211 | /** (tree, DirectPathAccess) => f(tree) 212 | * 213 | * (tree, SealedPathAccess(Set(T1, T2, ...)) => tree match { case x1: T1 => f(x1) case x2: T2 => f(x2) ... } 214 | */ 215 | def generateAccess(tree: c.Tree, access: PathAccess)(f: c.Tree => c.Tree) = access match { 216 | case DirectPathAccess => f(tree) 217 | case SealedPathAccess(types) => 218 | val cases = types map { tp => 219 | val pat = TermName(c.freshName()) 220 | cq"$pat: $tp => ${f(Ident(pat))}" 221 | } 222 | q"$tree match { case ..$cases }" 223 | } 224 | 225 | /** (a, List(TPE(d), TPE(c), FPE(functor, method, xargs), TPE(b)), k) => (aa, aa.copy(b = functor.method(aa.b, 226 | * xargs)(a => a.copy(c = a.c.copy(d = k))) 227 | */ 228 | def generateCopies( 229 | rootPathEl: c.TermName, 230 | reversePathEls: List[PathElement], 231 | newVal: c.Tree 232 | ): (c.TermName, c.Tree) = 233 | (reversePathEls: @unchecked) match { 234 | case Nil => (rootPathEl, newVal) 235 | case TermPathElement(pathEl, access) :: tail => 236 | val selectCurrVal = generateSelects(rootPathEl, tail) 237 | val copy = generateAccess(selectCurrVal, access) { currVal => 238 | q"$currVal.copy($pathEl = $newVal)" 239 | } 240 | generateCopies(rootPathEl, tail, copy) 241 | case SubtypePathElement(subtype) :: tail => 242 | val newRootPathEl = TermName(c.freshName()) 243 | val intactPathEl = TermName(c.freshName()) 244 | val selectCurrVal = generateSelects(newRootPathEl, tail) 245 | val cases = Seq( 246 | cq"$rootPathEl: $subtype => $newVal", 247 | cq"$intactPathEl => ${Ident(intactPathEl)}" 248 | ) 249 | val modifySubtype = q"$selectCurrVal match { case ..$cases }" 250 | generateCopies(newRootPathEl, tail, modifySubtype) 251 | case FunctorPathElement(functor, method, xargs @ _*) :: tail => 252 | val newRootPathEl = TermName(c.freshName()) 253 | // combine the selected path with variable args 254 | val args = generateSelects(newRootPathEl, tail) :: xargs.toList.map(c.untypecheck(_)) 255 | val rootPathElParamTree = ValDef(Modifiers(), rootPathEl, TypeTree(), EmptyTree) 256 | val functorMap = q"$functor.$method(..$args)(($rootPathElParamTree) => $newVal)" 257 | generateCopies(newRootPathEl, tail, functorMap) 258 | } 259 | 260 | // 261 | 262 | val pathEls = path.tree match { 263 | case q"($arg) => $pathBody" => collectPathElements(pathBody, Nil) 264 | case _ => c.abort(c.enclosingPosition, s"$ShapeInfo, got: ${path.tree}") 265 | } 266 | 267 | // the initial root object (the end-root object can be different if there are .each's on the way) 268 | val initialRootPathEl = TermName(c.freshName()) 269 | val fn = TermName(c.freshName()) // the function that modifies the last path element 270 | 271 | val reversePathEls = pathEls.reverse 272 | 273 | // new value of the last path element is an invocation of $fn on the current last path element value 274 | val select = generateSelects(initialRootPathEl, reversePathEls) 275 | val mod = q"$fn($select)" 276 | 277 | val (rootPathEl, copies) = generateCopies(initialRootPathEl, reversePathEls, mod) 278 | 279 | val rootPathElParamTree = q"val $rootPathEl: ${weakTypeOf[T]}" 280 | val fnParamTree = q"val $fn: (${weakTypeOf[U]} => ${weakTypeOf[U]})" 281 | 282 | q"($rootPathElParamTree, $fnParamTree) => $copies" 283 | } 284 | } 285 | -------------------------------------------------------------------------------- /quicklens/src/main/scala-3/com/softwaremill/quicklens/QuicklensMacros.scala: -------------------------------------------------------------------------------- 1 | package com.softwaremill.quicklens 2 | 3 | import scala.quoted.* 4 | 5 | object QuicklensMacros { 6 | def toPathModify[S: Type, A: Type](obj: Expr[S], f: Expr[(A => A) => S])(using Quotes): Expr[PathModify[S, A]] = '{ 7 | PathModify(${ obj }, ${ f }) 8 | } 9 | 10 | def fromPathModify[S: Type, A: Type](pathModify: Expr[PathModify[S, A]])(using Quotes): Expr[(A => A) => S] = '{ 11 | ${ pathModify }.f 12 | } 13 | 14 | def to[T: Type, R: Type](f: Expr[T] => Expr[R])(using Quotes): Expr[T => R] = '{ (x: T) => ${ f('x) } } 15 | 16 | def from[T: Type, R: Type](f: Expr[T => R])(using Quotes): Expr[T] => Expr[R] = (x: Expr[T]) => '{ $f($x) } 17 | 18 | def modifyLensApplyImpl[T, U](path: Expr[T => U])(using Quotes, Type[T], Type[U]): Expr[PathLazyModify[T, U]] = '{ 19 | PathLazyModify { (t, mod) => 20 | ${ 21 | toPathModify('t, modifyImpl('t, Seq(path))) 22 | }.using(mod) 23 | } 24 | } 25 | 26 | def modifyAllLensApplyImpl[T: Type, U: Type]( 27 | path1: Expr[T => U], 28 | paths: Expr[Seq[T => U]] 29 | )(using Quotes): Expr[PathLazyModify[T, U]] = 30 | '{ PathLazyModify((t, mod) => ${ modifyAllImpl('t, path1, paths) }.using(mod)) } 31 | 32 | def modifyAllImpl[S: Type, A: Type]( 33 | obj: Expr[S], 34 | focus: Expr[S => A], 35 | focusesExpr: Expr[Seq[S => A]] 36 | )(using Quotes): Expr[PathModify[S, A]] = { 37 | val focuses = focusesExpr match { 38 | case Varargs(args) => focus +: args 39 | } 40 | 41 | val modF = modifyImpl(obj, focuses) 42 | 43 | toPathModify(obj, modF) 44 | } 45 | 46 | def toPathModifyFromFocus[S: Type, A: Type](obj: Expr[S], focus: Expr[S => A])(using Quotes): Expr[PathModify[S, A]] = 47 | toPathModify(obj, modifyImpl(obj, Seq(focus))) 48 | 49 | private def modifyImpl[S: Type, A: Type]( 50 | obj: Expr[S], 51 | focuses: Seq[Expr[S => A]] 52 | )(using Quotes): Expr[(A => A) => S] = { 53 | import quotes.reflect.* 54 | 55 | def unsupportedShapeInfo(tree: Tree) = 56 | s"Unsupported path element. Path must have shape: _.field1.field2.each.field3.(...), got: ${tree.show}" 57 | 58 | def noSuchMember(tpeStr: String, name: String) = 59 | s"$tpeStr has no member named $name" 60 | 61 | def noSuitableMember(tpeStr: String, name: String, argNames: Iterable[String]) = 62 | s"$tpeStr has no member $name with parameters ${argNames.mkString("(", ", ", ")")}" 63 | 64 | def multipleMatchingMethods(tpeStr: String, name: String, syms: Seq[Symbol]) = 65 | val symsStr = syms.map(s => s" - $s: ${s.termRef.dealias.widen.show}").mkString("\n", "\n", "") 66 | s"Multiple methods named $name found in $tpeStr: $symsStr" 67 | 68 | def methodSupported(method: String) = 69 | Seq("at", "each", "eachWhere", "eachRight", "eachLeft", "atOrElse", "index", "when").contains(method) 70 | 71 | enum PathTree: 72 | case Empty 73 | case Node(children: Seq[(PathSymbol, Seq[PathTree])]) 74 | 75 | def <>(symbols: Seq[PathSymbol]): PathTree = ((this, symbols): @unchecked) match 76 | case (PathTree.Empty, _) => 77 | symbols.toPathTree 78 | case (PathTree.Node(children), (symbol :: Nil)) => 79 | PathTree.Node { 80 | if children.find(_._1 equiv symbol).isEmpty then children :+ (symbol -> Seq(PathTree.Empty)) 81 | else 82 | children.map { 83 | case (sym, trees) if sym equiv symbol => 84 | sym -> (trees :+ PathTree.Empty) 85 | case c => c 86 | } 87 | } 88 | case (PathTree.Node(children), Nil) => 89 | this 90 | case (PathTree.Node(children), (symbol :: tail)) => 91 | PathTree.Node { 92 | if children.find(_._1 equiv symbol).isEmpty then children :+ (symbol -> Seq(tail.toPathTree)) 93 | else 94 | children.map { 95 | case (sym, trees) if sym equiv symbol => 96 | sym -> (trees.init ++ { 97 | trees.last match 98 | case PathTree.Empty => Seq(PathTree.Empty, tail.toPathTree) 99 | case node => Seq(node <> tail) 100 | }) 101 | case c => c 102 | } 103 | } 104 | end PathTree 105 | 106 | object PathTree: 107 | def empty: PathTree = Empty 108 | 109 | extension (symbols: Seq[PathSymbol]) 110 | def toPathTree: PathTree = symbols match 111 | case Nil => PathTree.Empty 112 | case (symbol :: tail) => PathTree.Node(Seq(symbol -> Seq(tail.toPathTree))) 113 | 114 | enum PathSymbol: 115 | case Field(override val name: String) 116 | case Extension(term: Term, override val name: String) 117 | case FunctionDelegate(override val name: String, givn: Term, typeTree: TypeTree, args: List[Term]) 118 | def name: String 119 | 120 | def equiv(other: Any): Boolean = (this, other) match 121 | case (Field(name1), Field(name2)) => name1 == name2 122 | case (Extension(term1, name1), Extension(term2, name2)) => term1 == term2 && name1 == name2 123 | case (FunctionDelegate(name1, _, typeTree1, args1), FunctionDelegate(name2, _, typeTree2, args2)) => 124 | name1 == name2 && typeTree1.tpe == typeTree2.tpe && args1 == args2 125 | case _ => false 126 | end PathSymbol 127 | 128 | def toPath(tree: Tree, focus: Expr[S => A]): Seq[PathSymbol] = { 129 | tree match { 130 | /** Field access */ 131 | case Select(deep, ident) => 132 | toPath(deep, focus) :+ PathSymbol.Field(ident) 133 | /** Method call with arguments and using clause */ 134 | case Apply(Apply(Apply(TypeApply(Ident(s), typeTrees), idents), args), List(givn)) if methodSupported(s) => 135 | idents.flatMap(toPath(_, focus)) :+ PathSymbol.FunctionDelegate(s, givn, typeTrees.last, args) 136 | /** Method call with no arguments and using clause */ 137 | case Apply(Apply(TypeApply(Ident(s), typeTrees), idents), List(givn)) if methodSupported(s) => 138 | idents.flatMap(toPath(_, focus)) :+ PathSymbol.FunctionDelegate(s, givn, typeTrees.last, List.empty) 139 | /** Method call with one type parameter and using clause */ 140 | case a @ Apply(TypeApply(Apply(TypeApply(Ident(s), _), idents), typeTrees), List(givn)) if methodSupported(s) => 141 | idents.flatMap(toPath(_, focus)) :+ PathSymbol.FunctionDelegate(s, givn, typeTrees.last, List.empty) 142 | /** Extension method, which is called e.g. as x(_$1) */ 143 | case Apply(obj@Select(term, member), Seq(deep)) if obj.symbol.flags.is(Flags.ExtensionMethod) => 144 | toPath(deep, focus) :+ PathSymbol.Extension(term, member) 145 | /** Field access */ 146 | case Apply(deep, idents) => 147 | toPath(deep, focus) ++ idents.flatMap(toPath(_, focus)) 148 | /** Wild card from path */ 149 | case i: Ident if i.name.startsWith("_") => 150 | Seq.empty 151 | case _ => 152 | report.errorAndAbort(unsupportedShapeInfo(focus.asTerm)) 153 | } 154 | } 155 | 156 | extension (tpe: TypeRepr) 157 | def poorMansLUB: TypeRepr = tpe match { 158 | case AndType(l, r) if l <:< r => l 159 | case AndType(l, r) if r <:< l => r 160 | case _ => tpe 161 | } 162 | 163 | def widenAll: TypeRepr = 164 | tpe.widen.dealias.poorMansLUB 165 | 166 | def matchingTypeSymbol: Symbol = tpe.widenAll match { 167 | case AndType(l, r) => 168 | val lSym = l.matchingTypeSymbol 169 | if lSym != Symbol.noSymbol then lSym else r.matchingTypeSymbol 170 | case tpe if isProduct(tpe.typeSymbol) || isSum(tpe.typeSymbol) || isProductLike(tpe.typeSymbol) => 171 | tpe.typeSymbol 172 | case _ => 173 | Symbol.noSymbol 174 | } 175 | 176 | extension (term: Term) 177 | def appliedToIfNeeded(args: List[Term]): Term = 178 | if args.isEmpty then term else term.appliedToArgs(args) 179 | 180 | def symbolAccessorByNameOrError(obj: Term, name: String): Term = { 181 | val objTpe = obj.tpe.widenAll 182 | val objSymbol = objTpe.matchingTypeSymbol 183 | // opaque types can find members of underlying types - ignore them (see https://github.com/scala/scala3/issues/22143) 184 | val fieldMemberSym = objSymbol.fieldMember(name) 185 | if !objSymbol.flags.is(Flags.Deferred) && fieldMemberSym.exists then 186 | Select(obj, fieldMemberSym) 187 | else 188 | objSymbol.methodMember(name) match 189 | case List(m) => 190 | Select(obj, m) 191 | case lst => 192 | report.errorAndAbort(reportMethodError(objSymbol, name, lst)) 193 | } 194 | 195 | def reportMethodError(sym: Symbol, name: String, lst: List[Symbol], maybeArgNames: Option[Iterable[String]] = None): String = { 196 | (lst, maybeArgNames) match 197 | case (Nil, _) => noSuchMember(sym.name, name) 198 | case (lst, None) => multipleMatchingMethods(sym.name, name, lst) 199 | case (lst, Some(argNames)) => noSuitableMember(sym.name, name, argNames) 200 | } 201 | 202 | def methodSymbolByNameOrError(sym: Symbol, name: String): Symbol = { 203 | sym.methodMember(name) match 204 | case List(m) => m 205 | case lst => report.errorAndAbort(reportMethodError(sym, name, lst)) 206 | } 207 | 208 | def filterMethodsByNameAndArgs(allMethods: List[Symbol], argsMap: Map[String, Term]): Option[Symbol] = { 209 | val argNames = argsMap.keys 210 | allMethods.filter { msym => 211 | // for copy, we filter out the methods that don't have the desired parameter names 212 | val paramNames = msym.paramSymss.flatten.filter(_.isTerm).map(_.name) 213 | argNames.forall(paramNames.contains) 214 | } match 215 | case List(m) => Some(m) 216 | case Nil => None 217 | case lst@(m :: _) => 218 | // if we have multiple matching copy methods, pick the synthetic one, if it exists, otherwise, pick any method 219 | val syntheticCopies = lst.filter(_.flags.is(Flags.Synthetic)) 220 | syntheticCopies match 221 | case List(mSynth) => Some(mSynth) 222 | case _ => Some(m) 223 | } 224 | 225 | def methodSymbolByNameAndArgs(sym: Symbol, name: String, argsMap: Map[String, Term]): Either[String, Symbol] = { 226 | if !sym.flags.is(Flags.Deferred) then 227 | val memberMethods = sym.methodMember(name) 228 | filterMethodsByNameAndArgs(memberMethods, argsMap) 229 | .toRight(reportMethodError(sym, name, memberMethods, Some(argsMap.keys))) 230 | else Left(s"Deferred type ${sym.name}") 231 | } 232 | 233 | /** 234 | * @param argsMap normal methods receive one parameter list, extensions methods two, the first one contains the value 235 | * on which the extension is called 236 | * */ 237 | def callMethod(obj: Term, copy: Symbol, argsMap: List[Map[String, Term]]) = { 238 | require(argsMap.size == 1 || argsMap.size == 2, s"argsMap.size should be either 1 or 2, got: ${argsMap.size} ($argsMap)") 239 | val objTpe = obj.tpe.widenAll 240 | val objSymbol = objTpe.matchingTypeSymbol 241 | 242 | val typeParams = objTpe.typeArgs 243 | val copyTree: DefDef = copy.tree.asInstanceOf[DefDef] 244 | val copyParams: List[(String, Option[Term])] = copyTree.termParamss.zip(argsMap) 245 | .map((params, args) => params.params.map(_.name).map(name => name -> args.get(name))) 246 | .flatten.toList 247 | 248 | val args = copyParams.zipWithIndex.map { case ((n, v), _i) => 249 | val i = _i + 1 250 | def defaultMethod: Term = 251 | val methodSymbol = methodSymbolByNameOrError(objSymbol, copy.name + "$default$" + i.toString) 252 | // default values in extension methods take the extension receiver as the first parameter 253 | val defaultMethodArgs = argsMap.dropRight(1).flatMap(_.values) 254 | obj.select(methodSymbol).appliedToIfNeeded(defaultMethodArgs) 255 | n -> v.getOrElse(defaultMethod) 256 | }.toMap 257 | 258 | val argLists: List[List[Term]] = copyTree.termParamss.take(argsMap.size).map(list => list.params.map(p => args(p.name))) 259 | 260 | if copyTree.termParamss.drop(argLists.size).exists(_.params.exists(!_.symbol.flags.is(Flags.Implicit))) then 261 | report.errorAndAbort( 262 | s"Implementation limitation: Only the first parameter list of the modified case classes can be non-implicit. ${copyTree.termParamss.drop(1)}" 263 | ) 264 | 265 | val withTypeParamsApplied = obj.select(copy).appliedToTypes(typeParams) 266 | argLists.foldLeft(withTypeParamsApplied)(Apply(_, _)) 267 | } 268 | 269 | def termMethodByNameUnsafe(term: Term, name: String): Symbol = { 270 | val typeSymbol = term.tpe.widenAll.typeSymbol 271 | methodSymbolByNameOrError(typeSymbol, name) 272 | } 273 | 274 | def isProduct(sym: Symbol): Boolean = { 275 | sym.flags.is(Flags.Case) 276 | } 277 | 278 | def isSum(sym: Symbol): Boolean = { 279 | (sym.flags.is(Flags.Enum) && !sym.flags.is(Flags.Case)) || 280 | (sym.flags.is(Flags.Sealed) && (sym.flags.is(Flags.Trait) || sym.flags.is(Flags.Abstract))) 281 | } 282 | 283 | def findCompanionLikeObject(objSymbol: Symbol): Symbol = { 284 | if objSymbol.companionModule.exists then 285 | objSymbol.companionModule 286 | else 287 | val namedFromOwnerScope = objSymbol.owner.fieldMember(objSymbol.name) 288 | if namedFromOwnerScope.flags.is(Flags.Module) then namedFromOwnerScope 289 | else Symbol.noSymbol 290 | } 291 | 292 | def hasExtensionNamed(sym: Symbol, methodName: String): List[Symbol] = { 293 | val companionSymbol = findCompanionLikeObject(sym) 294 | if companionSymbol.exists then 295 | companionSymbol.methodMember(methodName).filter(s => s.name == methodName && s.flags.is(Flags.ExtensionMethod)) 296 | else 297 | Nil 298 | } 299 | 300 | def isProductLike(sym: Symbol): Boolean = { 301 | sym.methodMember("copy").nonEmpty || hasExtensionNamed(sym, "copy").nonEmpty 302 | } 303 | 304 | def caseClassCopy( 305 | owner: Symbol, 306 | mod: Expr[A => A], 307 | obj: Term, 308 | fields: Seq[(PathSymbol.Field | PathSymbol.Extension, Seq[PathTree])] 309 | ): Term = { 310 | val objTpe = obj.tpe.widenAll 311 | val objSymbol = objTpe.matchingTypeSymbol 312 | if isSum(objSymbol) then { 313 | objTpe match { 314 | case AndType(_, _) => 315 | report.errorAndAbort( 316 | s"Implementation limitation: Cannot modify sealed hierarchies mixed with & types. Try providing a more specific type." 317 | ) 318 | case _ => 319 | } 320 | /* 321 | if (obj.isInstanceOf[Child1]) caseClassCopy(obj.asInstanceOf[Child1]) else 322 | if (obj.isInstanceOf[Child2]) caseClassCopy(obj.asInstanceOf[Child2]) else 323 | ... 324 | else throw new IllegalStateException() 325 | */ 326 | val ifThens = objSymbol.children.map { child => 327 | val ifCond = TypeApply(Select.unique(obj, "isInstanceOf"), List(TypeIdent(child))) 328 | 329 | val ifThen = ValDef.let(owner, TypeApply(Select.unique(obj, "asInstanceOf"), List(TypeIdent(child)))) { 330 | castToChildVal => 331 | caseClassCopy(owner, mod, castToChildVal, fields) 332 | } 333 | 334 | ifCond -> ifThen 335 | } 336 | 337 | val elseThrow = '{ throw new IllegalStateException() }.asTerm 338 | 339 | ifThens.foldRight(elseThrow) { case ((ifCond, ifThen), ifElse) => 340 | If(ifCond, ifThen, ifElse) 341 | } 342 | } else if isProduct(objSymbol) || isProductLike(objSymbol) then { 343 | val argsMap: Map[String, Term] = fields.map { (field, trees) => 344 | val fieldMethod = field match { 345 | case PathSymbol.Field(name) => 346 | symbolAccessorByNameOrError(obj, name) 347 | case PathSymbol.Extension(term, name) => 348 | val extensionMethod = symbolAccessorByNameOrError(term, name) 349 | Apply(extensionMethod, List(obj)) 350 | } 351 | val resTerm: Term = trees.foldLeft[Term](fieldMethod) { (term, tree) => 352 | mapToCopy(owner, mod, term, tree) 353 | } 354 | val namedArg = NamedArg(field.name, resTerm) 355 | field.name -> namedArg 356 | }.toMap 357 | methodSymbolByNameAndArgs(objSymbol, "copy", argsMap) match 358 | case Right(copy) => 359 | callMethod(obj, copy, List(argsMap)) 360 | case Left(error) => 361 | val objCompanion = findCompanionLikeObject(objSymbol) 362 | methodSymbolByNameAndArgs(objCompanion, "copy", argsMap).toOption match 363 | case Some(copy) => 364 | // now try to call the extension as a method, assume the object is its first parameter 365 | val extensionParameter = copy.paramSymss.headOption.map(_.headOption).flatten 366 | val argsWithObj = List(extensionParameter.map(name => name.name -> obj).toMap, argsMap) 367 | callMethod(Ref(objCompanion), copy, argsWithObj) 368 | case None => report.errorAndAbort(error) 369 | } else 370 | report.errorAndAbort(s"Unsupported source object: must be a case class, sealed trait or class with copy method, but got: $objSymbol of type ${objTpe.show} (${obj.show})") 371 | } 372 | 373 | def applyFunctionDelegate( 374 | owner: Symbol, 375 | mod: Expr[A => A], 376 | objTerm: Term, 377 | f: PathSymbol.FunctionDelegate, 378 | tree: PathTree 379 | ): Term = 380 | val defdefSymbol = Symbol.newMethod( 381 | owner, 382 | "$anonfun", 383 | MethodType(List("x"))(_ => List(f.typeTree.tpe), _ => f.typeTree.tpe) 384 | ) 385 | val fMethod = termMethodByNameUnsafe(f.givn, f.name) 386 | val fun = TypeApply( 387 | Select(f.givn, fMethod), 388 | List(f.typeTree) 389 | ) 390 | val defdefStatements = DefDef( 391 | defdefSymbol, 392 | ((_: @unchecked) match 393 | case List(List(x)) => Some(mapToCopy(defdefSymbol, mod, x.asExpr.asTerm, tree)) 394 | ) 395 | ) 396 | val closure = Closure(Ref(defdefSymbol), None) 397 | val block = Block(List(defdefStatements), closure) 398 | Apply(fun, List(objTerm, block) ++ f.args.map(_.changeOwner(owner))) 399 | 400 | def accumulateToCopy( 401 | owner: Symbol, 402 | mod: Expr[A => A], 403 | objTerm: Term, 404 | pathSymbols: Seq[(PathSymbol, Seq[PathTree])] 405 | ): Term = pathSymbols match { 406 | 407 | case Nil => 408 | objTerm 409 | 410 | case (_: (PathSymbol.Field | PathSymbol.Extension), _) :: _ => 411 | val (fs, funs) = pathSymbols.span((ps, _) => ps.isInstanceOf[PathSymbol.Field] || ps.isInstanceOf[PathSymbol.Extension]) 412 | val fields = fs.collect { case (p: (PathSymbol.Field | PathSymbol.Extension), trees) => p -> trees } 413 | val withCopiedFields: Term = caseClassCopy(owner, mod, objTerm, fields) 414 | accumulateToCopy(owner, mod, withCopiedFields, funs) 415 | 416 | /** For FunctionDelegate(method, givn, T, args) 417 | * 418 | * Generates: `givn.method[T](obj, x => mapToCopy(...), ...args)` 419 | */ 420 | case (f: PathSymbol.FunctionDelegate, actions: Seq[PathTree]) :: tail => 421 | val term = actions.foldLeft(objTerm) { (term, tree) => 422 | applyFunctionDelegate(owner, mod, term, f, tree) 423 | } 424 | accumulateToCopy(owner, mod, term, tail) 425 | } 426 | 427 | def mapToCopy(owner: Symbol, mod: Expr[A => A], objTerm: Term, pathTree: PathTree): Term = pathTree match { 428 | case PathTree.Empty => 429 | val apply = termMethodByNameUnsafe(mod.asTerm, "apply") 430 | Apply(Select(mod.asTerm, apply), List(objTerm)) 431 | case PathTree.Node(children) => 432 | accumulateToCopy(owner, mod, objTerm, children) 433 | } 434 | 435 | def extractFocus(tree: Tree): Tree = tree match { 436 | /** Single inlined path */ 437 | case Inlined(_, _, p) => 438 | extractFocus(p) 439 | /** One of paths from modifyAll */ 440 | case Block(List(DefDef(_, _, _, Some(p))), _) => 441 | p 442 | case _ => 443 | report.errorAndAbort(unsupportedShapeInfo(tree)) 444 | } 445 | 446 | val focusesTrees: Seq[Tree] = focuses.map(_.asTerm) 447 | val paths: Seq[Seq[PathSymbol]] = 448 | focusesTrees.zip(focuses).map { (tree, focus) => 449 | toPath(extractFocus(tree), focus) 450 | } 451 | 452 | val pathTree: PathTree = 453 | paths.foldLeft(PathTree.empty) { (tree, path) => tree <> path } 454 | 455 | val res: (Expr[A => A] => Expr[S]) = (mod: Expr[A => A]) => 456 | Typed(mapToCopy(Symbol.spliceOwner, mod, obj.asTerm, pathTree), TypeTree.of[S]).asExpr.asInstanceOf[Expr[S]] 457 | 458 | to(res) 459 | } 460 | } 461 | -------------------------------------------------------------------------------- /quicklens/src/main/scala-3/com/softwaremill/quicklens/package.scala: -------------------------------------------------------------------------------- 1 | package com.softwaremill 2 | 3 | import scala.collection.SortedMap 4 | import scala.annotation.compileTimeOnly 5 | import com.softwaremill.quicklens.QuicklensMacros._ 6 | import scala.reflect.ClassTag 7 | 8 | package object quicklens { 9 | 10 | // #114: obj shouldn't be inline since we want to reference the parameter by-name, rather then embedding the whole 11 | // expression whenever obj is used; this is especially important for chained .modify invocations 12 | extension [S](obj: S) 13 | /** Create an object allowing modifying the given (deeply nested) field accessible in a `case class` hierarchy via 14 | * `path` on the given `obj`. 15 | * 16 | * All modifications are side-effect free and create copies of the original objects. 17 | * 18 | * You can use `.each` to traverse options, lists, etc. 19 | */ 20 | inline def modify[A](inline path: S => A): PathModify[S, A] = ${ toPathModifyFromFocus('obj, 'path) } 21 | 22 | /** Create an object allowing modifying the given (deeply nested) fields accessible in a `case class` hierarchy via 23 | * `paths` on the given `obj`. 24 | * 25 | * All modifications are side-effect free and create copies of the original objects. 26 | * 27 | * You can use `.each` to traverse options, lists, etc. 28 | */ 29 | inline def modifyAll[A](inline path: S => A, inline paths: (S => A)*): PathModify[S, A] = ${ 30 | modifyAllImpl('obj, 'path, 'paths) 31 | } 32 | 33 | case class PathModify[S, A](obj: S, f: (A => A) => S) { 34 | 35 | /** Transform the value of the field(s) using the given function. 36 | * 37 | * @return 38 | * A copy of the root object with the (deeply nested) field(s) modified. 39 | */ 40 | def using(mod: A => A): S = f.apply(mod) 41 | 42 | /** An alias for [[using]]. Explicit calls to [[using]] are preferred over this alias, but quicklens provides this 43 | * option because code auto-formatters (like scalafmt) will generally not keep [[modify]]/[[using]] pairs on the 44 | * same line, leading to code like 45 | * {{{ 46 | * x 47 | * .modify(_.foo) 48 | * .using(newFoo :: _) 49 | * .modify(_.bar) 50 | * .using(_ + newBar) 51 | * }}} 52 | * When using [[apply]], scalafmt will allow 53 | * {{{ 54 | * x 55 | * .modify(_.foo)(newFoo :: _) 56 | * .modify(_.bar)(_ + newBar) 57 | * }}} 58 | */ 59 | def apply(mod: A => A): S = using(mod) 60 | 61 | /** Transform the value of the field(s) using the given function, if the condition is true. Otherwise, returns the 62 | * original object unchanged. 63 | * 64 | * @return 65 | * A copy of the root object with the (deeply nested) field(s) modified, if `condition` is true. 66 | */ 67 | def usingIf(condition: Boolean)(mod: A => A): S = if condition then using(mod) else obj 68 | 69 | /** Set the value of the field(s) to a new value. 70 | * 71 | * @return 72 | * A copy of the root object with the (deeply nested) field(s) set to the new value. 73 | */ 74 | def setTo(v: A): S = f.apply(Function.const(v)) 75 | 76 | /** Set the value of the field(s) to a new value, if it is defined. Otherwise, returns the original object 77 | * unchanged. 78 | * 79 | * @return 80 | * A copy of the root object with the (deeply nested) field(s) set to the new value, if it is defined. 81 | */ 82 | def setToIfDefined(v: Option[A]): S = v.fold(obj)(setTo) 83 | 84 | /** Set the value of the field(s) to a new value, if the condition is true. Otherwise, returns the original object 85 | * unchanged. 86 | * 87 | * @return 88 | * A copy of the root object with the (deeply nested) field(s) set to the new value, if `condition` is true. 89 | */ 90 | def setToIf(condition: Boolean)(v: A): S = if condition then setTo(v) else obj 91 | } 92 | 93 | def modifyLens[T]: LensHelper[T] = LensHelper[T]() 94 | def modifyAllLens[T]: MultiLensHelper[T] = MultiLensHelper[T]() 95 | 96 | case class LensHelper[T] private[quicklens] () { 97 | inline def apply[U](inline path: T => U): PathLazyModify[T, U] = ${ modifyLensApplyImpl('path) } 98 | } 99 | 100 | case class MultiLensHelper[T] private[quicklens] () { 101 | inline def apply[U](inline path1: T => U, inline paths: (T => U)*): PathLazyModify[T, U] = ${ 102 | modifyAllLensApplyImpl('path1, 'paths) 103 | } 104 | } 105 | 106 | case class PathLazyModify[T, U](doModify: (T, U => U) => T) { self => 107 | 108 | /** see [[PathModify.using]] */ 109 | def using(mod: U => U): T => T = obj => doModify(obj, mod) 110 | 111 | /** see [[PathModify.usingIf]] */ 112 | def usingIf(condition: Boolean)(mod: U => U): T => T = 113 | obj => 114 | if (condition) doModify(obj, mod) 115 | else obj 116 | 117 | /** see [[PathModify.setTo]] */ 118 | def setTo(v: U): T => T = obj => doModify(obj, _ => v) 119 | 120 | /** see [[PathModify.setToIfDefined]] */ 121 | def setToIfDefined(v: Option[U]): T => T = v.fold((obj: T) => obj)(setTo) 122 | 123 | /** see [[PathModify.setToIf]] */ 124 | def setToIf(condition: Boolean)(v: => U): T => T = 125 | if (condition) setTo(v) 126 | else obj => obj 127 | 128 | def andThenModify[V](f2: PathLazyModify[U, V]): PathLazyModify[T, V] = 129 | PathLazyModify[T, V]((t, vv) => self.doModify(t, u => f2.doModify(u, vv))) 130 | } 131 | 132 | trait QuicklensFunctor[F[_]] { 133 | def map[A](fa: F[A], f: A => A): F[A] 134 | def each[A](fa: F[A], f: A => A): F[A] = map(fa, f) 135 | def eachWhere[A](fa: F[A], f: A => A, cond: A => Boolean): F[A] = map(fa, x => if cond(x) then f(x) else x) 136 | } 137 | 138 | object QuicklensFunctor { 139 | given [S <: ([V] =>> Seq[V])]: QuicklensFunctor[S] with { 140 | def map[A](fa: S[A], f: A => A): S[A] = fa.map(f).asInstanceOf[S[A]] 141 | } 142 | 143 | given QuicklensFunctor[Option] with { 144 | def map[A](fa: Option[A], f: A => A): Option[A] = fa.map(f) 145 | } 146 | 147 | given QuicklensFunctor[Array] with { 148 | def map[A](fa: Array[A], f: A => A): Array[A] = 149 | implicit val aClassTag: ClassTag[A] = fa.elemTag.asInstanceOf[ClassTag[A]] 150 | fa.map(f) 151 | } 152 | 153 | given [K, M <: ([V] =>> Map[K, V])]: QuicklensFunctor[M] with { 154 | def map[A](fa: M[A], f: A => A): M[A] = { 155 | val mapped = fa.view.mapValues(f) 156 | (fa match { 157 | case sfa: SortedMap[K, A]@unchecked => sfa.sortedMapFactory.from(mapped)(using sfa.ordering) 158 | case _ => mapped.to(fa.mapFactory) 159 | }).asInstanceOf[M[A]] 160 | } 161 | } 162 | } 163 | 164 | trait QuicklensIndexedFunctor[F[_], -I] { 165 | def at[A](fa: F[A], f: A => A, idx: I): F[A] 166 | def atOrElse[A](fa: F[A], f: A => A, idx: I, default: => A): F[A] 167 | def index[A](fa: F[A], f: A => A, idx: I): F[A] 168 | } 169 | 170 | object QuicklensIndexedFunctor { 171 | given [S <: ([V] =>> Seq[V])]: QuicklensIndexedFunctor[S, Int] with { 172 | def at[A](fa: S[A], f: A => A, idx: Int): S[A] = 173 | fa.updated(idx, f(fa(idx))).asInstanceOf[S[A]] 174 | def atOrElse[A](fa: S[A], f: A => A, idx: Int, default: => A): S[A] = 175 | fa.updated(idx, f(fa.applyOrElse(idx, Function.const(default)))).asInstanceOf[S[A]] 176 | def index[A](fa: S[A], f: A => A, idx: Int): S[A] = 177 | if fa.isDefinedAt(idx) then fa.updated(idx, f(fa(idx))).asInstanceOf[S[A]] else fa 178 | } 179 | 180 | given QuicklensIndexedFunctor[Array, Int] with { 181 | def at[A](fa: Array[A], f: A => A, idx: Int): Array[A] = 182 | implicit val aClassTag: ClassTag[A] = fa.elemTag.asInstanceOf[ClassTag[A]] 183 | fa.updated(idx, f(fa(idx))) 184 | def atOrElse[A](fa: Array[A], f: A => A, idx: Int, default: => A): Array[A] = 185 | implicit val aClassTag: ClassTag[A] = fa.elemTag.asInstanceOf[ClassTag[A]] 186 | fa.updated(idx, f(fa.applyOrElse(idx, Function.const(default)))) 187 | def index[A](fa: Array[A], f: A => A, idx: Int): Array[A] = 188 | implicit val aClassTag: ClassTag[A] = fa.elemTag.asInstanceOf[ClassTag[A]] 189 | if fa.isDefinedAt(idx) then fa.updated(idx, f(fa(idx))) else fa 190 | } 191 | 192 | given [K, M <: ([V] =>> Map[K, V])]: QuicklensIndexedFunctor[M, K] with { 193 | def at[A](fa: M[A], f: A => A, idx: K): M[A] = 194 | fa.updated(idx, f(fa(idx))).asInstanceOf[M[A]] 195 | def atOrElse[A](fa: M[A], f: A => A, idx: K, default: => A): M[A] = 196 | fa.updated(idx, f(fa.applyOrElse(idx, _ => default))).asInstanceOf[M[A]] 197 | def index[A](fa: M[A], f: A => A, idx: K): M[A] = 198 | if fa.isDefinedAt(idx) then fa.updated(idx, f(fa(idx))).asInstanceOf[M[A]] else fa 199 | } 200 | } 201 | 202 | trait QuicklensEitherFunctor[T[_, _], L, R]: 203 | def eachLeft[A](e: T[A, R], f: A => A): T[A, R] 204 | def eachRight[A](e: T[L, A], f: A => A): T[L, A] 205 | 206 | object QuicklensEitherFunctor: 207 | given [L, R]: QuicklensEitherFunctor[Either, L, R] with 208 | override def eachLeft[A](e: Either[A, R], f: A => A) = e.left.map(f) 209 | override def eachRight[A](e: Either[L, A], f: A => A) = e.map(f) 210 | 211 | // Currently only used for [[Option]], but could be used for [[Right]]-biased [[Either]]s. 212 | trait QuicklensSingleAtFunctor[F[_]]: 213 | def at[A](fa: F[A], f: A => A): F[A] 214 | def atOrElse[A](fa: F[A], f: A => A, default: => A): F[A] 215 | def index[A](fa: F[A], f: A => A): F[A] 216 | 217 | object QuicklensSingleAtFunctor: 218 | given QuicklensSingleAtFunctor[Option] with 219 | override def at[A](fa: Option[A], f: A => A): Option[A] = Some(fa.map(f).get) 220 | override def atOrElse[A](fa: Option[A], f: A => A, default: => A): Option[A] = fa.orElse(Some(default)).map(f) 221 | override def index[A](fa: Option[A], f: A => A): Option[A] = fa.map(f) 222 | 223 | trait QuicklensWhen[A]: 224 | inline def when[B <: A](a: A, f: B => B): A 225 | 226 | object QuicklensWhen: 227 | given [A]: QuicklensWhen[A] with 228 | override inline def when[B <: A](a: A, f: B => B): A = 229 | a match 230 | case b: B => f(b) 231 | case _ => a 232 | 233 | extension [F[_]: QuicklensFunctor, A](fa: F[A]) 234 | @compileTimeOnly(canOnlyBeUsedInsideModify("each")) 235 | def each: A = ??? 236 | 237 | @compileTimeOnly(canOnlyBeUsedInsideModify("eachWhere")) 238 | def eachWhere(cond: A => Boolean): A = ??? 239 | 240 | extension [F[_]: ([G[_]] =>> QuicklensIndexedFunctor[G, I]), I, A](fa: F[A]) 241 | @compileTimeOnly(canOnlyBeUsedInsideModify("at")) 242 | def at(idx: I): A = ??? 243 | 244 | @compileTimeOnly(canOnlyBeUsedInsideModify("atOrElse")) 245 | def atOrElse(idx: I, default: => A): A = ??? 246 | 247 | @compileTimeOnly(canOnlyBeUsedInsideModify("index")) 248 | def index(idx: I): A = ??? 249 | 250 | extension [T[_, _]: ([E[_, _]] =>> QuicklensEitherFunctor[E, L, R]), R, L](e: T[L, R]) 251 | @compileTimeOnly(canOnlyBeUsedInsideModify("eachLeft")) 252 | def eachLeft: L = ??? 253 | 254 | extension [T[_, _]: ([E[_, _]] =>> QuicklensEitherFunctor[E, L, R]), L, R](e: T[L, R]) 255 | @compileTimeOnly(canOnlyBeUsedInsideModify("eachRight")) 256 | def eachRight: R = ??? 257 | 258 | extension [F[_]: QuicklensSingleAtFunctor, T](t: F[T]) 259 | @compileTimeOnly(canOnlyBeUsedInsideModify("at")) 260 | def at: T = ??? 261 | 262 | @compileTimeOnly(canOnlyBeUsedInsideModify("atOrElse")) 263 | def atOrElse(default: => T): T = ??? 264 | 265 | @compileTimeOnly(canOnlyBeUsedInsideModify("index")) 266 | def index: T = ??? 267 | 268 | extension [A: QuicklensWhen](value: A) 269 | @compileTimeOnly(canOnlyBeUsedInsideModify("when")) 270 | def when[B <: A]: B = ??? 271 | 272 | extension [T, U](f1: T => PathModify[T, U]) 273 | def andThenModify[V](f2: U => PathModify[U, V]): T => PathModify[T, V] = (t: T) => 274 | PathModify[T, V](t, vv => f1(t).f(u => f2(u).f(vv))) 275 | 276 | private def canOnlyBeUsedInsideModify(method: String) = s"$method can only be used as a path component inside modify" 277 | } 278 | -------------------------------------------------------------------------------- /quicklens/src/test/scala-2/com.softwaremill.quicklens/QuicklensMapAtFunctorTest.scala: -------------------------------------------------------------------------------- 1 | package com.softwaremill.quicklens 2 | 3 | import com.softwaremill.quicklens.TestData._ 4 | import org.scalatest.flatspec.AnyFlatSpec 5 | import org.scalatest.matchers.should.Matchers 6 | 7 | class QuicklensMapAtFunctorTest extends AnyFlatSpec with Matchers { 8 | 9 | "QuicklensMapAtFunctor" should "work for types which is not a subtype of Map" in { 10 | case class MapLike[K, V](underlying: Map[K, V]) 11 | 12 | implicit def instance[K, T]: QuicklensMapAtFunctor[MapLike, K, T] = new QuicklensMapAtFunctor[MapLike, K, T] { 13 | private val mapInstance: QuicklensMapAtFunctor[Map, K, T] = 14 | implicitly[QuicklensMapAtFunctor[Map, K, T]] 15 | 16 | def at(fa: MapLike[K, T], idx: K)(f: T => T): MapLike[K, T] = 17 | MapLike(mapInstance.at(fa.underlying, idx)(f)) 18 | def atOrElse(fa: MapLike[K, T], idx: K, default: => T)(f: T => T): MapLike[K, T] = 19 | MapLike(mapInstance.atOrElse(fa.underlying, idx, default)(f)) 20 | def index(fa: MapLike[K, T], idx: K)(f: T => T): MapLike[K, T] = 21 | MapLike(mapInstance.index(fa.underlying, idx)(f)) 22 | def each(fa: MapLike[K, T])(f: T => T): MapLike[K, T] = 23 | MapLike(mapInstance.each(fa.underlying)(f)) 24 | } 25 | 26 | modify(MapLike(m1))(_.at("K1").a5.name).using(duplicate) should be(MapLike(m1dup)) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /quicklens/src/test/scala-3/com/softwaremill/quicklens/test/CompileTimeTest.scala: -------------------------------------------------------------------------------- 1 | package com.softwaremill.quicklens.test 2 | 3 | import org.scalatest.flatspec.AnyFlatSpec 4 | import org.scalatest.matchers.should.Matchers 5 | 6 | class CompileTimeTest extends AnyFlatSpec with Matchers { 7 | // #114 8 | it should "not compile for too long in case of chained modify invocations" in { 9 | val start = System.currentTimeMillis() 10 | assertDoesNotCompile(""" 11 | case class B(a1: Double, a2: Double, a3: Double, a4: Double, a5: Double) 12 | case class C(b: B) 13 | 14 | import com.softwaremill.quicklens.* 15 | 16 | val c = C(B(1, 1, 1, 1, 1)) 17 | c 18 | .modify(_.b.a1).setTo("") 19 | .modify(_.b.a2).setTo("") 20 | .modify(_.b.a3).setTo("") 21 | .modify(_.b.a4).setTo("") 22 | .modify(_.b.a5).setTo("") 23 | """) 24 | val end = System.currentTimeMillis() 25 | (end - start) shouldBe <=(5000L) // that's a lot anyway 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /quicklens/src/test/scala-3/com/softwaremill/quicklens/test/CustomModifyProxyTest.scala: -------------------------------------------------------------------------------- 1 | package com.softwaremill.quicklens.test 2 | 3 | import org.scalatest.flatspec.AnyFlatSpec 4 | import org.scalatest.matchers.should.Matchers 5 | import com.softwaremill.quicklens.* 6 | 7 | class CustomModifyProxyTest extends AnyFlatSpec with Matchers { 8 | 9 | it should "correctly modify a class using a custom modify proxy method" in { 10 | case class State(foo: Int) 11 | 12 | inline def set[A](state: State, inline path: State => A, value: A): State = { 13 | modify(state)(path).setTo(value) 14 | } 15 | 16 | val state = State(100) 17 | val res = set(state, _.foo, 200) 18 | val expected = State(200) 19 | res.shouldBe(expected) 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /quicklens/src/test/scala-3/com/softwaremill/quicklens/test/ExplicitCopyTest.scala: -------------------------------------------------------------------------------- 1 | package com.softwaremill.quicklens 2 | package test 3 | 4 | import org.scalatest.flatspec.AnyFlatSpec 5 | import org.scalatest.matchers.should.Matchers 6 | 7 | class ExplicitCopyTest extends AnyFlatSpec with Matchers { 8 | it should "modify a class with an explicit copy method" in { 9 | case class V(x: Double, y: Double) 10 | class Vec(val v: V) { 11 | def x: Double = v.x 12 | def y: Double = v.y 13 | def copy(x: Double = v.x, y: Double = v.y): Vec = new Vec(V(x, y)) {} 14 | def show: String = s"Vec(${v.x}, ${v.y})" 15 | } 16 | object Vec { 17 | def apply(x: Double, y: Double): Vec = new Vec(V(x, y)) {} 18 | } 19 | 20 | val vec = Vec(1, 2) 21 | val modified = vec.modify(_.x).using(_ + 1) 22 | val expected = Vec(2, 2) 23 | modified.show shouldEqual expected.show 24 | } 25 | 26 | it should "modify a class that has a method with the same name as a field" in { 27 | final case class PathItem() 28 | final case class Paths( 29 | pathItems: Map[String, PathItem] = Map.empty 30 | ) 31 | final case class Docs( 32 | paths: Paths = Paths() 33 | ) { 34 | def paths(paths: Paths): Docs = copy(paths = paths) 35 | } 36 | val docs = Docs() 37 | val r = docs.modify(_.paths.pathItems).using(m => m + ("a" -> PathItem())) 38 | r.paths.pathItems should contain ("a" -> PathItem()) 39 | } 40 | 41 | it should "modify a case class with an additional explicit copy" in { 42 | case class Frozen(state: String, ext: Int) { 43 | def copy(stateC: Char): Frozen = Frozen(stateC.toString, ext) 44 | } 45 | 46 | val f = Frozen("A", 0) 47 | val r = f.modify(_.state).setTo("B") 48 | r.state shouldEqual "B" 49 | } 50 | 51 | it should "modify a case class with an ambiguous additional explicit copy" in { 52 | case class Frozen(state: String, ext: Int) { 53 | def copy(state: String): Frozen = Frozen(state, ext) 54 | } 55 | 56 | val f = Frozen("A", 0) 57 | val r = f.modify(_.state).setTo("B") 58 | r.state shouldEqual "B" 59 | } 60 | 61 | it should "modify a class with two explicit copy methods" in { 62 | class Frozen(val state: String, val ext: Int) { 63 | def copy(state: String = state, ext: Int = ext): Frozen = new Frozen(state, ext) 64 | def copy(state: String): Frozen = new Frozen(state, ext) 65 | } 66 | 67 | val f = new Frozen("A", 0) 68 | val r = f.modify(_.state).setTo("B") 69 | r.state shouldEqual "B" 70 | } 71 | 72 | it should "modify a case class with an ambiguous additional explicit copy and pick the synthetic one first" in { 73 | var accessed = 0 74 | case class Frozen(state: String, ext: Int) { 75 | def copy(state: String): Frozen = 76 | accessed += 1 77 | Frozen(state, ext) 78 | } 79 | 80 | val f = Frozen("A", 0) 81 | f.modify(_.state).setTo("B") 82 | accessed shouldEqual 0 83 | } 84 | 85 | it should "not compile when modifying a field which is not present as a copy parameter" in { 86 | """ 87 | case class Content(x: String) 88 | 89 | class A(val c: Content) { 90 | def copy(x: String = c.x): A = new A(Content(x)) 91 | } 92 | 93 | val a = new A(Content("A")) 94 | val am = a.modify(_.c).setTo(Content("B")) 95 | """ shouldNot compile 96 | } 97 | 98 | // TODO: Would be nice to be able to handle this case. Based on the types, it 99 | // is obvious, that the explicit copy should be picked, but I'm not sure if we 100 | // can get that information 101 | 102 | // it should "pick the correct copy method, based on the type" in { 103 | // case class Frozen(state: String, ext: Int) { 104 | // def copy(state: Char): Frozen = 105 | // Frozen(state.toString, ext) 106 | // } 107 | 108 | // val f = Frozen("A", 0) 109 | // f.modify(_.state).setTo('B') 110 | // } 111 | } 112 | -------------------------------------------------------------------------------- /quicklens/src/test/scala-3/com/softwaremill/quicklens/test/ExtensionCopyTest.scala: -------------------------------------------------------------------------------- 1 | package com.softwaremill.quicklens 2 | package test 3 | 4 | import org.scalatest.flatspec.AnyFlatSpec 5 | import org.scalatest.matchers.should.Matchers 6 | 7 | object ExtensionCopyTest { 8 | case class V(x: Double, y: Double, z: Double) 9 | 10 | opaque type Vec = V 11 | 12 | object Vec { 13 | def apply(x: Double, y: Double): Vec = V(x, y, 0) 14 | 15 | extension (v: Vec) { 16 | def x: Double = v.x 17 | def y: Double = v.y 18 | def copy(x: Double = v.x, y: Double = v.y): Vec = V(x, y, 0) 19 | } 20 | } 21 | } 22 | 23 | class ExtensionCopyTest extends AnyFlatSpec with Matchers { 24 | it should "modify a simple class with an extension copy method" in { 25 | // this test does compile at the moment, because we search extensions in companions only 26 | /* 27 | class VecSimple(xp: Double, yp: Double) { 28 | val xMember = xp 29 | val yMember = yp 30 | } 31 | 32 | object VecSimple { 33 | def apply(x: Double, y: Double): VecSimple = new VecSimple(x, y) 34 | } 35 | 36 | extension (v: VecSimple) { 37 | def copy(x: Double = v.xMember, y: Double = v.yMember): VecSimple = new VecSimple(x, y) 38 | } 39 | val a = VecSimple(1, 2) 40 | val b = a.modify(_.xMember).using(_ + 10) 41 | b.xMember shouldEqual 11 42 | */ 43 | } 44 | 45 | it should "modify a simple class with an extension copy method in companion" in { 46 | class VecCompanion(xp: Double, yp: Double) { 47 | val x = xp 48 | val y = yp 49 | } 50 | 51 | object VecCompanion { 52 | def apply(x: Double, y: Double): VecCompanion = new VecCompanion(x, y) 53 | extension (v: VecCompanion) { 54 | def copy(x: Double = v.x, y: Double = v.y): VecCompanion = new VecCompanion(x, y) 55 | } 56 | } 57 | 58 | val a = VecCompanion(1, 2) 59 | val b = a.modify(_.x).using(_ + 10) 60 | b.x shouldEqual 11 61 | } 62 | 63 | it should "modify a class with extension methods in companion" in { 64 | case class V(xm: Double, ym: Double) 65 | 66 | class VecClass(val v: V) 67 | 68 | object VecClass { 69 | def apply(x: Double, y: Double): VecClass = new VecClass(V(x, y)) 70 | 71 | extension (v: VecClass) { 72 | def x: Double = v.v.xm 73 | def y: Double = v.v.ym 74 | def copy(x: Double = v.x, y: Double = v.y): VecClass = new VecClass(V(x, y)) 75 | } 76 | } 77 | 78 | val a = VecClass(1, 2) 79 | val b = a.modify(_.x).using(_ + 10) 80 | b.x shouldEqual 11 81 | } 82 | 83 | it should "modify an opaque type with extension methods" in { 84 | import ExtensionCopyTest.* 85 | 86 | val a = Vec(1, 2) 87 | val b = a.modify(_.x).using(_ + 10) 88 | b.x shouldEqual 11 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /quicklens/src/test/scala-3/com/softwaremill/quicklens/test/LiteralTypeTest.scala: -------------------------------------------------------------------------------- 1 | package com.softwaremill.quicklens.test 2 | 3 | import com.softwaremill.quicklens.TestData.duplicate 4 | import org.scalatest.flatspec.AnyFlatSpec 5 | import org.scalatest.matchers.should.Matchers 6 | 7 | import com.softwaremill.quicklens._ 8 | 9 | object LiteralTypeTestData { 10 | case class Test(f: "foo") 11 | case class Test1[A](f: A) 12 | } 13 | 14 | class LiteralTypeTest extends AnyFlatSpec with Matchers { 15 | import LiteralTypeTestData._ 16 | 17 | it should "modify a literal type field with an explicit parameter" in { 18 | Test("foo").modify["foo"](_.f).setTo("foo") should be(Test("foo")) 19 | } 20 | 21 | it should "modify a literal type field as a type parameter with an explicit parameter" in { 22 | Test1["foo"]("foo").modify["foo"](_.f).setTo("foo") should be(Test1("foo")) 23 | } 24 | 25 | it should "not compile for a wrong literal type" in { 26 | assertDoesNotCompile(""" 27 | import com.softwaremill.quicklens.* 28 | 29 | case class Test1[A](f: A) 30 | 31 | Test1["foo"]("foo").modify["foo"](_.f).setTo("bar") 32 | """) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /quicklens/src/test/scala-3/com/softwaremill/quicklens/test/ModifyAllOptimizedTest.scala: -------------------------------------------------------------------------------- 1 | package com.softwaremill.quicklens 2 | 3 | import org.scalatest.flatspec.AnyFlatSpec 4 | import org.scalatest.BeforeAndAfterEach 5 | import org.scalatest.matchers.should.Matchers 6 | import scala.reflect.ClassTag 7 | 8 | class ModifyAllOptimizedTest extends AnyFlatSpec with Matchers with BeforeAndAfterEach { 9 | import ModifyAllOptimizedTest._ 10 | 11 | override def beforeEach() = { 12 | Cons.copyCount = 0 13 | ConsOpt.copyCount = 0 14 | Opt.eachCount = 0 15 | } 16 | 17 | it should "Have a correct number of copy calls with single focus" in { 18 | val lst: Cons = Cons.fromList(List(1, 2, 3, 4, 5, 6)).get 19 | 20 | lst 21 | .modifyAll( 22 | _.head 23 | ) 24 | .using(_ + 1) 25 | 26 | Cons.copyCount should be(1) 27 | } 28 | 29 | it should "optimize number of copy calls 1" in { 30 | val lst: Cons = Cons.fromList(List(1, 2, 3, 4, 5, 6)).get 31 | 32 | lst 33 | .modifyAll( 34 | _.head, 35 | _.tail.each.head 36 | ) 37 | .using(_ + 1) 38 | 39 | Cons.copyCount should be(2) 40 | } 41 | 42 | it should "optimize number of copy calls 2" in { 43 | val lst: Cons = Cons.fromList(List(1, 2, 3, 4, 5, 6)).get 44 | 45 | lst 46 | .modifyAll( 47 | _.head, 48 | _.tail.each.head, 49 | _.tail.each.tail.each.head 50 | ) 51 | .using(_ + 1) 52 | 53 | Cons.copyCount should be(3) 54 | } 55 | 56 | it should "optimize number of copy calls 3" in { 57 | val lst: Cons = Cons.fromList(List(1, 2, 3, 4, 5, 6)).get 58 | 59 | lst 60 | .modifyAll( 61 | _.tail.each.tail.each.head, 62 | _.tail.each.tail.each.tail.each.head 63 | ) 64 | .using(_ + 1) 65 | 66 | Cons.copyCount should be(4) 67 | } 68 | 69 | it should "optimize number of copy calls 4" in { 70 | val lst: Cons = Cons.fromList(List(1, 2, 3, 4, 5, 6)).get 71 | 72 | lst 73 | .modifyAll( 74 | _.tail, 75 | _.tail.each.tail, 76 | _.tail, 77 | _.tail.each.tail 78 | ) 79 | .using { 80 | case None => None 81 | case Some(Cons(head, tail)) => Some(Cons(head + 1, tail)) 82 | } 83 | 84 | Cons.copyCount should be(3) 85 | } 86 | 87 | it should "optimize number of copy calls 5" in { 88 | val lst: Cons = Cons.fromList(List(1, 2, 3, 4, 5, 6)).get 89 | 90 | lst 91 | .modifyAll( 92 | _.tail, 93 | _.tail.each.tail, 94 | _.tail.each.tail, 95 | _.tail 96 | ) 97 | .using { 98 | case None => None 99 | case Some(Cons(head, tail)) => Some(Cons(head + 1, tail)) 100 | } 101 | 102 | Cons.copyCount should be(2) 103 | } 104 | 105 | it should "optimize number of copy calls 6" in { 106 | val lst: Cons = Cons.fromList(List(1, 2, 3, 4, 5, 6)).get 107 | 108 | lst 109 | .modifyAll( 110 | _.tail, 111 | _.tail.each.tail.each.tail, 112 | _.tail.each.tail, 113 | _.tail.each.tail.each.tail.each.tail, 114 | _.tail.each.tail, 115 | _.tail 116 | ) 117 | .using { 118 | case None => None 119 | case Some(Cons(head, tail)) => Some(Cons(head + 1, tail)) 120 | } 121 | 122 | Cons.copyCount should be(5) 123 | } 124 | 125 | it should "optimize number of each function delegates 1" in { 126 | val lst: ConsOpt = ConsOpt.fromList(List(1, 2, 3)).get 127 | 128 | lst 129 | .modifyAll( 130 | _.tail.each.head, 131 | _.tail.each.tail.each.head 132 | ) 133 | .using(_ + 1) 134 | 135 | Opt.eachCount should be(2) 136 | } 137 | 138 | it should "optimize number of each function delegates 2" in { 139 | val lst: ConsOpt = ConsOpt.fromList(List(1, 2, 3, 4, 5, 6)).get 140 | 141 | lst 142 | .modifyAll( 143 | _.tail.each.head, 144 | _.tail.each.tail.each.head 145 | ) 146 | .using(_ + 1) 147 | 148 | Opt.eachCount should be(2) 149 | } 150 | 151 | it should "optimize number of each function delegates 3" in { 152 | val lst: ConsOpt = ConsOpt.fromList(List(1, 2, 3, 4, 5, 6)).get 153 | 154 | lst 155 | .modifyAll( 156 | _.tail.each.tail.each.head, 157 | _.tail.each.tail.each.tail.each.head 158 | ) 159 | .using(_ + 1) 160 | 161 | Opt.eachCount should be(3) 162 | } 163 | } 164 | 165 | object ModifyAllOptimizedTest { 166 | 167 | case class Cons(head: Int, tail: Option[Cons]) { 168 | def copy(head: Int = head, tail: Option[Cons] = tail): Cons = { 169 | Cons.copyCount = Cons.copyCount + 1 170 | Cons(head, tail) 171 | } 172 | } 173 | object Cons { 174 | var copyCount = 0 175 | def fromList(list: List[Int]): Option[Cons] = list match { 176 | case Nil => None 177 | case head :: tail => 178 | Some(Cons(head, fromList(tail))) 179 | } 180 | } 181 | 182 | sealed trait Opt[+A] { 183 | def get: A 184 | } 185 | object Opt { 186 | var eachCount = 0 187 | } 188 | case object Nada extends Opt[Nothing] { 189 | override def get: Nothing = ??? 190 | } 191 | case class Just[+A](val a: A) extends Opt[A] { 192 | override def get: A = a 193 | override def equals(other: Any): Boolean = other match { 194 | case Just(a1) => a == a1 195 | case _ => false 196 | } 197 | } 198 | 199 | given QuicklensFunctor[Opt] with { 200 | def map[A](fa: Opt[A], f: A => A): Opt[A] = 201 | Opt.eachCount = Opt.eachCount + 1 202 | fa match { 203 | case Nada => Nada 204 | case Just(a) => Just(f(a)) 205 | } 206 | } 207 | 208 | case class ConsOpt(head: Int, tail: Opt[ConsOpt]) { 209 | def copy(head: Int = head, tail: Opt[ConsOpt] = tail): ConsOpt = { 210 | ConsOpt.copyCount = ConsOpt.copyCount + 1 211 | ConsOpt(head, tail) 212 | } 213 | } 214 | object ConsOpt { 215 | var copyCount = 0 216 | def fromList(list: List[Int]): Opt[ConsOpt] = list match { 217 | case Nil => Nada 218 | case head :: tail => 219 | Just(ConsOpt(head, fromList(tail))) 220 | } 221 | } 222 | } 223 | -------------------------------------------------------------------------------- /quicklens/src/test/scala-3/com/softwaremill/quicklens/test/ModifyAllOrderTest.scala: -------------------------------------------------------------------------------- 1 | package com.softwaremill.quicklens 2 | 3 | import org.scalatest.flatspec.AnyFlatSpec 4 | import org.scalatest.matchers.should.Matchers 5 | 6 | class ModifyAllOrderTest extends AnyFlatSpec with Matchers { 7 | import ModifyAllOrderTest._ 8 | 9 | it should "apply modifications in the correct order if child is first" in { 10 | val lst: Cons = Cons.fromList(List(1, 2, 3)).get 11 | 12 | val expected: Cons = Cons.fromList(List(1, 2, 4)).get 13 | 14 | val res = lst 15 | .modifyAll( 16 | _.tail.each.tail.each, 17 | _.tail.each 18 | ) 19 | .using { 20 | case Cons(head, tail) if head == 3 => 21 | Cons(head + 1, tail) 22 | case Cons(head, Some(Cons(head2, tail))) if head2 == 3 => 23 | Cons(head + 1, Some(Cons(head2, tail))) 24 | case c => c 25 | } 26 | 27 | res should be(expected) 28 | } 29 | 30 | it should "apply modifications in the correct order if parent is first" in { 31 | val lst: Cons = Cons.fromList(List(1, 2, 3)).get 32 | 33 | val expected: Cons = Cons.fromList(List(1, 3, 4)).get 34 | 35 | val res = lst 36 | .modifyAll( 37 | _.tail.each, 38 | _.tail.each.tail.each 39 | ) 40 | .using { 41 | case Cons(head, tail) if head == 3 => 42 | Cons(head + 1, tail) 43 | case Cons(head, Some(Cons(head2, tail))) if head2 == 3 => 44 | Cons(head + 1, Some(Cons(head2, tail))) 45 | case c => c 46 | } 47 | 48 | res should be(expected) 49 | } 50 | 51 | it should "apply modifications in the correct order: child, parent, child" in { 52 | val lst: Cons = Cons.fromList(List(1, 2, 3)).get 53 | 54 | val expected: Cons = Cons.fromList(List(1, 3, 5)).get 55 | 56 | val res = lst 57 | .modifyAll( 58 | _.tail.each.tail.each, 59 | _.tail.each, 60 | _.tail.each.tail.each 61 | ) 62 | .using { 63 | case Cons(head, tail) if head >= 3 => 64 | Cons(head + 1, tail) 65 | case Cons(head, Some(Cons(head2, tail))) if head2 == 4 => 66 | Cons(head + 1, Some(Cons(head2, tail))) 67 | case c => c 68 | } 69 | 70 | res should be(expected) 71 | } 72 | 73 | it should "apply modifications in the correct order: child, child, parent" in { 74 | val lst: Cons = Cons.fromList(List(1, 2, 3)).get 75 | 76 | val expected: Cons = Cons.fromList(List(1, 2, 5)).get 77 | 78 | val res = lst 79 | .modifyAll( 80 | _.tail.each.tail.each, 81 | _.tail.each.tail.each, 82 | _.tail.each 83 | ) 84 | .using { 85 | case Cons(head, tail) if head >= 3 => 86 | Cons(head + 1, tail) 87 | case Cons(head, Some(Cons(head2, tail))) if head2 == 4 => 88 | Cons(head + 1, Some(Cons(head2, tail))) 89 | case c => c 90 | } 91 | 92 | res should be(expected) 93 | } 94 | 95 | it should "apply modifications in the correct order on option: child, child, parent" in { 96 | val lst: Option[Cons] = Cons.fromList(List(1, 2, 3)) 97 | 98 | val expected: Option[Cons] = Cons.fromList(List(1, 2, 5)) 99 | 100 | val res = lst 101 | .modifyAll( 102 | _.each.tail.each.tail.each, 103 | _.each.tail.each.tail.each, 104 | _.each.tail.each 105 | ) 106 | .using { 107 | case Cons(head, tail) if head >= 3 => 108 | Cons(head + 1, tail) 109 | case Cons(head, Some(Cons(head2, tail))) if head2 == 4 => 110 | Cons(head + 1, Some(Cons(head2, tail))) 111 | case c => c 112 | } 113 | 114 | res should be(expected) 115 | } 116 | 117 | it should "apply modifications in the correct order on option: child, parent, child" in { 118 | val lst: Option[Cons] = Cons.fromList(List(1, 2, 3)) 119 | 120 | val expected: Option[Cons] = Cons.fromList(List(1, 3, 5)) 121 | 122 | val res = lst 123 | .modifyAll( 124 | _.each.tail.each.tail.each, 125 | _.each.tail.each, 126 | _.each.tail.each.tail.each 127 | ) 128 | .using { 129 | case Cons(head, tail) if head >= 3 => 130 | Cons(head + 1, tail) 131 | case Cons(head, Some(Cons(head2, tail))) if head2 == 4 => 132 | Cons(head + 1, Some(Cons(head2, tail))) 133 | case c => c 134 | } 135 | 136 | res should be(expected) 137 | } 138 | } 139 | 140 | object ModifyAllOrderTest { 141 | case class Cons(head: Int, tail: Option[Cons]) 142 | 143 | object Cons { 144 | def fromList(list: List[Int]): Option[Cons] = list match { 145 | case Nil => None 146 | case head :: tail => 147 | Some(Cons(head, fromList(tail))) 148 | } 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /quicklens/src/test/scala-3/com/softwaremill/quicklens/test/ModifyAndTypeTest.scala: -------------------------------------------------------------------------------- 1 | package com.softwaremill.quicklens 2 | 3 | import org.scalatest.flatspec.AnyFlatSpec 4 | import org.scalatest.matchers.should.Matchers 5 | 6 | import ModifyAndTypeTest._ 7 | 8 | object ModifyAndTypeTest { 9 | case class A(a: Int) extends B 10 | trait B { 11 | def a: Int 12 | } 13 | 14 | case class A1(a: Int) 15 | 16 | sealed trait T 17 | case class C(a: Int) extends T with B 18 | 19 | sealed trait T1 20 | case class C1(a: Int) extends T1 21 | } 22 | 23 | class ModifyAndTypeTest extends AnyFlatSpec with Matchers { 24 | it should "modify an & type object" in { 25 | val ab: A & B = A(0) 26 | 27 | val modified = ab.modify(_.a).setTo(1) 28 | 29 | modified.a shouldBe 1 30 | } 31 | 32 | it should "modify an & type object 1" in { 33 | val ab: B & A = A(0) 34 | 35 | val modified = ab.modify(_.a).setTo(1) 36 | 37 | modified.a shouldBe 1 38 | } 39 | 40 | it should "modify an & type object 2" in { 41 | val ab: B & A1 = new A1(0) with B 42 | 43 | val modified = ab.modify(_.a).setTo(1) 44 | 45 | modified.a shouldBe 1 46 | } 47 | 48 | it should "modify an & type object 3" in { 49 | val ab: A1 & B = new A1(0) with B 50 | 51 | val modified = ab.modify(_.a).setTo(1) 52 | 53 | modified.a shouldBe 1 54 | } 55 | 56 | // TODO this is an implemenation limitation for now, since anonymous classes crash on runtime 57 | // it should "modify an & type object with a sealed trait" in { 58 | // val tb: T & B = C(0) 59 | 60 | // val modified = tb.modify(_.a).setTo(1) 61 | 62 | // modified.a shouldBe 1 63 | // } 64 | 65 | // it should "modify an & type object with a sealed trait 1" in { 66 | // val tb: B & T = C(0) 67 | 68 | // val modified = tb.modify(_.a).setTo(1) 69 | 70 | // modified.a shouldBe 1 71 | // } 72 | 73 | // it should "modify an & type object with a sealed trait 2" in { 74 | // val tb: B & T1 = new C1(0) with B 75 | 76 | // val modified = tb.modify(_.a).setTo(1) 77 | 78 | // modified.a shouldBe 1 79 | // } 80 | 81 | // it should "modify an & type object with a sealed trait 3" in { 82 | // val tb: T1 & B = new C1(0) with B 83 | 84 | // val modified = tb.modify(_.a).setTo(1) 85 | 86 | // modified.a shouldBe 1 87 | // } 88 | } 89 | -------------------------------------------------------------------------------- /quicklens/src/test/scala-3/com/softwaremill/quicklens/test/ModifyEnumTest.scala: -------------------------------------------------------------------------------- 1 | package com.softwaremill.quicklens.test 2 | 3 | import com.softwaremill.quicklens.TestData.duplicate 4 | import org.scalatest.flatspec.AnyFlatSpec 5 | import org.scalatest.matchers.should.Matchers 6 | 7 | import com.softwaremill.quicklens._ 8 | 9 | object EnumTestData { 10 | enum P3(val a: String): 11 | case C5(override val a: String, b: Int) extends P3(a) 12 | case C6(override val a: String, c: Option[String]) extends P3(a) 13 | 14 | val p3: P3 = P3.C5("c2", 0) 15 | val p3dup: P3 = P3.C5("c2c2", 0) 16 | } 17 | 18 | class ModifyEnumTest extends AnyFlatSpec with Matchers { 19 | import EnumTestData._ 20 | 21 | it should "modify a field in an enum case" in { 22 | modify(p3)(_.a).using(duplicate) should be(p3dup) 23 | } 24 | 25 | it should "modify a field in an enum case with extension method" in { 26 | p3.modify(_.a).using(duplicate) should be(p3dup) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /quicklens/src/test/scala-3/com/softwaremill/quicklens/test/ModitySealedAbstractClass.scala: -------------------------------------------------------------------------------- 1 | package com.softwaremill.quicklens.test 2 | 3 | import com.softwaremill.quicklens.TestData._ 4 | import org.scalatest.flatspec.AnyFlatSpec 5 | import org.scalatest.matchers.should.Matchers 6 | 7 | import com.softwaremill.quicklens._ 8 | 9 | class ModitySealedAbstractClass extends AnyFlatSpec with Matchers { 10 | it should "Modify abstract class hierarchy" in { 11 | invInt.modify(_.typ).setTo(Type("Long")) should be(invLong) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /quicklens/src/test/scala/com/softwaremill/quicklens/EnormousModifyAllTest.scala: -------------------------------------------------------------------------------- 1 | package com.softwaremill.quicklens 2 | 3 | import org.scalatest.flatspec.AnyFlatSpec 4 | import org.scalatest.matchers.should.Matchers 5 | 6 | class EnormousModifyAllTest extends AnyFlatSpec with Matchers { 7 | import EnormousModifyAllTest._ 8 | 9 | it should "expand an enormous function" in { 10 | val c6 = C6(1) 11 | val c5 = C5(c6, c6, c6, c6) 12 | val c4 = C4(c5, c5, c5, c5) 13 | val c3 = C3(c4, c4, c4, c4) 14 | val c2 = C2(c3, c3, c3, c3) 15 | val c1 = C1(c2, c2, c2, c2) 16 | 17 | val c6e = C6(2) 18 | val c5e = C5(c6e, c6, c6, c6) 19 | val c4e = C4(c5e, c5, c5, c5) 20 | val c3e = C3(c4e, c4, c4, c4) 21 | val c2e = C2(c3e, c3, c3, c3) 22 | val c1e = C1(c2e, c2e, c2e, c2e) 23 | 24 | val res = c1 25 | .modifyAll( 26 | _.a.a.a.a.a.a, 27 | _.b.a.a.a.a.a, 28 | _.c.a.a.a.a.a, 29 | _.d.a.a.a.a.a 30 | ) 31 | .using(_ + 1) 32 | res should be(c1e) 33 | } 34 | } 35 | 36 | object EnormousModifyAllTest { 37 | case class C1( 38 | a: C2, 39 | b: C2, 40 | c: C2, 41 | d: C2 42 | ) 43 | 44 | case class C2( 45 | a: C3, 46 | b: C3, 47 | c: C3, 48 | d: C3 49 | ) 50 | 51 | case class C3( 52 | a: C4, 53 | b: C4, 54 | c: C4, 55 | d: C4 56 | ) 57 | 58 | case class C4( 59 | a: C5, 60 | b: C5, 61 | c: C5, 62 | d: C5 63 | ) 64 | 65 | case class C5( 66 | a: C6, 67 | b: C6, 68 | c: C6, 69 | d: C6 70 | ) 71 | 72 | case class C6( 73 | a: Int 74 | ) 75 | } 76 | -------------------------------------------------------------------------------- /quicklens/src/test/scala/com/softwaremill/quicklens/HugeModifyTest.scala: -------------------------------------------------------------------------------- 1 | package com.softwaremill.quicklens 2 | 3 | import org.scalatest.flatspec.AnyFlatSpec 4 | import org.scalatest.matchers.should.Matchers 5 | 6 | class HugeModifyTest extends AnyFlatSpec with Matchers { 7 | import HugeModifyTestData._ 8 | 9 | it should "expand a huge function" in { 10 | val c5 = C5(1) 11 | val c4 = C4(c5, c5, c5, c5) 12 | val c3 = C3(c4, c4, c4, c4) 13 | val c2 = C2(c3, c3, c3, c3) 14 | val c1 = C1(c2, c2, c2, c2) 15 | 16 | val c5e = C5(2) 17 | val c4e = C4(c5e, c5, c5, c5) 18 | val c3e = C3(c4e, c4, c4, c4) 19 | val c2e = C2(c3e, c3, c3, c3) 20 | val c1e = C1(c2e, c2e, c2e, c2) 21 | 22 | val res = c1 23 | .modifyAll( 24 | _.a.a.a.a.a, 25 | _.b.a.a.a.a, 26 | _.c.a.a.a.a 27 | ) 28 | .using(_ + 1) 29 | res should be(c1e) 30 | } 31 | } 32 | 33 | object HugeModifyTestData { 34 | case class C1( 35 | a: C2, 36 | b: C2, 37 | c: C2, 38 | d: C2 39 | ) 40 | 41 | case class C2( 42 | a: C3, 43 | b: C3, 44 | c: C3, 45 | d: C3 46 | ) 47 | 48 | case class C3( 49 | a: C4, 50 | b: C4, 51 | c: C4, 52 | d: C4 53 | ) 54 | 55 | case class C4( 56 | a: C5, 57 | b: C5, 58 | c: C5, 59 | d: C5 60 | ) 61 | 62 | case class C5( 63 | a: Int 64 | ) 65 | } 66 | -------------------------------------------------------------------------------- /quicklens/src/test/scala/com/softwaremill/quicklens/LensLazyTest.scala: -------------------------------------------------------------------------------- 1 | package com.softwaremill.quicklens 2 | 3 | import com.softwaremill.quicklens.TestData._ 4 | import org.scalatest.flatspec.AnyFlatSpec 5 | import org.scalatest.matchers.should.Matchers 6 | 7 | class LensLazyTest extends AnyFlatSpec with Matchers { 8 | it should "create reusable lens of the given type" in { 9 | val lens = modifyLens[A1](_.a2.a3.a4.a5.name) 10 | 11 | val lm = lens.using(duplicate) 12 | lm(a1) should be(a1dup) 13 | } 14 | 15 | it should "compose lens" in { 16 | val lens_a1_a3 = modifyLens[A1](_.a2.a3) 17 | val lens_a3_name = modifyLens[A3](_.a4.a5.name) 18 | 19 | val lm = (lens_a1_a3 andThenModify lens_a3_name).using(duplicate) 20 | lm(a1) should be(a1dup) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /quicklens/src/test/scala/com/softwaremill/quicklens/LensTest.scala: -------------------------------------------------------------------------------- 1 | package com.softwaremill.quicklens 2 | 3 | import com.softwaremill.quicklens.TestData._ 4 | import org.scalatest.flatspec.AnyFlatSpec 5 | import org.scalatest.matchers.should.Matchers 6 | 7 | class LensTest extends AnyFlatSpec with Matchers { 8 | it should "create reusable lens of the given type" in { 9 | val lens = modify(_: A1)(_.a2.a3.a4.a5.name) 10 | 11 | lens(a1).using(duplicate) should be(a1dup) 12 | } 13 | 14 | it should "compose lens" in { 15 | val lens_a1_a3 = modify(_: A1)(_.a2.a3) 16 | val lens_a3_name = modify(_: A3)(_.a4.a5.name) 17 | 18 | (lens_a1_a3 andThenModify lens_a3_name)(a1).using(duplicate) should be(a1dup) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /quicklens/src/test/scala/com/softwaremill/quicklens/ModifyAliasTest.scala: -------------------------------------------------------------------------------- 1 | package com.softwaremill.quicklens 2 | 3 | import org.scalatest.flatspec.AnyFlatSpec 4 | import org.scalatest.matchers.should.Matchers 5 | 6 | import ModifyAliasTest._ 7 | 8 | object ModifyAliasTest { 9 | 10 | case class State(x: Int) 11 | 12 | type S = State 13 | 14 | sealed trait Expr { 15 | def i: Int 16 | } 17 | case class ListInt(i: Int) extends Expr 18 | 19 | type E = Expr 20 | } 21 | 22 | class ModifyAliasTest extends AnyFlatSpec with Matchers { 23 | it should "modify an object declared using type alias" in { 24 | val s: S = State(0) 25 | val modified = s.modify(_.x).setTo(1) 26 | 27 | modified.x shouldBe 1 28 | } 29 | 30 | it should "modify a sealed hierarchy declared using type alias" in { 31 | val s: E = ListInt(0) 32 | val modified = s.modify(_.i).setTo(1) 33 | 34 | modified.i shouldBe 1 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /quicklens/src/test/scala/com/softwaremill/quicklens/ModifyArrayIndexTest.scala: -------------------------------------------------------------------------------- 1 | package com.softwaremill.quicklens 2 | 3 | import com.softwaremill.quicklens.TestData._ 4 | import org.scalatest.flatspec.AnyFlatSpec 5 | import org.scalatest.matchers.should.Matchers 6 | 7 | class ModifyArrayIndexTest extends AnyFlatSpec with Matchers { 8 | 9 | it should "modify a non-nested array with case class item" in { 10 | modify(ar1)(_.index(2).a4.a5.name).using(duplicate) should be(l1at2dup) 11 | modify(ar1)(_.index(2)) 12 | .using(a3 => modify(a3)(_.a4.a5.name).using(duplicate)) should be(l1at2dup) 13 | } 14 | 15 | it should "modify a nested array using index" in { 16 | modify(arar1)(_.index(2).index(1).name).using(duplicate) should be(ll1at2at1dup) 17 | } 18 | 19 | it should "modify a nested array using index and each" in { 20 | modify(arar1)(_.index(2).each.name).using(duplicate) should be(ll1at2eachdup) 21 | modify(arar1)(_.each.index(1).name).using(duplicate) should be(ll1eachat1dup) 22 | } 23 | 24 | it should "not modify if given index does not exist" in { 25 | modify(ar1)(_.index(10).a4.a5.name).using(duplicate) should be(l1) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /quicklens/src/test/scala/com/softwaremill/quicklens/ModifyAtTest.scala: -------------------------------------------------------------------------------- 1 | package com.softwaremill.quicklens 2 | 3 | import com.softwaremill.quicklens.TestData._ 4 | import org.scalatest.flatspec.AnyFlatSpec 5 | import org.scalatest.matchers.should.Matchers 6 | 7 | class ModifyAtTest extends AnyFlatSpec with Matchers { 8 | 9 | it should "modify a non-nested list with case class item" in { 10 | modify(l1)(_.at(2).a4.a5.name).using(duplicate) should be(l1at2dup) 11 | modify(l1)(_.at(2)) 12 | .using(a3 => modify(a3)(_.a4.a5.name).using(duplicate)) should be(l1at2dup) 13 | } 14 | 15 | it should "modify a nested list using at" in { 16 | modify(ll1)(_.at(2).at(1).name).using(duplicate) should be(ll1at2at1dup) 17 | } 18 | 19 | it should "modify a nested list using at and each" in { 20 | modify(ll1)(_.at(2).each.name).using(duplicate) should be(ll1at2eachdup) 21 | modify(ll1)(_.each.at(1).name).using(duplicate) should be(ll1eachat1dup) 22 | } 23 | 24 | it should "modify both lists and options" in { 25 | modify(y1)(_.y2.y3.at(1).y4.each.name).using(duplicate) should be(y1at1dup) 26 | } 27 | 28 | it should "throw an exception if there's no element at the given index" in { 29 | an[IndexOutOfBoundsException] should be thrownBy { 30 | modify(l1)(_.at(10).a4.a5.name).using(duplicate) 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /quicklens/src/test/scala/com/softwaremill/quicklens/ModifyEachTest.scala: -------------------------------------------------------------------------------- 1 | package com.softwaremill.quicklens 2 | 3 | import com.softwaremill.quicklens.TestData._ 4 | import org.scalatest.flatspec.AnyFlatSpec 5 | import org.scalatest.matchers.should.Matchers 6 | 7 | class ModifyEachTest extends AnyFlatSpec with Matchers { 8 | it should "modify a single-nested optional case class field" in { 9 | modify(x4)(_.x5.each.name).using(duplicate) should be(x4dup) 10 | } 11 | 12 | it should "modify a single-nested optional case class field (pimped)" in { 13 | x4.modify(_.x5.each.name).using(duplicate) should be(x4dup) 14 | } 15 | 16 | it should "modify multiple deeply-nested optional case class field" in { 17 | modify(x1)(_.x2.x3.each.x4.x5.each.name).using(duplicate) should be(x1dup) 18 | } 19 | 20 | it should "not modify an optional case class field if it is none" in { 21 | modify(x1none)(_.x2.x3.each.x4.x5.each.name).using(duplicate) should be(x1none) 22 | modify(x4none)(_.x5.each.name).using(duplicate) should be(x4none) 23 | } 24 | 25 | it should "modify both lists and options" in { 26 | modify(y1)(_.y2.y3.each.y4.each.name).using(duplicate) should be(y1dup) 27 | } 28 | 29 | it should "allow .each at the end" in { 30 | modify(z1)(_.name.each).using(duplicate) should be(z1dup) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /quicklens/src/test/scala/com/softwaremill/quicklens/ModifyEachWhereTest.scala: -------------------------------------------------------------------------------- 1 | package com.softwaremill.quicklens 2 | 3 | import com.softwaremill.quicklens.TestData._ 4 | import org.scalatest.flatspec.AnyFlatSpec 5 | import org.scalatest.matchers.should.Matchers 6 | 7 | class ModifyEachWhereTest extends AnyFlatSpec with Matchers { 8 | it should "modify a single-nested optional case class field only if the condition returns true" in { 9 | modify(x4)(_.x5.eachWhere(_ => true).name).using(duplicate) should be(x4dup) 10 | modify(x4)(_.x5.eachWhere(_ => false).name).using(duplicate) should be(x4) 11 | } 12 | 13 | it should "modify a single-nested optional case class field (pimped) only if the condition returns true" in { 14 | x4.modify(_.x5.eachWhere(_ => true).name).using(duplicate) should be(x4dup) 15 | x4.modify(_.x5.eachWhere(_ => false).name).using(duplicate) should be(x4) 16 | } 17 | 18 | it should "modify be able to eachWhere a lambda" in { 19 | val foo = true 20 | x4.modify(_.x5.eachWhere(_ => foo).name).using(duplicate) should be(x4dup) 21 | val bar = false 22 | x1.modify(_.x2.x3.eachWhere(_ => bar).x4.x5.eachWhere(_ => bar).name).using(duplicate) should be(x1) 23 | } 24 | 25 | it should "modify be able to eachWhere a lambda 1" in { 26 | val one = "1" 27 | person 28 | .modify(_.addresses.each.street.eachWhere(_.name.startsWith(one)).name) 29 | .using(_.toUpperCase) 30 | } 31 | 32 | it should "allow referencing local variable" in { 33 | val zmienna = "d" 34 | modify(z1)(_.name.eachWhere(_.startsWith(zmienna))).using(duplicate) should be(z1dup) 35 | } 36 | 37 | it should "allow referencing local variable 1" in { 38 | val lang2 = "hey" 39 | 40 | Seq(Foo("asdf")) 41 | .modify(_.eachWhere(_.field != lang2).field) 42 | .setTo(lang2) should be(Seq(Foo("hey"))) 43 | } 44 | 45 | it should "not modify an optional case class field if it is none regardless of the condition" in { 46 | modify(x4none)(_.x5.eachWhere(_ => true).name).using(duplicate) should be(x4none) 47 | modify(x4none)(_.x5.eachWhere(_ => false).name).using(duplicate) should be(x4none) 48 | } 49 | 50 | it should "modify only those list elements where the condition returns true" in { 51 | modify(y1)(_.y2.y3.eachWhere(_.y4.map(_.name) == Some("d2")).y4.each.name) 52 | .using(duplicate) should be(y1at1dup) 53 | } 54 | 55 | it should "allow .each at then end only if the condition returns true" in { 56 | modify(z1)(_.name.eachWhere(_.startsWith("d"))).using(duplicate) should be(z1dup) 57 | modify(z1)(_.name.eachWhere(_.startsWith("e"))).using(duplicate) should be(z1) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /quicklens/src/test/scala/com/softwaremill/quicklens/ModifyEitherTest.scala: -------------------------------------------------------------------------------- 1 | package com.softwaremill.quicklens 2 | 3 | import org.scalatest.flatspec.AnyFlatSpec 4 | import org.scalatest.matchers.should.Matchers 5 | 6 | case class Named(name: String) 7 | 8 | case class Aged(age: Int) 9 | 10 | case class Eithers(e: Either[Named, Aged]) 11 | 12 | case class MoreEithers(e1: Either[Eithers, MoreEithers], e2: Either[Eithers, MoreEithers]) 13 | 14 | class ModifyEitherTest extends AnyFlatSpec with Matchers { 15 | 16 | it should "modify a single-nested left case class field" in { 17 | modify( 18 | Eithers(Left(Named("boo"))) 19 | )(_.e.eachLeft.name).setTo("moo") should be( 20 | Eithers(Left(Named("moo"))) 21 | ) 22 | } 23 | 24 | it should "modify a single-nested left case class field (pimped)" in { 25 | Eithers(Left(Named("boo"))) 26 | .modify(_.e.eachLeft.name) 27 | .setTo("moo") should be( 28 | Eithers(Left(Named("moo"))) 29 | ) 30 | } 31 | 32 | it should "modify a single-nested right case class field" in { 33 | modify( 34 | Eithers(Right(Aged(23))) 35 | )(_.e.eachRight.age).setTo(32) should be( 36 | Eithers(Right(Aged(32))) 37 | ) 38 | } 39 | 40 | it should "modify a single-nested right case class field (pimped)" in { 41 | Eithers(Right(Aged(23))) 42 | .modify(_.e.eachRight.age) 43 | .setTo(32) should be( 44 | Eithers(Right(Aged(32))) 45 | ) 46 | } 47 | 48 | it should "modify multiple deeply-nested either case class fields" in { 49 | 50 | modify( 51 | MoreEithers( 52 | e1 = Right( 53 | MoreEithers( 54 | e1 = Left(Eithers(Right(Aged(23)))), 55 | e2 = Left(Eithers(Left(Named("boo")))) 56 | ) 57 | ), 58 | e2 = Left(Eithers(Left(Named("boo")))) 59 | ) 60 | )(_.e1.eachRight.e2.eachLeft.e.eachLeft.name) 61 | .using(_.toUpperCase) should be( 62 | MoreEithers( 63 | e1 = Right( 64 | MoreEithers( 65 | e1 = Left(Eithers(Right(Aged(23)))), 66 | e2 = Left(Eithers(Left(Named("BOO")))) 67 | ) 68 | ), 69 | e2 = Left(Eithers(Left(Named("boo")))) 70 | ) 71 | ) 72 | } 73 | 74 | it should "not modify left case class field if it is right" in { 75 | modify( 76 | Eithers(Right(Aged(23))) 77 | )(_.e.eachLeft.name).setTo("moo") should be( 78 | Eithers(Right(Aged(23))) 79 | ) 80 | } 81 | 82 | it should "not modify right case class field if it is left" in { 83 | modify( 84 | Eithers(Left(Named("boo"))) 85 | )(_.e.eachRight.age).setTo(33) should be( 86 | Eithers(Left(Named("boo"))) 87 | ) 88 | } 89 | 90 | it should "allow .eachLeft at then end" in { 91 | modify(Left("boo"): Either[String, Int])(_.eachLeft) 92 | .using(_.toUpperCase) should be(Left("BOO")) 93 | } 94 | 95 | it should "allow .eachRight at then end" in { 96 | modify(Right(23): Either[String, Int])(_.eachRight).using(_ + 3) should be(Right(26)) 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /quicklens/src/test/scala/com/softwaremill/quicklens/ModifyIndexTest.scala: -------------------------------------------------------------------------------- 1 | package com.softwaremill.quicklens 2 | 3 | import com.softwaremill.quicklens.TestData._ 4 | import org.scalatest.flatspec.AnyFlatSpec 5 | import org.scalatest.matchers.should.Matchers 6 | 7 | class ModifyIndexTest extends AnyFlatSpec with Matchers { 8 | 9 | it should "modify a non-nested list with case class item" in { 10 | modify(l1)(_.index(2).a4.a5.name).using(duplicate) should be(l1at2dup) 11 | modify(l1)(_.index(2)) 12 | .using(a3 => modify(a3)(_.a4.a5.name).using(duplicate)) should be(l1at2dup) 13 | } 14 | 15 | it should "modify a nested list using index" in { 16 | modify(ll1)(_.index(2).index(1).name).using(duplicate) should be(ll1at2at1dup) 17 | } 18 | 19 | it should "modify a nested list using index and each" in { 20 | modify(ll1)(_.index(2).each.name).using(duplicate) should be(ll1at2eachdup) 21 | modify(ll1)(_.each.index(1).name).using(duplicate) should be(ll1eachat1dup) 22 | } 23 | 24 | it should "modify both lists and options" in { 25 | modify(y1)(_.y2.y3.index(1).y4.each.name).using(duplicate) should be(y1at1dup) 26 | } 27 | 28 | it should "not modify if given index does not exist" in { 29 | modify(l1)(_.index(10).a4.a5.name).using(duplicate) should be(l1) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /quicklens/src/test/scala/com/softwaremill/quicklens/ModifyIndexedSeqIndexTest.scala: -------------------------------------------------------------------------------- 1 | package com.softwaremill.quicklens 2 | 3 | import com.softwaremill.quicklens.TestData._ 4 | import org.scalatest.flatspec.AnyFlatSpec 5 | import org.scalatest.matchers.should.Matchers 6 | 7 | class ModifyIndexedSeqIndexTest extends AnyFlatSpec with Matchers { 8 | 9 | it should "modify a non-nested indexed seq with case class item" in { 10 | modify(is1)(_.index(2).a4.a5.name).using(duplicate) should be(l1at2dup) 11 | modify(is1)(_.index(2)) 12 | .using(a3 => modify(a3)(_.a4.a5.name).using(duplicate)) should be(l1at2dup) 13 | } 14 | 15 | it should "modify a nested indexed seq using index" in { 16 | modify(iss1)(_.index(2).index(1).name).using(duplicate) should be(ll1at2at1dup) 17 | } 18 | 19 | it should "modify a nested indexed seq using index and each" in { 20 | modify(iss1)(_.index(2).each.name).using(duplicate) should be(ll1at2eachdup) 21 | modify(iss1)(_.each.index(1).name).using(duplicate) should be(ll1eachat1dup) 22 | } 23 | 24 | it should "not modify if given index does not exist" in { 25 | modify(is1)(_.index(10).a4.a5.name).using(duplicate) should be(l1) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /quicklens/src/test/scala/com/softwaremill/quicklens/ModifyLazyTest.scala: -------------------------------------------------------------------------------- 1 | package com.softwaremill.quicklens 2 | 3 | import com.softwaremill.quicklens.TestData._ 4 | import org.scalatest.flatspec.AnyFlatSpec 5 | import org.scalatest.matchers.should.Matchers 6 | 7 | class ModifyLazyTest extends AnyFlatSpec with Matchers { 8 | it should "modify a single-nested case class field" in { 9 | val ml = modifyLens[A5](_.name).using(duplicate) 10 | ml(a5) should be(a5dup) 11 | } 12 | 13 | it should "modify a deeply-nested case class field" in { 14 | val ml = modifyLens[A1](_.a2.a3.a4.a5.name).using(duplicate) 15 | ml(a1) should be(a1dup) 16 | } 17 | 18 | it should "modify several fields" in { 19 | val ml = modifyAllLens[B1](_.b2, _.b3.each).using(duplicate) 20 | ml(b1) should be(b1dupdup) 21 | } 22 | 23 | it should "modify a case class field if the condition is true" in { 24 | val ml = modifyLens[A5](_.name).usingIf(true)(duplicate) 25 | ml(a5) should be(a5dup) 26 | } 27 | 28 | it should "leave a case class unchanged if the condition is flase" in { 29 | val ml = modifyLens[A5](_.name).usingIf(false)(duplicate) 30 | ml(a5) should be(a5) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /quicklens/src/test/scala/com/softwaremill/quicklens/ModifyMapAtTest.scala: -------------------------------------------------------------------------------- 1 | package com.softwaremill.quicklens 2 | 3 | import com.softwaremill.quicklens.TestData._ 4 | import org.scalatest.flatspec.AnyFlatSpec 5 | import org.scalatest.matchers.should.Matchers 6 | 7 | class ModifyMapAtTest extends AnyFlatSpec with Matchers { 8 | 9 | it should "modify a non-nested map with case class item" in { 10 | modify(m1)(_.at("K1").a5.name).using(duplicate) should be(m1dup) 11 | } 12 | 13 | it should "modify a non-nested map with atOrElse" in { 14 | modify(m1)(_.atOrElse("K1", A4(A5("d4"))).a5.name).using(duplicate) should be(m1dup) 15 | modify(m1)(_.atOrElse("K1", ???).a5.name).using(duplicate) should be(m1dup) 16 | modify(m1)(_.atOrElse("K4", A4(A5("d4"))).a5.name).using(duplicate) should be(m1missingdup) 17 | } 18 | 19 | it should "modify a non-nested sorted map with case class item" in { 20 | modify(ms1)(_.at("K1").a5.name).using(duplicate) should be(m1dup) 21 | } 22 | 23 | it should "modify a non-nested hash map with case class item" in { 24 | modify(mh1)(_.at("K1").a5.name).using(duplicate) should be(m1dup) 25 | } 26 | 27 | it should "modify a non-nested listed map with case class item" in { 28 | modify(ml1)(_.at("K1").a5.name).using(duplicate) should be(m1dup) 29 | } 30 | 31 | it should "modify a nested map using at" in { 32 | modify(m2)(_.m3.at("K1").a5.name).using(duplicate) should be(m2dup) 33 | } 34 | 35 | it should "modify a nested map using atOrElse" in { 36 | modify(m2)(_.m3.atOrElse("K4", A4(A5("d4"))).a5.name).using(duplicate) should be(m2missingdup) 37 | } 38 | 39 | it should "modify a non-nested map using each" in { 40 | modify(m1)(_.each.a5.name).using(duplicate) should be(m1dupEach) 41 | } 42 | 43 | it should "modify a non-nested sorted map using each" in { 44 | modify(ms1)(_.each.a5.name).using(duplicate) should be(m1dupEach) 45 | } 46 | 47 | it should "modify a non-nested hash map using each" in { 48 | modify(mh1)(_.each.a5.name).using(duplicate) should be(m1dupEach) 49 | } 50 | 51 | it should "modify a non-nested list map using each" in { 52 | modify(ml1)(_.each.a5.name).using(duplicate) should be(m1dupEach) 53 | } 54 | 55 | it should "throw an exception if there's no such element" in { 56 | an[NoSuchElementException] should be thrownBy { 57 | modify(m1)(_.at("K0").a5.name).using(duplicate) 58 | } 59 | } 60 | 61 | it should "modify a map using at with a derived class" in { 62 | class C 63 | object D extends C 64 | val m = Map[C, String](D -> "") 65 | val expected = Map(D -> "x") 66 | modify(m)(_.at(D)).setTo("x") should be(expected) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /quicklens/src/test/scala/com/softwaremill/quicklens/ModifyMapIndexTest.scala: -------------------------------------------------------------------------------- 1 | package com.softwaremill.quicklens 2 | 3 | import com.softwaremill.quicklens.TestData._ 4 | import org.scalatest.flatspec.AnyFlatSpec 5 | import org.scalatest.matchers.should.Matchers 6 | 7 | class ModifyMapIndexTest extends AnyFlatSpec with Matchers { 8 | 9 | it should "modify a non-nested map with case class item" in { 10 | modify(m1)(_.index("K1").a5.name).using(duplicate) should be(m1dup) 11 | } 12 | 13 | it should "modify a non-nested sorted map with case class item" in { 14 | modify(ms1)(_.index("K1").a5.name).using(duplicate) should be(m1dup) 15 | } 16 | 17 | it should "modify a non-nested hash map with case class item" in { 18 | modify(mh1)(_.index("K1").a5.name).using(duplicate) should be(m1dup) 19 | } 20 | 21 | it should "modify a non-nested listed map with case class item" in { 22 | modify(ml1)(_.index("K1").a5.name).using(duplicate) should be(m1dup) 23 | } 24 | 25 | it should "modify a nested map using index" in { 26 | modify(m2)(_.m3.index("K1").a5.name).using(duplicate) should be(m2dup) 27 | } 28 | 29 | it should "not modify if there's no such element" in { 30 | modify(m1)(_.index("K0").a5.name).using(duplicate) should be(m1) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /quicklens/src/test/scala/com/softwaremill/quicklens/ModifyOptionAtOrElseTest.scala: -------------------------------------------------------------------------------- 1 | package com.softwaremill.quicklens 2 | 3 | import org.scalatest.flatspec.AnyFlatSpec 4 | import org.scalatest.matchers.should.Matchers 5 | 6 | class ModifyOptionAtOrElseTest extends AnyFlatSpec with Matchers { 7 | 8 | it should "modify a Some" in { 9 | modify(Option(1))(_.atOrElse(3)).using(_ + 1) should be(Option(2)) 10 | } 11 | 12 | it should "modify a None with default" in { 13 | modify(None: Option[Int])(_.atOrElse(3)).using(_ + 1) should be(Option(4)) 14 | } 15 | 16 | it should "modify a Option in a case class hierarchy" in { 17 | case class Foo(a: Int) 18 | case class Bar(foo: Foo) 19 | case class BarOpt(maybeBar: Option[Bar]) 20 | case class BazOpt(barOpt: BarOpt) 21 | modify(BazOpt(BarOpt(None)))(_.barOpt.maybeBar.atOrElse(Bar(Foo(5))).foo.a).using(_ + 1) should be( 22 | BazOpt(BarOpt(Some(Bar(Foo(6))))) 23 | ) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /quicklens/src/test/scala/com/softwaremill/quicklens/ModifyOptionAtTest.scala: -------------------------------------------------------------------------------- 1 | package com.softwaremill.quicklens 2 | 3 | import java.util.NoSuchElementException 4 | 5 | import org.scalatest.flatspec.AnyFlatSpec 6 | import org.scalatest.matchers.should.Matchers 7 | 8 | class ModifyOptionAtTest extends AnyFlatSpec with Matchers { 9 | 10 | it should "modify a Option with case class item" in { 11 | modify(Option(1))(_.at).using(_ + 1) should be(Option(2)) 12 | } 13 | 14 | it should "modify a Option in a case class hierarchy" in { 15 | case class Foo(a: Int) 16 | case class Bar(foo: Foo) 17 | case class BarOpt(maybeBar: Option[Bar]) 18 | case class BazOpt(barOpt: BarOpt) 19 | modify(BazOpt(BarOpt(Some(Bar(Foo(4))))))(_.barOpt.maybeBar.at.foo.a).using(_ + 1) should be( 20 | BazOpt(BarOpt(Some(Bar(Foo(5))))) 21 | ) 22 | } 23 | 24 | it should "crashes on missing key" in { 25 | an[NoSuchElementException] should be thrownBy modify(Option.empty[Int])(_.at).using(_ + 1) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /quicklens/src/test/scala/com/softwaremill/quicklens/ModifyOptionIndexTest.scala: -------------------------------------------------------------------------------- 1 | package com.softwaremill.quicklens 2 | 3 | import org.scalatest.flatspec.AnyFlatSpec 4 | import org.scalatest.matchers.should.Matchers 5 | 6 | class ModifyOptionIndexTest extends AnyFlatSpec with Matchers { 7 | 8 | it should "modify a Option with case class item" in { 9 | modify(Option(1))(_.index).using(_ + 1) should be(Option(2)) 10 | } 11 | 12 | it should "modify a Option in a case class hierarchy" in { 13 | case class Foo(a: Int) 14 | case class Bar(foo: Foo) 15 | case class BarOpt(maybeBar: Option[Bar]) 16 | case class BazOpt(barOpt: BarOpt) 17 | modify(BazOpt(BarOpt(Some(Bar(Foo(4))))))(_.barOpt.maybeBar.index.foo.a).using(_ + 1) should be( 18 | BazOpt(BarOpt(Some(Bar(Foo(5))))) 19 | ) 20 | } 21 | 22 | it should "not modify on missing key" in { 23 | modify(Option.empty[Int])(_.index).using(_ + 1) should be(None) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /quicklens/src/test/scala/com/softwaremill/quicklens/ModifyPimpTest.scala: -------------------------------------------------------------------------------- 1 | package com.softwaremill.quicklens 2 | 3 | import com.softwaremill.quicklens.TestData._ 4 | import org.scalatest.flatspec.AnyFlatSpec 5 | import org.scalatest.matchers.should.Matchers 6 | 7 | class ModifyPimpTest extends AnyFlatSpec with Matchers { 8 | it should "modify a field once" in { 9 | a1.modify(_.a2.a3.a4.a5.name).using(duplicate) should be(a1dup) 10 | } 11 | 12 | it should "modify a deeply-nested case class field" in { 13 | a1.modify(_.a2.a3.a4.a5.name) 14 | .using(duplicate) 15 | .modify(_.a2.a3.a4.a5.name) 16 | .using(duplicate) should be(a1dupdup) 17 | } 18 | 19 | it should "modify several fields" in { 20 | b1.modifyAll(_.b2, _.b3.each).using(duplicate) should be(b1dupdup) 21 | } 22 | 23 | it should "modify polymorphic case class field" in { 24 | aPoly.modify(_.poly).using(duplicate) should be(aPolyDup) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /quicklens/src/test/scala/com/softwaremill/quicklens/ModifySelfThisTest.scala: -------------------------------------------------------------------------------- 1 | package com.softwaremill.quicklens 2 | 3 | import org.scalatest.flatspec.AnyFlatSpec 4 | import org.scalatest.matchers.should.Matchers 5 | 6 | import ModifySelfThisTest._ 7 | 8 | object ModifySelfThisTest { 9 | 10 | case class State(x: Int) { self => 11 | 12 | def mod: State = this.modify(_.x).setTo(1) 13 | } 14 | 15 | trait A { 16 | def a: Unit 17 | } 18 | 19 | case class State1(x: Int) extends A { self: A => 20 | 21 | def mod: State1 = this.modify(_.x).setTo(1) 22 | 23 | def a: Unit = () 24 | } 25 | } 26 | 27 | class ModifySelfThisTest extends AnyFlatSpec with Matchers { 28 | it should "modify an object even in presence of self alias" in { 29 | val s = State(0) 30 | val modified = s.mod 31 | 32 | modified.x shouldBe 1 33 | } 34 | 35 | it should "modify an object even in presence of self type" in { 36 | val s = State(0) 37 | val modified = s.mod 38 | 39 | modified.x shouldBe 1 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /quicklens/src/test/scala/com/softwaremill/quicklens/ModifySeqIndexTest.scala: -------------------------------------------------------------------------------- 1 | package com.softwaremill.quicklens 2 | 3 | import com.softwaremill.quicklens.TestData._ 4 | import org.scalatest.flatspec.AnyFlatSpec 5 | import org.scalatest.matchers.should.Matchers 6 | 7 | class ModifySeqIndexTest extends AnyFlatSpec with Matchers { 8 | 9 | it should "modify a non-nested seq with case class item" in { 10 | modify(s1)(_.index(2).a4.a5.name).using(duplicate) should be(l1at2dup) 11 | modify(s1)(_.index(2)) 12 | .using(a3 => modify(a3)(_.a4.a5.name).using(duplicate)) should be(l1at2dup) 13 | } 14 | 15 | it should "modify a nested seq using index" in { 16 | modify(ss1)(_.index(2).index(1).name).using(duplicate) should be(ll1at2at1dup) 17 | } 18 | 19 | it should "modify a nested seq using index and each" in { 20 | modify(ss1)(_.index(2).each.name).using(duplicate) should be(ll1at2eachdup) 21 | modify(ss1)(_.each.index(1).name).using(duplicate) should be(ll1eachat1dup) 22 | } 23 | 24 | it should "not modify if given index does not exist" in { 25 | modify(s1)(_.index(10).a4.a5.name).using(duplicate) should be(l1) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /quicklens/src/test/scala/com/softwaremill/quicklens/ModifySimpleTest.scala: -------------------------------------------------------------------------------- 1 | package com.softwaremill.quicklens 2 | 3 | import org.scalatest.flatspec.AnyFlatSpec 4 | import org.scalatest.matchers.should.Matchers 5 | import TestData._ 6 | 7 | class ModifySimpleTest extends AnyFlatSpec with Matchers { 8 | it should "modify a single-nested case class field" in { 9 | modify(a5)(_.name).using(duplicate) should be(a5dup) 10 | } 11 | 12 | it should "modify a single-nested case class field using apply" in { 13 | modify(a5)(_.name)(duplicate) should be(a5dup) 14 | } 15 | 16 | it should "modify a deeply-nested case class field" in { 17 | modify(a1)(_.a2.a3.a4.a5.name).using(duplicate) should be(a1dup) 18 | } 19 | 20 | it should "modify several fields" in { 21 | modifyAll(b1)(_.b2, _.b3.each).using(duplicate) should be(b1dupdup) 22 | } 23 | 24 | it should "modify a case class field if the condition is true" in { 25 | modify(a5)(_.name).usingIf(true)(duplicate) should be(a5dup) 26 | } 27 | 28 | it should "leave a case class unchanged if the condition is false" in { 29 | modify(a5)(_.name).usingIf(false)(duplicate) should be(a5) 30 | } 31 | 32 | it should "modify polymorphic case class field" in { 33 | modify(aPoly)(_.poly).using(duplicate) should be(aPolyDup) 34 | } 35 | 36 | it should "modify polymorphic case class field using apply" in { 37 | modify(aPoly)(_.poly)(duplicate) should be(aPolyDup) 38 | } 39 | 40 | it should "modify polymorphic case class field if condition is true" in { 41 | modify(aPoly)(_.poly).usingIf(true)(duplicate) should be(aPolyDup) 42 | } 43 | it should "leave a polymorphic case class field if condition is false" in { 44 | modify(aPoly)(_.poly).usingIf(false)(duplicate) should be(aPoly) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /quicklens/src/test/scala/com/softwaremill/quicklens/ModifyWhenTest.scala: -------------------------------------------------------------------------------- 1 | package com.softwaremill.quicklens 2 | 3 | import org.scalatest.flatspec.AnyFlatSpec 4 | import org.scalatest.matchers.should.Matchers 5 | 6 | object ModifyWhenTestData { 7 | trait Animal 8 | case class Dog(age: Int) extends Animal 9 | case class Cat(ages: List[Int]) extends Animal 10 | case class Zoo(animals: List[Animal]) 11 | 12 | val dog: Animal = Dog(4) 13 | val olderDog: Animal = Dog(5) 14 | 15 | val cat: Animal = Cat(List(3, 12, 13)) 16 | val olderCat: Animal = Cat(List(4, 12, 13)) 17 | 18 | val zoo = Zoo(List(dog, cat)) 19 | val olderZoo = Zoo(List(olderDog, olderCat)) 20 | 21 | trait MyOption[+A] 22 | case class MySome[+A](value: A) extends MyOption[A] 23 | case object MyNone extends MyOption[Nothing] 24 | 25 | val someDog: MyOption[Dog] = MySome(Dog(4)) 26 | val someOlderDog: MyOption[Dog] = MySome(Dog(5)) 27 | val noDog: MyOption[Dog] = MyNone 28 | } 29 | 30 | class ModifyWhenTest extends AnyFlatSpec with Matchers { 31 | import ModifyWhenTestData._ 32 | 33 | it should "modify a field in a subtype" in { 34 | dog.modify(_.when[Dog].age).using(_ + 1) shouldEqual olderDog 35 | } 36 | 37 | it should "ignore subtypes other than the selected one" in { 38 | cat.modify(_.when[Dog].age).using(_ + 1) shouldEqual cat 39 | } 40 | 41 | it should "modify a Functor field in a subtype" in { 42 | cat.modify(_.when[Cat].ages.at(0)).using(_ + 1) shouldEqual olderCat 43 | } 44 | 45 | it should "modify a field in a subtype through a Functor" in { 46 | zoo 47 | .modifyAll( 48 | _.animals.each.when[Dog].age, 49 | _.animals.each.when[Cat].ages.at(0) 50 | ) 51 | .using(_ + 1) shouldEqual olderZoo 52 | } 53 | 54 | it should "modify a field in a subtypes (parameterized)" in { 55 | someDog.modify(_.when[MySome[Dog]].value.age).using(_ + 1) shouldEqual someOlderDog 56 | } 57 | 58 | it should "ignore subtypes other than the selected one (parameterized)" in { 59 | noDog.modify(_.when[MySome[Dog]].value.age).using(_ + 1) shouldEqual MyNone 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /quicklens/src/test/scala/com/softwaremill/quicklens/RepeatedModifyAllTest.scala: -------------------------------------------------------------------------------- 1 | package com.softwaremill.quicklens 2 | 3 | import org.scalatest.flatspec.AnyFlatSpec 4 | import org.scalatest.matchers.should.Matchers 5 | 6 | class RepeatedModifyAllTest extends AnyFlatSpec with Matchers { 7 | import RepeatedModifyAllTest._ 8 | 9 | it should "expand a very long repeated function" in { 10 | val c6 = C6(1) 11 | val c5 = C5(c6) 12 | val c4 = C4(c5) 13 | val c3 = C3(c4) 14 | val c2 = C2(c3) 15 | val c1 = C1(c2) 16 | 17 | val c6e = C6(2) 18 | val c5e = C5(c6e) 19 | val c4e = C4(c5e) 20 | val c3e = C3(c4e) 21 | val c2e = C2(c3e) 22 | val c1e = C1(c2e) 23 | 24 | val res = c1 25 | .modifyAll( 26 | _.a.a.a.a.a.a, 27 | _.a.a.a.a.a.a, 28 | _.a.a.a.a.a.a, 29 | _.a.a.a.a.a.a, 30 | _.a.a.a.a.a.a, 31 | _.a.a.a.a.a.a, 32 | _.a.a.a.a.a.a, 33 | _.a.a.a.a.a.a, 34 | _.a.a.a.a.a.a, 35 | _.a.a.a.a.a.a 36 | ) 37 | .setTo(2) 38 | res should be(c1e) 39 | } 40 | 41 | it should "expand a very long repeated function correct number of times" in { 42 | val c6 = C6(1) 43 | val c5 = C5(c6) 44 | val c4 = C4(c5) 45 | val c3 = C3(c4) 46 | val c2 = C2(c3) 47 | val c1 = C1(c2) 48 | 49 | val c6e = C6(11) 50 | val c5e = C5(c6e) 51 | val c4e = C4(c5e) 52 | val c3e = C3(c4e) 53 | val c2e = C2(c3e) 54 | val c1e = C1(c2e) 55 | 56 | val res = c1 57 | .modifyAll( 58 | _.a.a.a.a.a.a, 59 | _.a.a.a.a.a.a, 60 | _.a.a.a.a.a.a, 61 | _.a.a.a.a.a.a, 62 | _.a.a.a.a.a.a, 63 | _.a.a.a.a.a.a, 64 | _.a.a.a.a.a.a, 65 | _.a.a.a.a.a.a, 66 | _.a.a.a.a.a.a, 67 | _.a.a.a.a.a.a 68 | ) 69 | .using(_ + 1) 70 | res should be(c1e) 71 | } 72 | } 73 | 74 | object RepeatedModifyAllTest { 75 | case class C1( 76 | a: C2 77 | ) 78 | 79 | case class C2( 80 | a: C3 81 | ) 82 | 83 | case class C3( 84 | a: C4 85 | ) 86 | 87 | case class C4( 88 | a: C5 89 | ) 90 | 91 | case class C5( 92 | a: C6 93 | ) 94 | 95 | case class C6( 96 | a: Int 97 | ) 98 | } 99 | -------------------------------------------------------------------------------- /quicklens/src/test/scala/com/softwaremill/quicklens/RepeatedModifyTest.scala: -------------------------------------------------------------------------------- 1 | package com.softwaremill.quicklens 2 | 3 | import org.scalatest.flatspec.AnyFlatSpec 4 | import org.scalatest.matchers.should.Matchers 5 | 6 | class RepeatedModifyTest extends AnyFlatSpec with Matchers { 7 | import RepeatedModifyTest._ 8 | 9 | it should "properly handle repeated modify invocations for different fields" in { 10 | val c = C(B(1, 1, 1, 1, 1)) 11 | c 12 | .modify(_.b.a1) 13 | .setTo(0.0d) 14 | .modify(_.b.a2) 15 | .setTo(0.0d) 16 | .modify(_.b.a3) 17 | .setTo(0.0d) 18 | .modify(_.b.a4) 19 | .setTo(0.0d) 20 | .modify(_.b.a5) 21 | .setTo(0.0d) shouldBe C(B(0, 0, 0, 0, 0)) 22 | } 23 | 24 | it should "properly handle repeated modify invocations for the same field" in { 25 | val c = C(B(1, 1, 1, 1, 1)) 26 | c 27 | .modify(_.b.a1) 28 | .setTo(0.0d) 29 | .modify(_.b.a1) 30 | .setTo(1.0d) 31 | .modify(_.b.a1) 32 | .setTo(2.0d) 33 | .modify(_.b.a1) 34 | .setTo(3.0d) 35 | .modify(_.b.a1) 36 | .setTo(4.0d) shouldBe C(B(4, 1, 1, 1, 1)) 37 | } 38 | } 39 | 40 | object RepeatedModifyTest { 41 | case class B(a1: Double, a2: Double, a3: Double, a4: Double, a5: Double) 42 | case class C(b: B) 43 | } 44 | -------------------------------------------------------------------------------- /quicklens/src/test/scala/com/softwaremill/quicklens/SealedTest.scala: -------------------------------------------------------------------------------- 1 | package com.softwaremill.quicklens 2 | 3 | import com.softwaremill.quicklens.TestData.duplicate 4 | import org.scalatest.flatspec.AnyFlatSpec 5 | import org.scalatest.matchers.should.Matchers 6 | 7 | /** This test data is in the same file as the test to ensure correct compilation order. See 8 | * https://issues.scala-lang.org/browse/SI-7046. 9 | */ 10 | object SealedTestData { 11 | case class G(p1: Option[P1]) 12 | sealed trait P1 { 13 | def x: String 14 | def f: Option[String] 15 | } 16 | case class C1(x: String, f: Option[String]) extends P1 17 | case class C2(x: String, f: Option[String]) extends P1 18 | 19 | val p1: P1 = C2("c2", None) 20 | val p1dup: P1 = C2("c2c2", None) 21 | 22 | val g1 = G(Some(C1("c1", Some("c2")))) 23 | val g1dup = G(Some(C1("c1c1", Some("c2")))) 24 | val g1eachdup = G(Some(C1("c1", Some("c2c2")))) 25 | 26 | sealed trait P2 { 27 | def x: String 28 | } 29 | case class C3(x: String) extends P2 30 | sealed trait P3 extends P2 31 | case class C4(x: String) extends P3 32 | 33 | val p2: P2 = C4("c4") 34 | val p2dup: P2 = C4("c4c4") 35 | 36 | // example from the README 37 | 38 | sealed trait Pet { def name: String } 39 | case class Fish(name: String) extends Pet 40 | sealed trait LeggedPet extends Pet 41 | case class Cat(name: String) extends LeggedPet 42 | case class Dog(name: String) extends LeggedPet 43 | 44 | val pets = List[Pet](Fish("Finn"), Cat("Catia"), Dog("Douglas")) 45 | val juniorPets = 46 | List[Pet](Fish("Finn, Jr."), Cat("Catia, Jr."), Dog("Douglas, Jr.")) 47 | } 48 | 49 | class SealedTest extends AnyFlatSpec with Matchers { 50 | import SealedTestData._ 51 | 52 | it should "modify a field in a sealed trait" in { 53 | modify(p1)(_.x).using(duplicate) should be(p1dup) 54 | } 55 | 56 | it should "modify a field in a sealed trait through a Functor" in { 57 | modify(g1)(_.p1.each.x).using(duplicate) should be(g1dup) 58 | } 59 | 60 | it should "modify a Functor field in a sealed trait" in { 61 | modify(g1)(_.p1.each.f.each).using(duplicate) should be(g1eachdup) 62 | } 63 | 64 | it should "modify a field in a hierarchy of sealed traits" in { 65 | modify(p2)(_.x).using(duplicate) should be(p2dup) 66 | } 67 | 68 | it should "modify a list of pets from the example" in { 69 | modify(pets)(_.each.name).using(_ + ", Jr.") should be(juniorPets) 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /quicklens/src/test/scala/com/softwaremill/quicklens/SecondParamListTest.scala: -------------------------------------------------------------------------------- 1 | package com.softwaremill.quicklens 2 | 3 | import com.softwaremill.quicklens.TestData._ 4 | import org.scalatest.flatspec.AnyFlatSpec 5 | import org.scalatest.matchers.should.Matchers 6 | 7 | class SecondParamListTest extends AnyFlatSpec with Matchers { 8 | it should "modify an object with second implicit param list" in { 9 | import com.softwaremill.quicklens._ 10 | 11 | case class State(inside: Boolean)(implicit d: Double) 12 | 13 | val d: Double = 1.0 14 | 15 | val state1 = State(true)(d) 16 | 17 | implicit val dd: Double = d 18 | val state2 = state1.modify(_.inside).setTo(true) 19 | 20 | state1 should be(state2) 21 | } 22 | 23 | it should "should give a meaningful error for an object with more than one non-implicit param list" in { 24 | import com.softwaremill.quicklens._ 25 | 26 | case class State(inside: Boolean)(d: Double) 27 | 28 | val d: Double = 1.0 29 | 30 | val state1 = State(true)(d) 31 | 32 | implicit val dd: Double = d 33 | 34 | assertDoesNotCompile("state1.modify(_.inside).setTo(true)") 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /quicklens/src/test/scala/com/softwaremill/quicklens/SetToSimpleTest.scala: -------------------------------------------------------------------------------- 1 | package com.softwaremill.quicklens 2 | 3 | import com.softwaremill.quicklens.TestData._ 4 | import org.scalatest.flatspec.AnyFlatSpec 5 | import org.scalatest.matchers.should.Matchers 6 | 7 | class SetToSimpleTest extends AnyFlatSpec with Matchers { 8 | it should "set a new value of a single-nested case class field" in { 9 | modify(a1)(_.a2.a3.a4.a5.name).setTo("mod") should be(a1mod) 10 | } 11 | 12 | it should "set a new value in a case class if the condition is true" in { 13 | modify(a1)(_.a2.a3.a4.a5.name).setToIf(true)("mod") should be(a1mod) 14 | } 15 | 16 | it should "leave a case class unchanged if the condition is false" in { 17 | modify(a1)(_.a2.a3.a4.a5.name).setToIf(false)("mod") should be(a1) 18 | } 19 | 20 | it should "set a new value in a case class if it is defined" in { 21 | modify(a1)(_.a2.a3.a4.a5.name).setToIfDefined(Some("mod")) should be(a1mod) 22 | } 23 | 24 | it should "leave a case class unchanged if the value is not defined" in { 25 | modify(a1)(_.a2.a3.a4.a5.name).setToIfDefined(None) should be(a1) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /quicklens/src/test/scala/com/softwaremill/quicklens/TestData.scala: -------------------------------------------------------------------------------- 1 | package com.softwaremill.quicklens 2 | 3 | object TestData { 4 | def duplicate(s: String) = s + s 5 | 6 | case class A1(a2: A2) 7 | case class A2(a3: A3) 8 | case class A3(a4: A4) 9 | case class A4(a5: A5) 10 | case class A5(name: String) 11 | 12 | val a5 = A5("data") 13 | val a5dup = A5("datadata") 14 | 15 | val a1 = A1(A2(A3(A4(A5("data"))))) 16 | val a1dup = A1(A2(A3(A4(A5("datadata"))))) 17 | val a1dupdup = A1(A2(A3(A4(A5("datadatadatadata"))))) 18 | val a1mod = A1(A2(A3(A4(A5("mod"))))) 19 | 20 | case class APoly[T](poly: T, i: Int) 21 | val aPoly = APoly("data", 1) 22 | val aPolyDup = APoly("datadata", 1) 23 | 24 | case class B1(b2: String, b3: Option[String]) 25 | 26 | val b1 = B1("data", Some("data")) 27 | val b1dupdup = B1("datadata", Some("datadata")) 28 | 29 | case class X1(x2: X2) 30 | case class X2(x3: Option[X3]) 31 | case class X3(x4: X4) 32 | case class X4(x5: Option[X5]) 33 | case class X5(name: String) 34 | 35 | val x4 = X4(Some(X5("data"))) 36 | val x4dup = X4(Some(X5("datadata"))) 37 | 38 | val x4none = X4(None) 39 | 40 | val x1 = X1(X2(Some(X3(X4(Some(X5("data"))))))) 41 | val x1dup = X1(X2(Some(X3(X4(Some(X5("datadata"))))))) 42 | 43 | val x1none = X1(X2(None)) 44 | 45 | case class Y1(y2: Y2) 46 | case class Y2(y3: List[Y3]) 47 | case class Y3(y4: Option[Y4]) 48 | case class Y4(name: String) 49 | 50 | val y1 = Y1(Y2(List(Y3(Some(Y4("d1"))), Y3(Some(Y4("d2"))), Y3(None)))) 51 | val y1dup = Y1(Y2(List(Y3(Some(Y4("d1d1"))), Y3(Some(Y4("d2d2"))), Y3(None)))) 52 | val y1at1dup = Y1(Y2(List(Y3(Some(Y4("d1"))), Y3(Some(Y4("d2d2"))), Y3(None)))) 53 | 54 | case class Z1(name: Option[String]) 55 | val z1 = Z1(Some("data")) 56 | val z1dup = Z1(Some("datadata")) 57 | 58 | val l1 = 59 | List(A3(A4(A5("d1"))), A3(A4(A5("d2"))), A3(A4(A5("d3"))), A3(A4(A5("d4")))) 60 | val l1at2dup = List(A3(A4(A5("d1"))), A3(A4(A5("d2"))), A3(A4(A5("d3d3"))), A3(A4(A5("d4")))) 61 | val ll1 = List(List(A5("d1"), A5("d2")), List(A5("d3"), A5("d4"), A5("d5")), List(A5("d6"), A5("d7"))) 62 | val ll1at2at1dup = List(List(A5("d1"), A5("d2")), List(A5("d3"), A5("d4"), A5("d5")), List(A5("d6"), A5("d7d7"))) 63 | val ll1at2eachdup = List(List(A5("d1"), A5("d2")), List(A5("d3"), A5("d4"), A5("d5")), List(A5("d6d6"), A5("d7d7"))) 64 | val ll1eachat1dup = List(List(A5("d1"), A5("d2d2")), List(A5("d3"), A5("d4d4"), A5("d5")), List(A5("d6"), A5("d7d7"))) 65 | 66 | val s1: Seq[A3] = l1 67 | val ss1: Seq[Seq[A5]] = ll1 68 | 69 | val is1 = l1.toIndexedSeq 70 | val iss1 = ss1.map(_.toIndexedSeq).toIndexedSeq 71 | 72 | val ar1: Array[A3] = s1.toArray 73 | val arar1: Array[Array[A5]] = ss1.map(_.toArray).toArray 74 | 75 | case class M2(m3: Map[String, A4]) 76 | 77 | val m1 = Map("K1" -> A4(A5("d1")), "K2" -> A4(A5("d2")), "K3" -> A4(A5("d3"))) 78 | val m2 = M2(Map("K1" -> A4(A5("d1")), "K2" -> A4(A5("d2")), "K3" -> A4(A5("d3")))) 79 | val m1dup = 80 | Map("K1" -> A4(A5("d1d1")), "K2" -> A4(A5("d2")), "K3" -> A4(A5("d3"))) 81 | val m1missingdup = 82 | Map("K1" -> A4(A5("d1")), "K2" -> A4(A5("d2")), "K3" -> A4(A5("d3")), "K4" -> A4(A5("d4d4"))) 83 | val m2missingdup = M2(Map("K1" -> A4(A5("d1")), "K2" -> A4(A5("d2")), "K3" -> A4(A5("d3")), "K4" -> A4(A5("d4d4")))) 84 | val m1dupEach = 85 | Map("K1" -> A4(A5("d1d1")), "K2" -> A4(A5("d2d2")), "K3" -> A4(A5("d3d3"))) 86 | val m2dup = M2(Map("K1" -> A4(A5("d1d1")), "K2" -> A4(A5("d2")), "K3" -> A4(A5("d3")))) 87 | val ms1 = collection.immutable.SortedMap("K1" -> A4(A5("d1")), "K2" -> A4(A5("d2")), "K3" -> A4(A5("d3"))) 88 | val mh1 = collection.immutable.HashMap("K1" -> A4(A5("d1")), "K2" -> A4(A5("d2")), "K3" -> A4(A5("d3"))) 89 | val ml1 = collection.immutable.ListMap("K1" -> A4(A5("d1")), "K2" -> A4(A5("d2")), "K3" -> A4(A5("d3"))) 90 | 91 | case class Type(name: String) 92 | sealed abstract class Variance { 93 | val typ: Type 94 | } 95 | case class Covariance(typ: Type) extends Variance 96 | case class Contravariance(typ: Type) extends Variance 97 | case class Invariance(typ: Type) extends Variance 98 | 99 | val invInt: Variance = Invariance(Type("Int")) 100 | val invLong: Variance = Invariance(Type("Long")) 101 | 102 | case class Street(name: String) 103 | case class Address(street: Option[Street]) 104 | case class Person(addresses: List[Address]) 105 | 106 | val person = Person( 107 | List( 108 | Address(Some(Street("1 Functional Rd."))), 109 | Address(Some(Street("2 Imperative Dr."))) 110 | ) 111 | ) 112 | 113 | case class Foo(field: String) 114 | } 115 | -------------------------------------------------------------------------------- /quicklens/src/test/scala/com/softwaremill/quicklens/TupleModifyTest.scala: -------------------------------------------------------------------------------- 1 | package com.softwaremill.quicklens 2 | 3 | import org.scalatest.flatspec.AnyFlatSpec 4 | import org.scalatest.matchers.should.Matchers 5 | 6 | class TupleModifyTest extends AnyFlatSpec with Matchers { 7 | it should "modify case classes using setTo" in { 8 | case class Pair(a: Int, b: Int) 9 | val p = Pair(0, 1) 10 | val modified = p.modify(_.b).setTo(2) 11 | modified shouldBe Pair(0, 2) 12 | } 13 | it should "modify tuples using setTo" in { 14 | val tuple = (0, 1) 15 | val modified = tuple.modify(_._2).setTo(2) 16 | modified shouldBe ((0, 2)) 17 | } 18 | it should "modify tuples using using" in { 19 | val tuple4 = (0, 1, 2, 3) 20 | val modified = tuple4.modify(_._3).using(_ + 1) 21 | modified shouldBe ((0, 1, 3, 3)) 22 | } 23 | 24 | it should "modify tuples using multiple modify" in { 25 | val tuple4 = (0, 1, 2, 3) 26 | val modified = tuple4.modify(_._3).using(_ + 1).modify(_._4).using(_ + 1) 27 | modified shouldBe ((0, 1, 3, 4)) 28 | } 29 | } 30 | --------------------------------------------------------------------------------