Skip to content

Commit 82efa62

Browse files
committed
Remove the complication of treating case class and string
1 parent 638ba37 commit 82efa62

File tree

9 files changed

+82
-137
lines changed

9 files changed

+82
-137
lines changed

docs/src/main/tut/examples.md

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,7 @@ res2: String = The a, b and c are: ghi, xyz, C@3aaeb14
3131

3232
// safeStr interpolation
3333
scala> safeStr"The a, b and c are: ${a}, ${b}, ${c}"
34-
<console>:24: error: The provided type is neither a string nor a case-class. Consider converting it to strings using <value>.asStr.
35-
safeStr"The a, b and c are: ${a}, ${b}, ${c}"
34+
<console>:18: error: unable to find a safe instance for class C. Make sure it is a case class or a type that has safe instance.
3635
^
3736
scala> safeStr"a and b: ${a}, ${b}"
3837
res2: com.thaj.safe.string.interpolator.SafeString = SafeString(a and b: ghi, xyz)

docs/src/main/tut/index.md

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -49,11 +49,11 @@ defined class X
4949
scala> val caseClassInstance = X("foo")
5050
caseClassInstance: X = X(foo)
5151

52-
scala> val onlyString: String = "bar"
52+
scala> val string: String = "bar"
5353
onlyString: String = bar
5454

55-
scala> safeStr"Works only if its either a string or a case class instance $caseClassInstance or $onlyString"
56-
res0: com.thaj.safe.string.interpolator.SafeString = SafeString(Works only if its either a string or a case class instance { name: foo } or bar)
55+
scala> safeStr"Works only if all of them has an instance of safe $caseClassInstance or $string"
56+
res0: com.thaj.safe.string.interpolator.SafeString = SafeString(Works only if it all of them has an instance of safe { name : foo } or bar)
5757

5858
scala> class C
5959
defined class C
@@ -62,24 +62,27 @@ scala> val nonCaseClass = new C
6262
nonCaseClass: C = C@7e3131c8
6363

6464
scala> safeStr"Doesn't work if there is a non-case class $nonCaseClass or $onlyString"
65-
<console>:17: error: The provided type is neither a string nor a case-class. Consider converting it to strings using <value>.asStr.
66-
safeStr"Doesn't work if there is a non-case class $nonCaseClass or $onlyString"
65+
<console>:17: error: unable to find a safe instance for class C. Make sure it is a case class or a type that has safe instance.
6766
^
6867
// And don't cheat by `toString`
6968
scala> safeStr"Doesn't work if there is a non-case class ${nonCaseClass.toString} or $onlyString"
70-
<console>:17: error: Identified `toString` being called on the types. Either remove it or use <yourType>.asStr if it has an instance of Safe.
71-
safeStr"Doesn't work if there is a non-case class ${nonCaseClass.toString} or $onlyString"
69+
<console>:17: error: Identified `toString` being called on the types. Make sure the type has a instance of Safe..
7270
^
73-
7471
```
7572

7673
# Concept and example usages.
7774

78-
`safeStr""` is just like `s""` in scala, but it is type safe and _allows only_
75+
`safeStr""` is just like `s""` in scala, but it is type safe and _allows only_ types that has a safe instance.
76+
77+
But don't worry. If you have a case class, the macros in `Safe.scala` will automatically derive it's safe instance
78+
as far as all ofthe individual fields has `Safe` instance which is also defined already in the companion object of `Safe`.
79+
It works for any deep/nested level of case classes.
80+
81+
To sum up,
7982

80-
* **strings**.
81-
* **case classes** which will be converted to json-like string by inspecting all fields, be it deeply nested or not, at compile time.
82-
* and provides consistent way to **hide secrets**.
83+
* Most of the types already has a Safe instance in companion object, hence `Int`, `Double` etc works straight away.
84+
* **case classes** will be converted to json-like string by inspecting all fields, be it deeply nested or not, at compile time.
85+
* `Secret` types will be hidden too. We will see more on these in the below links.
8386

8487
To understand more on the concepts and usages, please go through:
8588

docs/src/main/tut/pretty_print.md

Lines changed: 3 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -17,61 +17,17 @@ s: Xyz = Xyz(Abc(a,b,c),x)
1717

1818
// scala string interpolation
1919
scala> s"The value of xyz is $s"
20-
res0: String = The value of xyz is Xyz(Abc(a,b,c),x)
20+
res5: String = The value of xyz is Xyz(Abc(a,b,c),x)
2121

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

2626
scala> res1.string
27-
res2: The value of xyz is { name: x, abc: {x : a, y : b, z : c} }
27+
res7: String = The value of xyz is { abc : { a : a, b : b, c : c }, name : x }
2828
```
2929

