├── .github └── workflows │ ├── github-release.yml │ ├── sbt-coverage.yml │ └── sbt-release.yml ├── .gitignore ├── .scalafmt.conf ├── LICENSE ├── README.md ├── build.sbt ├── notes ├── 0.3.0.markdown ├── 0.3.1.markdown ├── 0.3.2.markdown ├── 0.3.4.markdown ├── 0.3.5.markdown ├── 0.3.6.markdown ├── 0.3.7.markdown └── about.markdown ├── project ├── build.properties └── plugins.sbt └── src ├── main └── scala │ └── com │ └── wacai │ └── config │ └── annotation │ ├── Configurable.scala │ ├── Macro.scala │ └── conf.scala └── test ├── resources ├── application.conf ├── backTicksKeys.conf ├── common.conf ├── concrete.conf ├── kafka.conf ├── list.conf ├── maps.conf └── specialchars.conf └── scala └── com └── wacai └── config └── annotation ├── ConfAnnotationSpec.scala └── UnitGenSpec.scala /.github/workflows/github-release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | milestone: 5 | types: 6 | - closed 7 | 8 | jobs: 9 | releasing: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Create a release with title of milestone 13 | run: gh -R $GITHUB_REPOSITORY release create $REL_TAG -t $REL_TAG -n "$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/milestone/$MS_ID?closed=1" 14 | env: 15 | GITHUB_TOKEN: ${{secrets.REPO_GITHUB_TOKEN}} 16 | REL_TAG: ${{github.event.milestone.title}} 17 | MS_ID: ${{github.event.milestone.number}} 18 | -------------------------------------------------------------------------------- /.github/workflows/sbt-coverage.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | - main 8 | pull_request: 9 | 10 | jobs: 11 | coverage: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v4 15 | - uses: coursier/setup-action@v1 16 | - name: branch-names 17 | id: branch-name 18 | uses: tj-actions/branch-names@v7 19 | - run: sbt coverage +test coverageReport 20 | - run: sbt coveralls 21 | env: 22 | COVERALLS_REPO_TOKEN: ${{ secrets.GITHUB_TOKEN }} 23 | CI_BRANCH: ${{ steps.branch-name.outputs.current_branch }} 24 | -------------------------------------------------------------------------------- /.github/workflows/sbt-release.yml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | on: 3 | push: 4 | tags: ["v*"] 5 | 6 | jobs: 7 | publish: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v4 11 | with: 12 | fetch-depth: 0 13 | - uses: coursier/setup-action@v1 14 | - uses: olafurpg/setup-gpg@v3 15 | - run: sbt ci-release 16 | env: 17 | PGP_PASSPHRASE: ${{ secrets.PGP_PASSPHRASE }} 18 | PGP_SECRET: ${{ secrets.PGP_SECRET }} 19 | SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }} 20 | SONATYPE_USERNAME: ${{ secrets.SONATYPE_USERNAME }} 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | target/ 3 | *.iml 4 | .bsp/ 5 | .metals/ 6 | metals.sbt 7 | .vscode/ 8 | .bloop/ -------------------------------------------------------------------------------- /.scalafmt.conf: -------------------------------------------------------------------------------- 1 | version = 3.7.17 2 | maxColumn = 150 3 | continuationIndent.callSite = 2 4 | continuationIndent.defnSite = 4 5 | assumeStandardLibraryStripMargin = true 6 | align.preset = "most" 7 | rewrite.rules = [SortModifiers, SortImports] 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![CI](https://github.com/hanabix/config-annotation/actions/workflows/sbt-coverage.yml/badge.svg)](https://github.com/hanabix/config-annotation/actions/workflows/sbt-coverage.yml) [![Publish](https://github.com/hanabix/config-annotation/actions/workflows/sbt-release.yml/badge.svg)](https://github.com/hanabix/config-annotation/actions/workflows/sbt-release.yml) [![Maven Central](https://img.shields.io/maven-central/v/com.wacai/config-annotation_2.13)](https://search.maven.org/artifact/com.wacai/config-annotation_2.13) [![Coverage Status](https://coveralls.io/repos/github/hanabix/config-annotation/badge.svg)](https://coveralls.io/github/hanabix/config-annotation) [![Codacy Badge](https://app.codacy.com/project/badge/Grade/ba5b0f3a2107473fbb756e8eabfcbc26)](https://www.codacy.com/gh/hanabix/config-annotation/dashboard?utm_source=github.com&utm_medium=referral&utm_content=hanabix/config-annotation&utm_campaign=Badge_Grade) 2 | 3 | A refactor-friendly configuration lib would help scala programmers to maintain [config][conf] files without any pain, 4 | by using scala [macro annotation][mcr]. 5 | 6 | ## Usage 7 | 8 | Create a config-style trait as configuration definition, eg: 9 | 10 | ```scala 11 | import com.wacai.config.annotation._ 12 | import scala.concurrent.duration._ 13 | 14 | @conf trait kafka { 15 | val server = new { 16 | val host = "wacai.com" 17 | val port = 12306 18 | } 19 | 20 | val socket = new { 21 | val timeout = 3 seconds 22 | val buffer = 1024 * 64L 23 | } 24 | 25 | val client = "wacai" 26 | } 27 | ``` 28 | 29 | Use config by extending it, 30 | 31 | ```scala 32 | class Consumer extends kafka { 33 | val client = new SimpleConsumer( 34 | server.host, 35 | server.port, 36 | socket.timeout, 37 | socket.buffer, 38 | client 39 | ) 40 | 41 | ... 42 | } 43 | ``` 44 | 45 | Compile, `@conf` will let scala compiler to insert codes to `kafka.scala`: 46 | 47 | ```scala 48 | trait kafka { 49 | val server = new { 50 | val host = config.getString("kafka.server.host") 51 | val port = config.getInt("kafka.server.port") 52 | } 53 | val socket = new { 54 | val timeout = Duration(config.getDuration("kafka.socket.timeout", SECONDS)) 55 | val buffer = config.getBytes("kafka.socket.buffer") 56 | } 57 | val client = config.getString("kafka.client") 58 | 59 | ... 60 | } 61 | ``` 62 | 63 | After that, a config file named `kafka.conf` was generated at `src/main/resources` as blow: 64 | 65 | ``` 66 | kafka { 67 | server { 68 | host = wacai.com 69 | port = 12306 70 | } 71 | 72 | socket { 73 | timeout = 3s 74 | buffer = 64K 75 | } 76 | 77 | client = wacai 78 | } 79 | 80 | ``` 81 | 82 | Last but not least, a `application.conf` need to be created to include `kafka.conf` like: 83 | 84 | ``` 85 | include "kafka.conf" 86 | ``` 87 | 88 | 89 | ## Installation 90 | 91 | > Caution: only support scala 2.11.0+ 92 | 93 | Set up your `build.sbt` with: 94 | 95 | ### Scala 2.11 96 | 97 | ```scala 98 | addCompilerPlugin("org.scalamacros" % "paradise" % "2.0.1" cross CrossVersion.full) 99 | 100 | libraryDependencies += "com.wacai" %% "config-annotation" % "0.3.5" 101 | ``` 102 | 103 | ### Scala 2.12 104 | 105 | ```scala 106 | addCompilerPlugin("org.scalamacros" % "paradise" % "2.1.0" cross CrossVersion.full) 107 | 108 | libraryDependencies += "com.wacai" %% "config-annotation" % "0.3.6" 109 | ``` 110 | 111 | ### Scala 2.13 112 | 113 | ```scala 114 | libraryDependencies += "com.wacai" %% "config-annotation" % "0.4.0" 115 | ``` 116 | 117 | 118 | ## Type covenant 119 | 120 | |Scala type | Config getter | Value | 121 | |-----------|---------------|------------| 122 | | Boolean | getBoolean | true/false | 123 | | Int | getInt | number | 124 | | Double | getDouble | float | 125 | | String | getString | text | 126 | | Long | getBytes | number with unit (B, K, M, G) | 127 | | +Duration | getDuration | number with unit (ns, us, ms, s, m, h, d)| 128 | 129 | 130 | ## Integrate with akka actor 131 | 132 | ```scala 133 | import com.wacai.config.annotation._ 134 | 135 | @conf trait kafka extends Configurable { self: Actor => 136 | def config = context.system.settings.config 137 | 138 | ... 139 | } 140 | ``` 141 | 142 | ## Change default generation directory 143 | 144 | Config files would be generated at `src/main/resources` as default. 145 | 146 | It can be changed by appending macro setting to `scalacOption` in `build.sbt`: 147 | 148 | ```scala 149 | scalacOptions += "-Xmacro-settings:conf.output.dir=/path/to/out" 150 | ``` 151 | 152 | ## A runnable example 153 | 154 | Please see [config-annotation-example][cae]. 155 | 156 | 157 | [mcr]:http://docs.scala-lang.org/overviews/macros/annotations.html 158 | [conf]:https://github.com/typesafehub/config 159 | [cae]:https://github.com/wacai/config-annotation-example 160 | -------------------------------------------------------------------------------- /build.sbt: -------------------------------------------------------------------------------- 1 | lazy val root = (project in file(".")) 2 | .settings(basicSettings: _*) 3 | .settings(dependencySettings: _*) 4 | 5 | lazy val basicSettings = Seq( 6 | name := "config-annotation", 7 | organization := "com.wacai", 8 | homepage := Some(url("https://github.com/hanabix/config-annotation")), 9 | licenses := List( 10 | "Apache-2.0" -> url("http://www.apache.org/licenses/LICENSE-2.0") 11 | ), 12 | developers := List( 13 | Developer( 14 | "zhongl", 15 | "Lunfu Zhong", 16 | "zhong.lunfu@gmail.com", 17 | url("https://github.com/zhongl") 18 | ) 19 | ), 20 | 21 | scalaVersion := "2.13.12", 22 | scalacOptions += "-Ymacro-annotations", 23 | scalacOptions += "-encoding", 24 | scalacOptions += "utf8", 25 | scalacOptions += "-feature", 26 | scalacOptions += "-unchecked", 27 | scalacOptions += "-deprecation", 28 | scalacOptions += "-Xmacro-settings:conf.output.dir=src/test/resources", 29 | scalacOptions += "-language:_" 30 | ) 31 | 32 | lazy val dependencySettings = Seq( 33 | libraryDependencies += "org.scala-lang" % "scala-reflect" % scalaVersion.value, 34 | libraryDependencies += "com.typesafe" % "config" % "1.4.3", 35 | libraryDependencies += "org.scalatest" %% "scalatest" % "3.2.17" % "test" 36 | ) 37 | -------------------------------------------------------------------------------- /notes/0.3.0.markdown: -------------------------------------------------------------------------------- 1 | - Define configuration in [config](https://github.com/typesafehub/config)-style DSL; 2 | - Generate `xxx.conf` file after refactoring definition automatically; 3 | - Bind values from the `xxx.conf` by macro annotation automatically. 4 | 5 | -------------------------------------------------------------------------------- /notes/0.3.1.markdown: -------------------------------------------------------------------------------- 1 | - Support substitution in config-style trait; 2 | - Supprot list value. 3 | 4 | -------------------------------------------------------------------------------- /notes/0.3.2.markdown: -------------------------------------------------------------------------------- 1 | - Quote strings if they contain special characters by @lustefaniak; 2 | - Simple support for Map[String,String] by @lustefaniak. 3 | 4 | -------------------------------------------------------------------------------- /notes/0.3.4.markdown: -------------------------------------------------------------------------------- 1 | - Config val name support dot and minus by @eagoo ; 2 | 3 | -------------------------------------------------------------------------------- /notes/0.3.5.markdown: -------------------------------------------------------------------------------- 1 | - Work with empty strings by @lustefaniak ; 2 | 3 | -------------------------------------------------------------------------------- /notes/0.3.6.markdown: -------------------------------------------------------------------------------- 1 | - Support scala 2.12 cross build by @lustefaniak ; 2 | 3 | -------------------------------------------------------------------------------- /notes/0.3.7.markdown: -------------------------------------------------------------------------------- 1 | - Support scala 2.13 cross build by @tzeman77 ; 2 | 3 | -------------------------------------------------------------------------------- /notes/about.markdown: -------------------------------------------------------------------------------- 1 | [config-annotation](https://github.com/wacai/config-annotation) is a refactor-friendly macro annotation for scala developer to use [config](https://github.com/typesafehub/config) in a better way. 2 | -------------------------------------------------------------------------------- /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version = 1.9.7 2 | -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | 2 | addSbtPlugin("org.scoverage" % "sbt-scoverage" % "2.0.9") 3 | 4 | addSbtPlugin("org.scoverage" % "sbt-coveralls" % "1.3.11") 5 | 6 | addSbtPlugin("com.github.sbt" % "sbt-ci-release" % "1.5.12") 7 | -------------------------------------------------------------------------------- /src/main/scala/com/wacai/config/annotation/Configurable.scala: -------------------------------------------------------------------------------- 1 | package com.wacai.config.annotation 2 | 3 | import com.typesafe.config.Config 4 | 5 | trait Configurable { 6 | def config: Config 7 | } 8 | -------------------------------------------------------------------------------- /src/main/scala/com/wacai/config/annotation/Macro.scala: -------------------------------------------------------------------------------- 1 | package com.wacai.config.annotation 2 | 3 | import java.io.{File, PrintWriter} 4 | 5 | import com.typesafe.config.ConfigFactory 6 | 7 | import annotation.switch 8 | import concurrent.duration._ 9 | import reflect.macros.whitebox 10 | 11 | class Macro(val c: whitebox.Context) { 12 | 13 | import Macro._ 14 | import c.universe._ 15 | import Flag._ 16 | 17 | lazy val outputDir = { 18 | val f = new File(c.settings 19 | .find(_.startsWith(OutputDirSettings)) 20 | .map(_.substring(OutputDirSettings.length)) 21 | .getOrElse(DefaultOutputDir)) 22 | 23 | if (!f.exists()) f.mkdirs() 24 | 25 | f 26 | } 27 | 28 | def impl(annottees: c.Expr[Any]*): c.Expr[Any] = { 29 | 30 | val result = annottees.map(_.tree).toList match { 31 | case (ClassDef(mods, name, a, Template(parents, s, body))) :: Nil if mods.hasFlag(DEFAULTPARAM | TRAIT) => 32 | 33 | implicit val out = new PrintWriter(new File(outputDir, s"$name.conf"), "UTF-8") 34 | 35 | try { 36 | node(0)(s"$name") { 37 | val imports = q"import scala.jdk.CollectionConverters._" 38 | 39 | val conf = if (parents exists configurable) { 40 | q"private val _config = config" 41 | } else { 42 | q"private val _config = ${reify(CONFIG).tree}" 43 | } 44 | 45 | ClassDef(mods, name, a, Template(parents, s, imports :: conf :: body.map { 46 | case Initialized(vd) => generate(vd, s"$name", 1) 47 | case t => t 48 | })) 49 | } 50 | 51 | } finally out.close() 52 | 53 | 54 | case _ => 55 | c.abort(c.enclosingPosition, "Annotation is only supported on trait") 56 | } 57 | 58 | c.Expr[Any](result) 59 | } 60 | 61 | 62 | lazy val seconds = reify(SECONDS) tree 63 | 64 | def tpe(t: Tree): Type = c.typecheck(t).tpe 65 | 66 | def is[T: TypeTag](t: Type) = t <:< typeOf[T] 67 | 68 | def duration(t: Tree) = q"scala.concurrent.duration.Duration($t, $seconds)" 69 | 70 | def configurable(t: Tree): Boolean = is[Configurable](tpe(q"0.asInstanceOf[$t]")) 71 | 72 | def generate(cd: ClassDef, owner: String, level: Int)(implicit out: PrintWriter): ClassDef = cd match { 73 | case ClassDef(m, name, a, Template(p, s, body)) => 74 | ClassDef(m, name, a, Template(p, s, body map { 75 | case Initialized(vd) => generate(vd, s"$owner", level + 1) 76 | case d => d 77 | })) 78 | 79 | case _ => 80 | c.abort(cd.pos, "A anonymous class definition should be here") 81 | 82 | } 83 | 84 | def generate(vd: ValDef, owner: String, level: Int)(implicit out: PrintWriter): ValDef = vd match { 85 | case ValDef(mods, _, _, _) if mods.hasFlag(DEFERRED) => 86 | c.abort(vd.pos, "value should be initialized") 87 | 88 | case ValDef(mods, _, _, _) if mods.hasFlag(MUTABLE) => 89 | c.abort(vd.pos, "var should be val") 90 | 91 | case ValDef(mods, name, tpt, Block((cd: ClassDef) :: Nil, expr)) => 92 | val owner4dot = owner.replaceAll("\\$u002E","\\.") 93 | val name4dot = name.toString.replaceAll("\\$u002E","\\.") 94 | node(level)(s"$name4dot") { 95 | ValDef(mods, name, tpt, Block(generate(cd, s"$owner4dot.$name4dot", level) :: Nil, expr)) 96 | } 97 | 98 | case ValDef(mods, name, tpt, rhs) => 99 | try { 100 | val e = c.eval(c.Expr[Any](Block(q"import scala.concurrent.duration._" :: Nil, rhs))) 101 | val t = c.typecheck(rhs).tpe 102 | 103 | val name4tricks = name.toString.replaceAll("\\$minus","\\-").replaceAll("\\$u002E","\\.") 104 | val owner4dot = owner.replaceAll("\\$u002E","\\.") 105 | leaf(level)(s"$name4tricks = ${value(t, e)}") 106 | ValDef(mods, name, tpt, get(t, s"$owner4dot.$name4tricks")) 107 | } catch { 108 | case e: IllegalStateException => c.abort(vd.pos, e.getMessage) 109 | case _: Throwable => vd 110 | } 111 | 112 | case _ => 113 | c.abort(vd.pos, "Unexpect value definition") 114 | 115 | } 116 | 117 | def value(t: Type, a: Any): String = { 118 | 119 | t match { 120 | case _ if is[Long](t) => bytes(a.asInstanceOf[Long]) 121 | case _ if is[Duration](t) => time(a.asInstanceOf[Duration]) 122 | case _ if is[List[Long]](t) => a.asInstanceOf[List[Long]].map(bytes).asArray 123 | case _ if is[List[Duration]](t) => a.asInstanceOf[List[Duration]].map(time).asArray 124 | case _ if is[List[_]](t) => a.asInstanceOf[List[_]].map(safeString).asArray 125 | case _ if is[Map[_, _]](t) => a.asInstanceOf[Map[_, _]] 126 | .map { case (k, v) => s"$k:${safeString(v)}" } 127 | .asObject 128 | case _ => safeString(a) 129 | } 130 | } 131 | 132 | def get(t: Type, path: String): Tree = t match { 133 | case _ if is[Boolean](t) => q"_config.getBoolean($path)" 134 | case _ if is[Int](t) => q"_config.getInt($path)" 135 | case _ if is[Long](t) => q"_config.getBytes($path)" 136 | case _ if is[String](t) => q"_config.getString($path)" 137 | case _ if is[Double](t) => q"_config.getDouble($path)" 138 | case _ if is[Duration](t) => duration(q"_config.getDuration($path, $seconds)") 139 | case _ if is[List[Boolean]](t) => q"_config.getBooleanList($path).asScala.toList" 140 | case _ if is[List[Int]](t) => q"_config.getIntList($path).asScala.toList" 141 | case _ if is[List[Long]](t) => q"_config.getBytesList($path).asScala.toList" 142 | case _ if is[List[String]](t) => q"_config.getStringList($path).asScala.toList" 143 | case _ if is[List[Double]](t) => q"_config.getDoubleList($path).asScala.toList" 144 | case _ if is[List[Duration]](t) => q"_config.getDurationList($path, $seconds).asScala.toList.map {l => ${duration(q"l")} }" 145 | case _ if is[Map[String, String]](t) => q"_config.getObject($path).asScala.map{case(x,y)=>x.toString -> y.unwrapped.toString}.toMap[String,String]" 146 | case _ => throw new IllegalStateException(s"Unsupported type: $t") 147 | } 148 | 149 | object Initialized { 150 | def unapply(t: Tree): Option[ValDef] = t match { 151 | case v @ ValDef(mods, _, _, _) if !mods.hasFlag(DEFERRED) => Some(v) 152 | case _ => None 153 | } 154 | } 155 | 156 | } 157 | 158 | object Macro { 159 | 160 | val DefaultOutputDir = "src/main/resources" 161 | val OutputDirSettings = "conf.output.dir=" 162 | 163 | lazy val CONFIG = ConfigFactory.load() 164 | 165 | private val TAB = " " 166 | 167 | def node[T](level: Int)(name: String)(f: => T)(implicit out: PrintWriter) = { 168 | out println s"${TAB * level}$name {" 169 | val r = f 170 | out println s"${TAB * level}}" 171 | r 172 | } 173 | 174 | def leaf(level: Int)(expr: String)(implicit out: PrintWriter) = { 175 | out.println(s"${TAB * level}$expr") 176 | } 177 | 178 | def bytes(l: Long): String = l match { 179 | case _ if l < 1024 || l % 1024 > 0 => s"${l}B" 180 | case _ if l >= 1024 && l < 1024 * 1024 => s"${l / 1024}K" 181 | case _ if l >= 1024 * 1024 && l < 1024 * 1024 * 1024 => s"${l / (1024 * 1024)}M" 182 | case _ => s"${l / (1024 * 1024 * 1024)}G" 183 | } 184 | 185 | def time(d: Duration): String = d.unit match { 186 | case NANOSECONDS => s"${d._1}ns" 187 | case MICROSECONDS => s"${d._1}us" 188 | case MILLISECONDS => s"${d._1}ms" 189 | case SECONDS => s"${d._1}s" 190 | case MINUTES => s"${d._1}m" 191 | case HOURS => s"${d._1}h" 192 | case DAYS => s"${d._1}d" 193 | } 194 | 195 | implicit class MkString(t: IterableOnce[_]) { 196 | def asArray = t.iterator.mkString("[", ", ", "]") 197 | 198 | def asObject = t.iterator.mkString("{", ", ", "}") 199 | } 200 | 201 | def safeString(input: Any) = { 202 | def quotationNeeded(s: String) = s.isEmpty || List( 203 | '$', '"', '{', '}', '[', ']', 204 | ':', '=', ',', '+', '#', '`', 205 | '^', '?', '!', '@', '*', '&', '\\' 206 | ).exists {s.indexOf(_) != -1} 207 | 208 | def renderJsonString(s: String): String = { 209 | val sb: StringBuilder = new StringBuilder 210 | sb.append('"') 211 | for (c <- s) { 212 | (c: @switch) match { 213 | case '"' => 214 | sb.append("\\\"") 215 | case '\\' => 216 | sb.append("\\\\") 217 | case '\n' => 218 | sb.append("\\n") 219 | case '\b' => 220 | sb.append("\\b") 221 | case '\f' => 222 | sb.append("\\f") 223 | case '\r' => 224 | sb.append("\\r") 225 | case '\t' => 226 | sb.append("\\t") 227 | case _ => 228 | if (Character.isISOControl(c)) sb.append("\\u%04x".format(c.toInt)) 229 | else sb.append(c) 230 | } 231 | } 232 | sb.append('"') 233 | sb.toString() 234 | } 235 | 236 | val s = input.toString 237 | if (quotationNeeded(s)) renderJsonString(s) else s 238 | } 239 | 240 | } 241 | -------------------------------------------------------------------------------- /src/main/scala/com/wacai/config/annotation/conf.scala: -------------------------------------------------------------------------------- 1 | package com.wacai.config.annotation 2 | 3 | import annotation.StaticAnnotation 4 | 5 | class conf extends StaticAnnotation { 6 | def macroTransform(annottees: Any*): Any = macro Macro.impl 7 | } 8 | -------------------------------------------------------------------------------- /src/test/resources/application.conf: -------------------------------------------------------------------------------- 1 | include "common.conf" 2 | include "list.conf" 3 | include "specialchars.conf" 4 | include "maps.conf" 5 | include "backTicksKeys.conf" 6 | -------------------------------------------------------------------------------- /src/test/resources/backTicksKeys.conf: -------------------------------------------------------------------------------- 1 | backTicksKeys { 2 | key-with-mid-line = dash 3 | netty.tcp { 4 | dotkey = dot value 5 | level3.key = level3value 6 | level3 { 7 | level4.key = level4value 8 | } 9 | } 10 | netty.port = port 11 | } 12 | -------------------------------------------------------------------------------- /src/test/resources/common.conf: -------------------------------------------------------------------------------- 1 | common { 2 | sub = 128 3 | } 4 | -------------------------------------------------------------------------------- /src/test/resources/concrete.conf: -------------------------------------------------------------------------------- 1 | concrete { 2 | } 3 | -------------------------------------------------------------------------------- /src/test/resources/kafka.conf: -------------------------------------------------------------------------------- 1 | kafka { 2 | server { 3 | host = localhost 4 | port = 9092 5 | } 6 | socket { 7 | timeout = 5s 8 | buffer = 64K 9 | } 10 | client = id 11 | debug = false 12 | delays = 2s 13 | } 14 | -------------------------------------------------------------------------------- /src/test/resources/list.conf: -------------------------------------------------------------------------------- 1 | list { 2 | i = [1, 2] 3 | b = [true, false] 4 | d = [1.1, 2.2] 5 | l = [512B, 3K] 6 | t = [1s, 2m] 7 | s = [a, b] 8 | e = ["", a, "", c, ""] 9 | } 10 | -------------------------------------------------------------------------------- /src/test/resources/maps.conf: -------------------------------------------------------------------------------- 1 | maps { 2 | ab = {a:b} 3 | abcd = {a:b, c:d} 4 | } 5 | -------------------------------------------------------------------------------- /src/test/resources/specialchars.conf: -------------------------------------------------------------------------------- 1 | specialchars { 2 | url = "http://localhost:8080/" 3 | urls = ["http://localhost:8080/"] 4 | all = ["$", "\"", "{", "}", "[", "]", ":", "=", ",", "+", "#", "`", "^", "?", "!", "@", "*", "&", "\\\\"] 5 | empty = "" 6 | } 7 | -------------------------------------------------------------------------------- /src/test/scala/com/wacai/config/annotation/ConfAnnotationSpec.scala: -------------------------------------------------------------------------------- 1 | package com.wacai.config.annotation 2 | 3 | import com.typesafe.config.ConfigFactory 4 | import org.scalatest._ 5 | import scala.concurrent.duration._ 6 | import org.scalatest.flatspec.AnyFlatSpec 7 | import org.scalatest.matchers.should.Matchers 8 | 9 | 10 | class ConfAnnotationSpec extends AnyFlatSpec with Matchers { 11 | "@conf annotated trait" should "get value" in { 12 | val conf = new kafka {} 13 | 14 | conf.server.host shouldBe "wacai.com" 15 | conf.server.port shouldBe 12306 16 | conf.socket.timeout shouldBe 3.seconds 17 | conf.socket.buffer shouldBe 1024 * 1024L 18 | conf.client shouldBe "wacai" 19 | conf.debug shouldBe true 20 | conf.delays shouldBe 2.seconds 21 | } 22 | 23 | it should "save and load strings with special characters" in { 24 | val conf = new specialchars {} 25 | conf.url shouldBe "http://localhost:8080/" 26 | conf.urls shouldBe List("http://localhost:8080/") 27 | conf.all shouldBe List("$", "\"", "{", "}", "[", "]", ":", "=", ",", "+", "#", "`", "^", "?", "!", "@", "*", "&", "\\\\") 28 | conf.empty shouldBe "" 29 | } 30 | 31 | it should "get substitution value" in { 32 | (new concrete {} value) shouldBe 128 33 | } 34 | 35 | it should "get list value" in { 36 | val conf = new list {} 37 | conf.i shouldBe List(1, 2) 38 | conf.b shouldBe List(true, false) 39 | conf.d shouldBe List(1.1, 2.2) 40 | conf.l shouldBe List(512L, 1024 * 3L) 41 | conf.t shouldBe List(1 second, 2 minutes) 42 | conf.s shouldBe List("a", "b") 43 | conf.e shouldBe List("", "a", "", "c", "") 44 | } 45 | 46 | it should "know how to save and load Map[String,String]" in { 47 | val conf = new maps{} 48 | conf.ab shouldBe Map("a" -> "b") 49 | conf.abcd shouldBe Map[String,String]("a" -> "b", "c" -> "d") 50 | } 51 | 52 | it should "save and load strings with special keys use scala back ticks literal identifier" in { 53 | val conf = new backTicksKeys {} 54 | conf.`key-with-mid-line` shouldBe "dash" 55 | conf.`netty.tcp`.dotkey shouldBe "dot value" 56 | conf.`netty.port` shouldBe "port" 57 | conf.`netty.tcp`.level3.`level4.key` shouldBe "level4value" 58 | conf.`netty.tcp`.`level3.key` shouldBe "level3value" 59 | } 60 | } 61 | 62 | @conf trait kafka extends Configurable { 63 | 64 | val server = new { 65 | val host = "localhost" 66 | val port = 9092 67 | } 68 | 69 | val socket = new { 70 | val timeout = 5.seconds 71 | val buffer = 1024 * 64L 72 | } 73 | 74 | val client = "id" 75 | 76 | val debug = false 77 | 78 | val delays = 2.seconds 79 | 80 | def config = ConfigFactory.parseString( 81 | """| 82 | |kafka { 83 | | server { 84 | | host: wacai.com 85 | | port: 12306 86 | | } 87 | | 88 | | socket { 89 | | timeout = 3s 90 | | buffer = 1M 91 | | } 92 | | 93 | | client: wacai 94 | | 95 | | debug:yes 96 | | 97 | | delays:2s 98 | |} 99 | """.stripMargin) 100 | } 101 | 102 | @conf trait common { 103 | val sub = 128 104 | } 105 | 106 | @conf trait concrete extends common { 107 | val value = sub 108 | } 109 | 110 | @conf trait specialchars { 111 | val url = "http://localhost:8080/" 112 | val urls = List("http://localhost:8080/") 113 | val all = List("$", "\"", "{", "}", "[", "]", ":", "=", ",", "+", "#", "`", "^", "?", "!", "@", "*", "&", "\\\\") 114 | val empty = "" 115 | } 116 | 117 | @conf trait list { 118 | val i = List(1, 2) 119 | val b = List(true, false) 120 | val d = List(1.1, 2.2) 121 | val l = List(512L, 1024 * 3L) 122 | val t = List(1.second, 2.minutes) 123 | val s = List("a", "b") 124 | val e = List("", "a", "", "c", "") 125 | } 126 | 127 | @conf trait maps { 128 | val ab = Map[String,String]("a" -> "b") 129 | val abcd = Map[String,String]("a" -> "b", "c" -> "d") 130 | } 131 | 132 | @conf trait backTicksKeys { 133 | val `key-with-mid-line` = "dash" 134 | val `netty.tcp` = new { 135 | val dotkey = "dot value" 136 | val `level3.key` = "level3value" 137 | val level3 = new { 138 | val `level4.key` = "level4value" 139 | } 140 | } 141 | val `netty.port` = "port" 142 | } -------------------------------------------------------------------------------- /src/test/scala/com/wacai/config/annotation/UnitGenSpec.scala: -------------------------------------------------------------------------------- 1 | package com.wacai.config.annotation 2 | 3 | import scala.concurrent.duration._ 4 | import org.scalatest.flatspec.AnyFlatSpec 5 | import org.scalatest.matchers.should.Matchers 6 | 7 | class UnitGenSpec extends AnyFlatSpec with Matchers { 8 | "duration" should "get right unit" in { 9 | Macro.time(1 nanosecond) shouldBe "1ns" 10 | Macro.time(1 microsecond) shouldBe "1us" 11 | Macro.time(1 millisecond) shouldBe "1ms" 12 | Macro.time(1 second) shouldBe "1s" 13 | Macro.time(1 minute) shouldBe "1m" 14 | Macro.time(1 hour) shouldBe "1h" 15 | Macro.time(1 day) shouldBe "1d" 16 | } 17 | 18 | "bytes" should "get right unit" in { 19 | Macro.bytes(1023L) shouldBe "1023B" 20 | Macro.bytes(1025L) shouldBe "1025B" 21 | Macro.bytes(1024L) shouldBe "1K" 22 | Macro.bytes(1024L * 1024) shouldBe "1M" 23 | Macro.bytes(1024L * 1024 * 1024) shouldBe "1G" 24 | } 25 | } 26 | --------------------------------------------------------------------------------