Skip to content

Commit

Permalink
Initial commit on the first version of safe string interpolation
Browse files Browse the repository at this point in the history
  • Loading branch information
afsalthaj committed Dec 27, 2018
0 parents commit b6a97c2
Show file tree
Hide file tree
Showing 15 changed files with 610 additions and 0 deletions.
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
project/
.idea/
*.iml
target/
10 changes: 10 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
language: scala
scala:
- 2.12.8
before_install:
- export PATH=${PATH}:./vendor/bundle
install:
- rvm use 2.2.8 --install --fuzzy
- gem update --system
- gem install sass
- gem install jekyll -v 3.2.1
21 changes: 21 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2018 Afsal Thaj

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
## Typesafe String Interpolation

Checkout the project [website](https://afsalthaj.github.io/safe-string-interpolation/) for all information !
70 changes: 70 additions & 0 deletions build.sbt
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import microsites.CdnDirectives

lazy val docs = project
.enablePlugins(MicrositesPlugin)
.settings(moduleName := "safe-string-interpolation-docs")
.settings(docSettings)
.settings(scalacOptions in Tut ~= (_.filterNot(Set("-Ywarn-unused-import", "-Ywarn-dead-code"))))
.enablePlugins(GhpagesPlugin)

lazy val docSettings = Seq(
micrositeName := "Typesafe Interpolation",
micrositeDescription := "Typesafe Interpolation",
micrositeHighlightTheme := "atom-one-light",
micrositeGithubRepo := "safe-string-interpolation",
micrositeHomepage := "https://afsalthaj.github.io/safe-string-interpolation",
micrositeBaseUrl := "/safe-string-interpolation",
micrositeGithubOwner := "afsalthaj",
micrositeGithubRepo := "safe-string-interpolation",
micrositeGitterChannelUrl := "safe-string-interpolation/community",
micrositePushSiteWith := GHPagesPlugin,
micrositePalette := Map(
"brand-primary" -> "#5B5988",
"brand-secondary" -> "#292E53",
"brand-tertiary" -> "#222749",
"gray-dark" -> "#49494B",
"gray" -> "#7B7B7E",
"gray-light" -> "#E5E5E6",
"gray-lighter" -> "#F4F3F4",
"white-color" -> "#FFFFFF"),
autoAPIMappings := true,
ghpagesNoJekyll := false,
fork in tut := true,
git.remoteRepo := "git@github.com:afsalthaj/safe-string-interpolation.git",
includeFilter in makeSite := "*.html" | "*.css" | "*.png" | "*.jpg" | "*.gif" | "*.js" | "*.swf" | "*.yml" | "*.md"
)

micrositeCDNDirectives := CdnDirectives(
jsList = List(
"https://cdnjs.cloudflare.com/ajax/libs/ag-grid/7.0.2/ag-grid.min.js",
"https://cdnjs.cloudflare.com/ajax/libs/ajaxify/6.6.0/ajaxify.min.js"
),
cssList = List(
"https://cdnjs.cloudflare.com/ajax/libs/animate.css/3.5.2/animate.css",
"https://cdnjs.cloudflare.com/ajax/libs/cssgram/0.1.12/1977.min.css",
"https://cdnjs.cloudflare.com/ajax/libs/cssgram/0.1.12/brooklyn.css"
)
)


micrositeGithubOwner := "afsalthaj"

lazy val macros = (project in file("macros"))
.settings(
name := "macros",
libraryDependencies ++= Seq(
"org.scala-lang" % "scala-reflect" % "2.12.6",
"org.specs2" %% "specs2-scalaz" % "4.2.0"
)
)

lazy val test = (project in file("test"))
.settings(
name := "test",
libraryDependencies ++= Seq(
"org.specs2" %% "specs2-scalacheck" % "4.2.0" % "test",
"org.specs2" %% "specs2-scalaz" % "4.2.0" % "test"
)
).dependsOn(macros)

enablePlugins(MicrositesPlugin)
13 changes: 13 additions & 0 deletions docs/src/main/resources/microsite/data/menu.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
options:

- title: Simple Example
url: examples.html
menu_type: main_menu

- title: Type Safe Pretty Print
url: pretty_print.html
menu_type: main_menu

- title: Secret / Passwords
url: secrets.html
menu_type: main_menu
37 changes: 37 additions & 0 deletions docs/src/main/tut/examples.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
---
layout: docs
title: "Simple Example"
section: "main_menu"
position: 1
---


## Simple Example
```scala

scala> val a: String = "ghi"
a: String = ghi

scala> val b: String = "xyz"
b: String = xyz

scala> class C
defined class C

scala> val c = new C
res1: C = C@54e3ae35

// unsafe interpolation
scala> s"The scala string interpol can be a bit dangerous with your secrets. ${a}, ${b}, ${c}"
res2: String = The scala string interpol can be a bit dangerous with your secrets. ghi, xyz, C@3aaeb14


// safeStr interpolation
scala> safeStr"The scala string interpol can be a bit dangerous with your secrets. ${a}, ${b}, ${c}"
<console>:24: error: The provided type isn't a string nor it's a case class, or you might have tried a `toString` on non-strings!
safeStr"The scala string interpol can be a bit dangerous with your secrets. ${a}, ${b}, ${c}"
^
scala> safeStr"The scala string interpol can be a bit dangerous with your secrets. ${a}, ${b}"
res2: com.thaj.safe.string.interpolator.SafeString = SafeString(The scala string interpol can be a bit dangerous with your secrets. ghi, xyz)

```
57 changes: 57 additions & 0 deletions docs/src/main/tut/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
---
layout: home
title: "Home"
section: "home"
---

Being able to pass anything on to scala string interpolations might have messed up your logs, exposed your secrets, and what not! I know you hate it.
We may also forget stringifying domain objects when using scala string interpolations, but stringifying it manually is a tedious job. Instead we just do `toString` which sometimes can spew out useless string representations.

Bad logs:

```
INFO: The student logged in: @4f9a2c08 // object.toString
INFO: The student logged in: Details(NameParts("john", "stephen"), "efg", "whoknowswhatiswhat"...)
INFO: The student logged in: scala.Map(...)
INFO: The student logged in: Details("name", "libraryPassword!!")
```

Sometimes we rely on `scalaz.Show/cats.Show` instances on companion objects of case classes and then do `s"my domain object is ${domainObject.show}"`, but the creation of `show` instances has never been proved practical in larger applications.

One simplification we did so far is to have automatic show instances (may be using shapeless), and guessing password-like fields and replacing it with "*****".

Hmmm... Not anymore !

# Solution

`safeStr""` is just like `s""` in scala, but it is type safe and _allows only_

* **strings**.
* **case classes** which will be converted to json-like string by inspecting all fields, be it deeply nested or not, at compile time.
* and provides consistent way to **hide secrets**.

Checkout the following:

* [simple examples](https://afsalthaj.github.io/safe-string-interpolation/examples.html)
* [type safe pretty print of case classes](https://afsalthaj.github.io/safe-string-interpolation/pretty_print.html) and
* [secret / password logging](https://afsalthaj.github.io/safe-string-interpolation/secrets.html) to get started !

# Add this in your logs !

`safeStr` returns a `SafeString` which your logger interfaces (an example below) can then accept !


```scala
trait Loggers[F[_], E] {
def info: SafeString => F[Unit]
def error: SafeString => F[Unit]
def debug: SafeString => F[Unit]

```

**Everything here is compile time. !**


----------------------------------------

Our application isn’t resilient if it lacks human-readable logs and doesn’t manage secret variables consistently. Moreover, it said to be maintainable only when it is type driven and possess more compile time behaviour, in this context, be able to fail a build/compile when someone does a `toString` in places where you shouldn’t. Hope it helps !
73 changes: 73 additions & 0 deletions docs/src/main/tut/pretty_print.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
---
layout: docs
title: "Typesafe Pretty Print"
section: "main_menu"
position: 3
---

## Case class Example

```scala

scala> case class Xyz(abc: Abc, name: String)
defined class Xyz

scala> val s = Xyz(Abc("a", "b", "c"), "x")
s: Xyz = Xyz(Abc(a,b,c),x)

// scala string interpolation
scala> s"The value of xyz is $s"
res0: String = The value of xyz is Xyz(Abc(a,b,c),x)

// type safe string interpolation
scala> safeStr"The value of xyz is $s"
res1: com.thaj.safe.string.interpolator.SafeString = SafeString(The value of xyz is { name: x, abc: {x : a, y : b, z : c} })

scala> res1.string
res2: The value of xyz is { name: x, abc: {x : a, y : b, z : c} }
```


This works for any level of **deep nested structure of case class**. This is done with the support of macro materializer in `Safe.scala`.
The main idea here is, if any field in any part of the nested case class isn't safe to be converted to string, it will throw a compile time.
Also, if any part of case classes has `Secret`s in it, the value will be hidden. More on this in `Secret / Password` section

## Type-safety

As mentioned in the simple example, `safeStr` can take **_ONLY_** **strings** and **case class instances** with each field in the case class having a `Safe` instance.

Don't worry, this doesn't mean you need to keep creating `Safe` instances. Macros machinaries under the hood takes care of it.

If something goes wrong, it will be most probably, macros was unable to find an instance for `Safe` for some field in your (may be deeply nested) case class.
So just create an instance for Safe. However, we should try and avoid manual creation of `Safe` instance.

`Safe` instances are already provided for collections, primitives and scalaz.Tag types.

```scala

scala> safeStr"I am going to call a toString on a case class to satisfy compiler ! ${a} : ${dummy.toString}"
<console>:23: error: The provided type isn't a string nor it's a case class, or you might have tried a `toString` on non-strings!
safeStr"I am going to call a toString on a case class to satisfy compiler ! ${a} : ${dummy.toString}"
^

```

safe-string-interpolator hates it when you do `toString` on non-string types. Instead, you can use `yourType.asStr`
and safe-string-interpolator will ensure it is safe to convert it to String.

i.e,

```scala

val a: String = "afsal"
val b: String = "john"
val c: Int = 1

scala> safeStr"The scala string interpol can be a bit dangerous with your secrets. ${a}, ${b}, ${c.toString}"
<console>:24: error: The provided type isn't a string nor it's a case class, or you might have tried a `toString` on non-strings!

scala> safeStr"The scala string interpol can be a bit dangerous with your secrets. ${a}, ${b}, ${c.asStr}"
// Compiles sucess


```
53 changes: 53 additions & 0 deletions docs/src/main/tut/secrets.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
---
layout: docs
title: "Logging Secrets"
section: "main_menu"
position: 4
---

## Logging secrets

As mentioned before, just wrap the secret with Secret.apply.


```scala

scala> import com.thaj.safe.string.interpolator.SafeString._
import com.thaj.safe.string.interpolator.SafeString._

scala> import com.thaj.safe.string.interpolator.Secret
import com.thaj.safe.string.interpolator.Secret

scala> case class DbConn(driver: String, password: Secret)
defined class DbConn

scala> val dbConn = DbConn("driverstring", Secret("adifficultpassword"))
dbConn: DbConn = DbConn(driverstring,Secret(adifficultpassword))

scala> s"The db connection is $dbConn"
res2: String = The db connection is DbConn(driverstring,Secret(adifficultpassword))

scala> safeStr"The db connection is $dbConn"
res3: com.thaj.safe.string.interpolator.SafeString = SafeString(The db connection is { password: ******************, driver: driverstring })


```

**Secrets will be hidden wherever it exists in your nested case class**

## Your own secret ?

If you don't want to use `interpolation.Secret` data type and need to use your own, then define `Safe` instance for it.

```scala
case class MySecret(value: String) extends AnyVal

implicit val safeMySec: Safe[MySecret] = _ => "****"

val conn = DbConnection("posgr", MySecret("this will be hidden"))


scala> safeStr"the db is $conn"
res1: com.thaj.safe.string.interpolator.SafeString = SafeString(the db is { password: ****, name: posgr })

```
Loading

0 comments on commit b6a97c2

Please sign in to comment.