3030

3131
This works for any level of **deep nested structure of case class**. This is done with the support of macro materializer in `Safe.scala`.
3232
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.
3333
Also, if any part of case classes has `Secret`s in it, the value will be hidden. More on this in `Secret / Password` section
34-
35-
## Why case-classes ?
36-
37-
While the purpose of `safe-string-interpolation` is to make sure you are passing only Strings to `safeStr`, it works for case-class instances as well.
38-
There is a reason for this.
39-
40-
Delegating the job of stringifying a case class to the user has always been an infamous problem and it kills the user's time.
41-
The `safe-string-interpolation` takes up this tedious job, and macros under the hood converts it to a readable string, while hiding `Secret` types.
42-
43-
PS: In the next release, we may ask the user to do `.asStr` explicitly on case classes as well. This will bring in more consistency.
44-
45-
```scala
46-
47-
@ import com.thaj.safe.string.interpolator.SafeString._
48-
import com.thaj.safe.string.interpolator.SafeString._
49-
50-
@ case class Test(list: List[String])
51-
defined class Test
52-
53-
@ val test = Test(List("foo", "bar"))
54-
test: Test = Test(List("foo", "bar"))
55-
56-
@ safeStr"test will work $test"
57-
res4: com.thaj.safe.string.interpolator.SafeString = SafeString("test will work { list: foo,bar }")
58-
59-
@ val test = List("foo", "bar")
60-
test: List[String] = List("foo", "bar")
61-
62-
@ safeStr"test will not work $test"
63-
cmd6.sc:1: The provided type isn't a string nor it's a case class, or you might have tried a `toString` on non-strings !
64-
val res6 = safeStr"test will not work $test"
65-
^
66-
Compilation Failed
67-
68-
@ safeStr"test will work by telling the compiler, yes, it is a string ${test.toString}"
69-
cmd6.sc:1: Identified `toString` being called on the types. Either remove it or use <yourType>.asStr if it has an instance of Safe.
70-
val res6 = safeStr"test will work by telling the compiler, yes, it is a string ${test.toString}"
71-
^
72-
Compilation Failed
73-
74-
@ safeStr"test will work by telling the compiler, yes, it is a string ${test.asStr}"
75-
res6: com.thaj.safe.string.interpolator.SafeString = SafeString("test will work by telling the compiler, yes, it is a string foo,bar")
76-
77-
```

docs/src/main/tut/secrets.md

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,26 +28,27 @@ scala> s"The db connection is $dbConn"
2828
res2: String = The db connection is DbConn(driverstring,Secret(adifficultpassword))
2929

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

3433
```
3534

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

3837
## Your own secret ?
3938

40-
If you don't want to use `interpolation.Secret` data type and need to use your own, then define `Safe` instance for it.
39+
If you don't want to use `com.thaj.safe.string.interpolator.Secret` data type and need to use your own, then define `Safe` instance for it.
4140

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

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

47-
val conn = DbConnection("posgr", MySecret("this will be hidden"))
46+
case class DbConn(driver: String, password: MySecret)
47+
48+
val conn = DbConn("posgr", MySecret("this will be hidden"))
4849

4950

5051
scala> safeStr"the db is $conn"
51-
res1: com.thaj.safe.string.interpolator.SafeString = SafeString(the db is { password: ****, name: posgr })
52+
res3: com.thaj.safe.string.interpolator.SafeString = SafeString(the db is { driver : posgr, password : **** })
5253

5354
```

macros/src/main/scala/com/thaj/safe/string/interpolator/Safe.scala

Lines changed: 37 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
package com.thaj.safe.string.interpolator
22

3-
import scalaz.{@@, NonEmptyList}
3+
import scalaz.{@@, IList, NonEmptyList}
44

