Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add tabular output to HumanRenderer #19

Closed
wants to merge 2 commits into from
Closed

Conversation

mmourick
Copy link
Contributor

@mmourick mmourick commented Dec 10, 2024

Introduces go-pretty/v6/table to keep human readable output aligned without tedious use of fmt.Sprintf(), previously found in formatRecord().

Additionally updates the Renderer interface as table rows need to be submitted before writing them to stdout.

Fixes #11

Introduces `go-pretty/v6/table` to keep human readable output aligned without tedious use of `fmt.Sprintf()`, previously found in `formatRecord()`.

Additionally updates the `Renderer` interface as table rows need to be submitted before writing them to stdout.
Copy link
Contributor

@bschaatsbergen bschaatsbergen left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Left a couple of comments—great work so far on replacing the flaky tab writing we currently have! I think there are a few areas we can improve, such as forcing the empty interface as a return value and avoiding breaking the interface. Thanks for your work on this @mmourick!

@@ -149,9 +149,10 @@ var (

for _, m := range messages {
for _, record := range m.Answer {
v.Render(args[0], record)
v.AddRecord(args[0], record)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From a consumer’s perspective, it’s a bit unclear what exactly is being added or appended. I’m still considering a cleaner alternative and will get back on this. What do you think?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It made sense to me, as a record is added to view v by means of AddRecord().

// formatRecord generates a human-readable string representing a DNS record with colors.
func formatRecord(domainName string, answer dns.RR) string {
// formatRecord generates human-readable strings representing a DNS record with colors.
func formatRecord(domain string, answer dns.RR) []interface{} {
Copy link
Contributor

@bschaatsbergen bschaatsbergen Dec 11, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can’t we preserve returning a string, with the conversion to []interface{} handled further down the call stack where that empty type is required? This keeps this function intact and more easily testable. There’s no reason for this function to return a []interface{} other than fulfilling a specific consumer requirement elsewhere. Or am I overseeing something?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I needed to change the return type (and the implementation, somewhat) of this function, because the table handles the formatting for rows containing a variadic amount of elements, and these elements need to be provided separately, namely via []interface{} .
I had an earlier implementation of this function that returned []string, however that caused an additional loop since I wouldn't be able to use the ... notation to unpack the values and add them to a row.


Thank you for your contribution!
`, recordType, recordType)
return []interface{}{fmt.Sprintf(`
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe preserving a multiline string—would improve readability from the user’s perspective. There's definitely room for improvement in the existing error message we return now.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah the current setup is not ideal. I shortened it because the multiline message severely messes up the formatting of the output, and while this current setup looks a bit better, perhaps this should be treated as an error?

@@ -182,7 +182,7 @@ func TestFormatRecord(t *testing.T) {
os.Setenv("NO_COLOR", "true") // Disable colors for easier testing

r := formatRecord(domain, record)
assert.Equal(t, "A\texample.com.\t03m42s\t127.0.0.1", r)
assert.Equal(t, []interface{}{"A", "example.com", "03m42s", "127.0.0.1"}, r)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This ties back to an earlier comment on the use of []interface{}. It highlights the issues that arise from forcing an empty type, as any other consumer of that function that doesn’t need it is now unnecessarily burdened with handling it.

@@ -69,3 +90,8 @@ func (v *JSONRenderer) Render(domain string, record dns.RR) {

v.view.Output("Successful query", params...)
}

// JSONRender is not buffered, so no need to flush
func (v *JSONRenderer) Render() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new Render method highlights a lack of true shared behavior between the renderers, conflicting with Go’s design philosophy for interfaces: "Don’t design with interfaces, discover them." This simply tries to say: introduce an interface when you discover genuine shared behavior—such as two renderers that do something similar, like rendering DNS records.

Previously, this was the case—each Renderer (JSON or Human) knew how to render a DNS record back to the TTY via a stream.

Copy link
Contributor Author

@mmourick mmourick Dec 11, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sadly, the human friendly renderer diverges a bit from the previous interface.
If preferred, the JSON rendered could adopt some likeness from the human renderer, storing the records in a buffer until Render() is called?

The table implementation otherwise cannot be implemented if it has to abide by the unmodified interface.

Perhaps another solution would be to pass the entire the entire answer []dns.RR to the Render() method. That would still require updating the interface, but eliminates the need for a second function in the interface.


// Render writes the human-readable table to the output stream
func (v *HumanRenderer) Render() {
v.t.Render()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This reads a bit odd—I’m guessing it’s because t (table) also has a Render method. However, it made me double-check t to ensure we weren’t unintentionally calling something recursively.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct, t has it's own Render() method. perhaps t should be renamed to table.

@bschaatsbergen
Copy link
Contributor

bschaatsbergen commented Dec 16, 2024

Hey @mmourick,

After our discussion above, I revisited the text/tabwriter package and found that it solves the problem while preserving much of the original code. I’ve prototyped an alternative implementation in #25.

Personally, I’d prefer to retain as much of the original rendering (view/stream code) as possible. If you’re considering a more substantial change, it might be worth to align on the best approach before moving forward. Thanks!

@mmourick mmourick closed this Dec 16, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Long TTL values result in incorrect tab formatting in stdout output
2 participants