Closed tanishiking closed 2 years ago
Hi,
Thank you for the kind words. They made my day :)
Indeed, there is no option to pass the renderIdentical
parameter when using matchers.
Let's explore some possibilities:
The simplest thing would be to add that parameter to all the matchers. We would lose the ability to call matchers as infix method. I am also not a fan of the resulting syntax.
Another one would be to add it as an implicit parameter as you suggested. I like the name ShowConfig
. However I am reluctant to introduce another implicit parameter. There is already a lot of implicitness in Diffx. Instead, maybe we could add it to the existing one - ConsoleColorConfig
. Then we would need to change its name. How about calling it ShowConfig
?
Both above options allow us to control how the whole hierarchy of DiffResults
will be printed. Could we do better?
Ideally we would have another type class to describe how the diffs for different types should be printed.
trait ShowConfig[T] {
def show(r: DiffResult) : String
}
Such type-class could be a part of Diff
and would be pass to DiffResult
for specific types.
On the other hand we could have a default instance of that type for every T
which would provide us the old behavior.
The downside of that is that we will have to do a pattern matching inside and handle all the different types of DiffResult
despite being interested in only one of them.
Instead we could define ShowConfig
for every main type of the DiffResult
hierarchy:
It might be good to define also DiffResultSeq
.
So we would end up with
ShowConfigObject
ShowConfigMap
ShowConfigSet
ShowConfgSeq
Each of them would contain a definition of a default generic instance for every T
/(K,V)
and have corresponding methods.
For example ShowConfigObject
could look like:
trait ShowConfigObject[T] {
def show(result: DiffObjectResult) : String
}
object ShowConfigObject {
def default[T] : ShowConfigObject = new ShowConfigObject[T] {
def show(result: DiffObjectResult) : String = // default implementation of the current `DiffResultObject.show`
}
}
Major advantage of such approach is that we would have full control over each particular type in the hierarchy. On the other hand in order to render only identical "things" one will have to override four instances. That I think might be taken care of by introducing a generalized super type ShowConfig[T]
.
I would be happy to hear your thoughts.
Hi, thank you so much for the detailed response!
maybe we could add it to the existing one - ConsoleColorConfig. Then we would need to change its name. How about calling it ShowConfig?
Indeed we already have a lot of implicit parameters, and it sounds good to merge renderIdentical
into ConsoleColorConfig
(and call it ShowConfig
) :+1:
Ideally we would have another typeclass to describe how the diffs for different types should be printed.
Maybe I misunderstand a lot, but here's my opinion below.
TL;DR, I prefer to adding renderIdentical
into ConsoleColorConfig
and call it ShowConfig
approach. I couldn't see much benefit in delegating the rendering logic into other typeclasses (like ShowConfigObject
).
Let me clarify. If we define ShowConfigObject
(and other typeclasses)
show
method will have implicit ConsoleColorObject
parameter, as showIndented
does? like this š ShowConfigObject
(and each of other typeclasses) something like ignoreIdentical
which doesn't render the identical fields?
renderIdentical
parameter to the show
method).renderIdentical
option to ConsoleColorConfig
?object ShowConfigObject {
def default[T] : ShowConfigObject = new ShowConfigObject[T] {
def show(result: DiffObjectResult)(implicit c: ConsoleColorConfig) : String = ???
}
def ignoreIdentical[T]: ShowConfigObject = new ... {
def show(result: DiffObjectResult)(implicit c: ConsoleColorConfig) : String = ??? // do not render the identical fields
}
}
renderIdentical
? (for example noNewlines
, or maybe we want to configure indentLevel
in the future).
ConsoleColorConfig
and call it the ShowConfig
approach.case class ShowConfig(
left: String => String,
right: String => String,
default: String => String,
arrow: String => String,
renderIdentical: Boolean,
noNewline: Boolean
)
object ShowConfigObject {
def default[T] : ShowConfigObject = new ShowConfigObject[T] {
def show(result: DiffObjectResult)(implicit c: ShowConfig) : String = ???
}
}
ShowConfigObject
(and etc...)?
DiffResult
will be printed out by defining their own ShowConfigObject
instances.ShowConfigObject
method, it's almost a developing DiffResult
renderer from the ground up (which is too much effort for users).Also, I'm a type of person who like to expose as small as interfaces to users: that's why I prefer not to expose the ShowConfig
typeclasses to users. (Are there any other benefits of defining ShowConfig
typeclasses?)
Hi,
Thanks alot for this thoughtful comment and sorry for the long delay.
Does the show method will have implicit ConsoleColorObject parameter, as showIndented does? like this point_down
Yes, I think so.
In that case, are we going to define another instance of ShowConfigObject (and each of other typeclasses) something like ignoreIdentical which doesn't render the identical fields?
Well, I thought that we can have only a default instance defined and then expose some transformation helper methods on these classes. After playing around that idea for a while I got that:
trait DiffResult //Simplified model just for the sake of example
case class DiffResultObject() extends DiffResult
case class DiffResultMap() extends DiffResult
case class ConsoleColorConfig(renderIdentical: Boolean)
trait ShowConfig[T] { outer =>
def show(d: DiffResult)(implicit c: ConsoleColorConfig): String = {
d match {
case dObject: DiffResultObject => ShowConfig.defaultShowObject(dObject, c)
case dMap: DiffResultMap => ShowConfig.defaultShowMap(dMap, c)
}
}
def ignoreIdentical: ShowConfig[T] = new ShowConfig[T] {
override def show(d: DiffResult)(implicit c: ConsoleColorConfig): String =
outer.show(d)(c.copy(renderIdentical = false))
}
}
object ShowConfig {
def defaultShowMap(d: DiffResultMap, c: ConsoleColorConfig): String = ???
def defaultShowObject(d: DiffResultObject, c: ConsoleColorConfig): String = ???
def create[T](
showMap: (DiffResultMap, ConsoleColorConfig) => String = defaultShowMap,
showObject: (DiffResultObject, ConsoleColorConfig) => String = defaultShowObject
): ShowConfig[T] = new ShowConfig[T] {
override def show(d: DiffResult)(implicit c: ConsoleColorConfig): String = {
d match {
case dObject: DiffResultObject => showObject(dObject, c)
case dMap: DiffResultMap => showMap(dMap, c)
}
}
}
}
As you can see there is only one type-class. It turned out that the rest was redundant/necessary.
There is still a question where to put modified parameters as we have to maintain the original ConsoleColorConfig
(in order to pass it down the hierarchy). We could expose methods with three parameters instead:
def show(d: DiffResult, original: ConsoleColorConfig, modified: ConsoleColorConfig)
,
but that looks clumsy.
But, I don't see much benefit: do users really want to have full control of the rendering logic? I don't think so.
Not yet, but as a library we should be reasonable generic and we should not close the door for such functionality in the anticipation that someone might ask about that in the future. With such a flexible design additional configuration options (such as noNewLines
or indentLevel
) could be developed on the user side without touching the library. [Obviously common things, or things that repeat across multiple projects should be at some point incorporated into the library for the convenience]
Because If users try to define their own ShowConfigObject method, it's almost a developing DiffResult renderer from the ground up (which is too much effort for users).
That could be possible with my wishful design, but that doesn't mean we would left the users alone. In contrary we would arm them with a bunch of helper methods for high-level transformations (like ignoreIdentical
), but still leave the option for low-level rewrite. See ObjectMatcher
as an example.
On the other hand we still can provide such flexibility even without ShowConfig
type-class. Assuming that for now we would go with global ShowConfig
we could provide in the future option to override ShowConfig
on the given instance level just as we do with the ObjectMatcher
.
What do you think about all of that? Would you like to open a PR with your proposed changes?
Hi, sorry for my delayed reply, and thank you so much for working on it!
Not yet, but as a library we should be reasonable generic and we should not close the door for such functionality in the anticipation that someone might ask about that in the future.
Yeah, indeed. What I concerned about was it might be too early to generalize the design, since we don't see the demand at this moment. However, if we want to customize the diff printer's behavior, your design definitely works perfectly :)
Anyway, thanks again for making it come to life.
No problem, I hope that new design will meet your needs. I am planning to release new version within upcoming days. I would like to finish my blogpost about all the new changes as since the last release there were a lot of them.
Introduction
When we compare two case class objects with large number of fields or nested objects using, for example,
mustMatchTo
provided by com.softwaremill.diffx.scalatest.DiffMustMatcher, and there's a mismatched field, we see pretty long lines of the diff, even if the only one field has mismatch.If the diff (for each failed test) has like 200-300 lines long (which usually happens in my project)
Proposal
It would be awesome if we could control the renderIdentical params for the
show
method over the Test Matchers.( Currently, there seems no way to control the
renderIdentical
param when we're using, for example,DiffMustMatcher
). https://github.com/softwaremill/diffx/blob/acb57c6e93603b1f9c2bf7f0b6bad5ea168c033b/scalatest-must/src/main/scala-2/com/softwaremill/diffx/scalatest/DiffMustMatcher.scala#L31What should we do?
case class ShowConfig(renderIdentical: Boolean)
, which defaults totrue
, asConsoleColorConfig
ShowConfig
settings by definingimplicit
instance ofShowConfig
Like this š : what do you think about it?
Anyway, thank you so much for making and maintaining this library :tada:
diffx
is literally saving our days!