55
import scala.language.experimental.macros
66
import scala.reflect.macros.blackbox
@@ -29,10 +29,10 @@ object Safe {
2929
implicit val safeLong: Safe[Long] =
3030
_.toString
3131

32-
implicit val safeFloat: Safe[Float] =
32+
implicit val safeDouble: Safe[Double] =
3333
_.toString
3434

35-
implicit val safeDouble: Safe[Double] =
35+
implicit val safeFloat: Safe[Float] =
3636
_.toString
3737

3838
implicit val safeChar: Safe[Char] =
@@ -50,6 +50,9 @@ object Safe {
5050
implicit def safeNonEmptyList[A: Safe]: Safe[NonEmptyList[A]] =
5151
_.map(t => Safe[A].value(t)).list.toList.mkString(",")
5252

53+
implicit def safeIList[A : Safe]: Safe[IList[A]] =
54+
l => Safe[List[A]].value(l.toList)
55+
5356
implicit def safeTagged[A: Safe, T]: Safe[A @@ T] =
5457
a => Safe[A].value(scalaz.Tag.unwrap(a))
5558

@@ -65,24 +68,41 @@ object Safe {
6568
def materializeSafe[T: c.WeakTypeTag](c: blackbox.Context): c.Expr[Safe[T]] = {
6669
import c.universe._
6770
val tpe = weakTypeOf[T]
68-
val fields = tpe.decls.collectFirst {
69-
case m: MethodSymbol if m.isPrimaryConstructor m
70-
}.getOrElse(c.abort(NoPosition, s"Unable to find a safe instance for $tpe. Consider creating one manually."))
71-
.paramLists.headOption.getOrElse(c.abort(NoPosition, s"Unable to find a safe instance for $tpe. Consider creating one manually."))
7271

73-
val str =
74-
fields.foldLeft(Set[(TermName, c.universe.Tree)]()) { (str, field)
75-
val tag = c.WeakTypeTag(field.typeSignature)
72+
val tag = c.WeakTypeTag(tpe)
73+
val symbol = tag.tpe.typeSymbol
7674

77-
val fieldName = field.name.toTermName
78-
str ++ Set((fieldName, q""" com.thaj.safe.string.interpolator.Safe[$tag]"""))
75+
if (symbol.isClass && symbol.asClass.isCaseClass) {
76+
val fields = tpe.decls.collectFirst {
77+
case m: MethodSymbol if m.isPrimaryConstructor m
7978
}
80-
81-
val res =
82-
q"""new com.thaj.safe.string.interpolator.Safe[$tpe] {
83-
override def value(a: $tpe): String = "{ " + ${str.map{case(x, y) => q""" ${x.toString} + " : " + $y.value(a.$x) """ }}.mkString(", ") + " }"
79+
.getOrElse(
80+
c.abort(NoPosition, s"Unable to find a safe instance for ${tpe.typeSymbol}. Consider creating one manually.")
81+
)
82+
.paramLists.headOption
83+
.getOrElse(c.abort(NoPosition, s"Unable to find a safe instance for $tpe. Consider creating one manually."))
84+
85+
val str =
86+
fields.foldLeft(Set[(TermName, c.universe.Tree)]()) { (str, field)
87+
val tag = c.WeakTypeTag(field.typeSignature)
88+
89+
val fieldName = field.name.toTermName
90+
str ++ Set((fieldName, q""" com.thaj.safe.string.interpolator.Safe[$tag]"""))
91+
}
92+
93+
val res =
94+
q"""new com.thaj.safe.string.interpolator.Safe[$tpe] {
95+
override def value(a: $tpe): String = "{ " + ${
96+
str
97+
.map { case (x, y) => q""" ${x.toString} + " : " + $y.value(a.$x) """ }
98+
}.mkString(", ") + " }"
8499
}"""
85100

86-
c.Expr[Safe[T]] { res }
101+
c.Expr[Safe[T]] {
102+
res
103+
}
104+
} else {
105+
c.abort(NoPosition, s"unable to find a safe instance for ${tpe.typeSymbol}. Make sure it is a case class or a type that has safe instance.")
106+
}
87107
}
88108
}

macros/src/main/scala/com/thaj/safe/string/interpolator/SafeString.scala

Lines changed: 6 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -22,63 +22,29 @@ object SafeString {
2222
}
2323

2424
object Macro {
25-
// Not public, just for macros
26-
def jsonLike(list: Set[String]) =
27-
s"{ ${list.mkString(", ")} }"
28-
2925
def impl(c: blackbox.Context)(args: c.Expr[Any]*): c.Expr[SafeString] = {
3026
import c.universe.{Name => _, _}
3127

32-
object CaseClassFieldAndName {
33-
def unapply(sym: TermSymbol): Option[(TermName, Type)] = {
34-
if (sym.isCaseAccessor && sym.isVal)
35-
Some((TermName(sym.name.toString.trim), sym.typeSignature))
36-
else
37-
None
38-
}
39-
}
40-
4128
c.prefix.tree match {
4229

4330
case Apply(_, List(Apply(_, partz))) =>
4431
val parts: Seq[String] = partz map { case Literal(Constant(const: String)) => const }
4532

4633
val res: c.universe.Tree =
47-
args.toList.foldLeft(q"""StringContext.apply(..${parts})""")({ (acc, t) => {
48-
34+
args.toList.foldLeft(q"""StringContext.apply(..$parts)""")({ (acc, t) => {
4935
val nextElement = t.tree
50-
5136
val tag = c.WeakTypeTag(nextElement.tpe)
52-
val symbol = tag.tpe.typeSymbol
5337

5438
if(nextElement.toString().contains(".toString"))
55-
c.abort(t.tree.pos, s"Identified `toString` being called on the types. Either remove it or use <yourType>.asStr if it has an instance of Safe.")
39+
c.abort(t.tree.pos, s"Identified `toString` being called on the types. Make sure the type has a instance of Safe.")
5640

57-
if (!(tag.tpe =:= typeOf[String]) && symbol.isClass && symbol.asClass.isCaseClass) {
58-
val r: Set[c.universe.Tree] =
59-
nextElement.tpe.members.collect {
60-
case CaseClassFieldAndName(nme, typ) =>
61-
q"""com.thaj.safe.string.interpolator.Field(${nme.toString}, $nextElement.$nme.asStr)"""
62-
}.toSet
63-
64-
val field = q"""com.thaj.safe.string.interpolator.SafeString.Macro.jsonLike($r.map(_.toString))"""
65-
66-
acc match {
67-
case q"""StringContext.apply(..$raw).s(..$previousElements)""" => q"""StringContext.apply(..$raw).s(($previousElements :+ ..${field}) :_*)"""
68-
case _ => q"""${acc}.s(..$field)"""
69-
}
70-
}
41+
val field = q"""com.thaj.safe.string.interpolator.Safe[${tag.tpe}].value($nextElement)"""
7142

72-
else if (tag.tpe =:= typeOf[String]) {
7343
acc match {
74-
case q"""StringContext.apply(..$raw).s(..$previousElements)""" => q"""StringContext.apply(..$raw).s(($previousElements :+ $nextElement) :_*)"""
75-
case _ => q"""${acc}.s($nextElement)"""
44+
case q"""StringContext.apply(..$raw).s(..$previousElements)""" => q"""StringContext.apply(..$raw).s(($previousElements :+ ..$field) :_*)"""
45+
case _ => q"""$acc.s(..$field)"""
7646
}
77-
} else {
78-
c.abort(t.tree.pos, "The provided type is neither a string nor a case-class. Consider converting it to strings using <value>.asStr.")
79-
}
80-
}
81-
})
47+
}})
8248

8349
res match {
8450
case q"""StringContext.apply(..$raw).s(..$previousElements)""" => c.Expr(q"""com.thaj.safe.string.interpolator.SafeString($res)""")

project/DocSupport.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ object DocSupport {
2828
micrositeHighlightTheme := "atom-one-light",
2929
micrositeGithubRepo := "safe-string-interpolation",
3030
micrositeHomepage := "https://afsalthaj.github.io/safe-string-interpolation",
31-
micrositeBaseUrl := "/safe-string-interpolation",
31+
micrositeBaseUrl := "iagcl/safe-string-interpolation",
3232
micrositeGithubOwner := "afsalthaj",
3333
micrositeGithubRepo := "safe-string-interpolation",
3434
micrositeGitterChannelUrl := "safe-string-interpolation/community",

0 commit comments

Comments
 (0)