Skip to content

Commit 279b043

Browse files
committed
jsonfmt options for detailed fractured output
1 parent 9863b2c commit 279b043

4 files changed

Lines changed: 272 additions & 8 deletions

File tree

cmd/jsonfmt/README.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,30 @@ Default mode is `--fractured` when no mode flag is provided.
1212
* `--trailing-newline`: if a trailing newline should be emitted
1313
* `--eol`: end-of-line character(s) `[ lf | cr' | crlf ]`. Default is `lf`.
1414
* `--fractured`: compact output with [FracturedJson](https://github.com/j-brooke/FracturedJson) using the [go-fractured-json](https://github.com/FileFormatInfo/go-fractured-json) library
15+
16+
## Fractured Options
17+
18+
`jsonfmt` exposes `go-fractured-json` options as `--fractured-*` flags, including:
19+
20+
* `--fractured-max-total-line-length`
21+
* `--fractured-max-inline-complexity`
22+
* `--fractured-max-compact-array-complexity`
23+
* `--fractured-max-table-row-complexity`
24+
* `--fractured-max-prop-name-padding`
25+
* `--fractured-colon-before-prop-name-padding`
26+
* `--fractured-table-comma-placement` (`before-padding`, `after-padding`, `before-padding-except-numbers`)
27+
* `--fractured-min-compact-array-row-items`
28+
* `--fractured-always-expand-depth`
29+
* `--fractured-nested-bracket-padding`
30+
* `--fractured-simple-bracket-padding`
31+
* `--fractured-colon-padding`
32+
* `--fractured-comma-padding`
33+
* `--fractured-comment-padding`
34+
* `--fractured-number-list-alignment` (`left`, `right`, `decimal`, `normalize`)
35+
* `--fractured-indent-spaces`
36+
* `--fractured-use-tab-to-indent`
37+
* `--fractured-prefix-string`
38+
* `--fractured-comment-policy` (`error`, `remove`, `preserve`)
39+
* `--fractured-preserve-blank-lines`
40+
* `--fractured-allow-trailing-commas`
41+
* `--fractured-json-eol-style` (`default`, `crlf`, `lf`)

cmd/jsonfmt/jsonfmt.go

Lines changed: 183 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -125,14 +125,141 @@ func maybeSortKeys(input []byte, sortKeys bool) ([]byte, error) {
125125
return encodeSortedJSON(decoded)
126126
}
127127

128-
func formatJSON(input []byte, canonical bool, line bool, fractured bool, sortKeys bool) (string, error) {
128+
type fracturedFlagValues struct {
129+
jsonEolStyle string
130+
maxTotalLineLength int
131+
maxInlineComplexity int
132+
maxCompactArrayComplexity int
133+
maxTableRowComplexity int
134+
maxPropNamePadding int
135+
colonBeforePropNamePadding bool
136+
tableCommaPlacement string
137+
minCompactArrayRowItems int
138+
alwaysExpandDepth int
139+
nestedBracketPadding bool
140+
simpleBracketPadding bool
141+
colonPadding bool
142+
commaPadding bool
143+
commentPadding bool
144+
numberListAlignment string
145+
indentSpaces int
146+
useTabToIndent bool
147+
prefixString string
148+
commentPolicy string
149+
preserveBlankLines bool
150+
allowTrailingCommas bool
151+
}
152+
153+
func parseEolStyle(v string) (fracturedjson.EolStyle, error) {
154+
switch strings.ToLower(v) {
155+
case "default":
156+
return fracturedjson.EolDefault, nil
157+
case "crlf":
158+
return fracturedjson.EolCRLF, nil
159+
case "lf":
160+
return fracturedjson.EolLF, nil
161+
default:
162+
return fracturedjson.EolDefault, fmt.Errorf("invalid fractured json eol style %q (expected: default, crlf, lf)", v)
163+
}
164+
}
165+
166+
func parseTableCommaPlacement(v string) (fracturedjson.TableCommaPlacement, error) {
167+
switch strings.ToLower(v) {
168+
case "before-padding":
169+
return fracturedjson.CommaBeforePadding, nil
170+
case "after-padding":
171+
return fracturedjson.CommaAfterPadding, nil
172+
case "before-padding-except-numbers":
173+
return fracturedjson.CommaBeforePaddingExceptNumbers, nil
174+
default:
175+
return fracturedjson.CommaBeforePaddingExceptNumbers, fmt.Errorf("invalid fractured table comma placement %q (expected: before-padding, after-padding, before-padding-except-numbers)", v)
176+
}
177+
}
178+
179+
func parseNumberListAlignment(v string) (fracturedjson.NumberListAlignment, error) {
180+
switch strings.ToLower(v) {
181+
case "left":
182+
return fracturedjson.NumberLeft, nil
183+
case "right":
184+
return fracturedjson.NumberRight, nil
185+
case "decimal":
186+
return fracturedjson.NumberDecimal, nil
187+
case "normalize":
188+
return fracturedjson.NumberNormalize, nil
189+
default:
190+
return fracturedjson.NumberDecimal, fmt.Errorf("invalid fractured number list alignment %q (expected: left, right, decimal, normalize)", v)
191+
}
192+
}
193+
194+
func parseCommentPolicy(v string) (fracturedjson.CommentPolicy, error) {
195+
switch strings.ToLower(v) {
196+
case "error":
197+
return fracturedjson.CommentTreatAsError, nil
198+
case "remove":
199+
return fracturedjson.CommentRemove, nil
200+
case "preserve":
201+
return fracturedjson.CommentPreserve, nil
202+
default:
203+
return fracturedjson.CommentTreatAsError, fmt.Errorf("invalid fractured comment policy %q (expected: error, remove, preserve)", v)
204+
}
205+
}
206+
207+
func buildFracturedOptions(flags fracturedFlagValues) (fracturedjson.Options, error) {
208+
options := fracturedjson.RecommendedOptions()
209+
210+
eolStyle, err := parseEolStyle(flags.jsonEolStyle)
211+
if err != nil {
212+
return options, err
213+
}
214+
commaPlacement, err := parseTableCommaPlacement(flags.tableCommaPlacement)
215+
if err != nil {
216+
return options, err
217+
}
218+
numberAlignment, err := parseNumberListAlignment(flags.numberListAlignment)
219+
if err != nil {
220+
return options, err
221+
}
222+
commentPolicy, err := parseCommentPolicy(flags.commentPolicy)
223+
if err != nil {
224+
return options, err
225+
}
226+
227+
options.JsonEolStyle = eolStyle
228+
options.MaxTotalLineLength = flags.maxTotalLineLength
229+
options.MaxInlineComplexity = flags.maxInlineComplexity
230+
options.MaxCompactArrayComplexity = flags.maxCompactArrayComplexity
231+
options.MaxTableRowComplexity = flags.maxTableRowComplexity
232+
options.MaxPropNamePadding = flags.maxPropNamePadding
233+
options.ColonBeforePropNamePadding = flags.colonBeforePropNamePadding
234+
options.TableCommaPlacement = commaPlacement
235+
options.MinCompactArrayRowItems = flags.minCompactArrayRowItems
236+
options.AlwaysExpandDepth = flags.alwaysExpandDepth
237+
options.NestedBracketPadding = flags.nestedBracketPadding
238+
options.SimpleBracketPadding = flags.simpleBracketPadding
239+
options.ColonPadding = flags.colonPadding
240+
options.CommaPadding = flags.commaPadding
241+
options.CommentPadding = flags.commentPadding
242+
options.NumberListAlignment = numberAlignment
243+
options.IndentSpaces = flags.indentSpaces
244+
options.UseTabToIndent = flags.useTabToIndent
245+
options.PrefixString = strings.ReplaceAll(flags.prefixString, `\t`, "\t")
246+
options.CommentPolicy = commentPolicy
247+
options.PreserveBlankLines = flags.preserveBlankLines
248+
options.AllowTrailingCommas = flags.allowTrailingCommas
249+
250+
return options, nil
251+
}
252+
253+
func formatJSON(input []byte, canonical bool, line bool, fractured bool, sortKeys bool, fracturedOptions fracturedjson.Options) (string, error) {
129254
input, err := maybeSortKeys(input, sortKeys)
130255
if err != nil {
131256
return "", err
132257
}
133258

134259
if fractured {
135-
return fracturedjson.Reformat(string(input))
260+
f := fracturedjson.NewFormatter()
261+
f.Options = fracturedOptions
262+
return f.Reformat(string(input), 0)
136263
}
137264

138265
if line {
@@ -193,6 +320,30 @@ func main() {
193320
var trailingNewline = pflag.Bool("trailing-newline", false, "Emit a trailing newline")
194321
var eol = pflag.String("eol", "lf", "End-of-line style: lf, cr, or crlf")
195322

323+
defaults := fracturedjson.RecommendedOptions()
324+
var fracturedJsonEolStyle = pflag.String("fractured-json-eol-style", "default", "Fractured JSON EOL style: default, crlf, lf")
325+
var fracturedMaxTotalLineLength = pflag.Int("fractured-max-total-line-length", defaults.MaxTotalLineLength, "Fractured max total line length")
326+
var fracturedMaxInlineComplexity = pflag.Int("fractured-max-inline-complexity", defaults.MaxInlineComplexity, "Fractured max inline complexity")
327+
var fracturedMaxCompactArrayComplexity = pflag.Int("fractured-max-compact-array-complexity", defaults.MaxCompactArrayComplexity, "Fractured max compact array complexity")
328+
var fracturedMaxTableRowComplexity = pflag.Int("fractured-max-table-row-complexity", defaults.MaxTableRowComplexity, "Fractured max table row complexity")
329+
var fracturedMaxPropNamePadding = pflag.Int("fractured-max-prop-name-padding", defaults.MaxPropNamePadding, "Fractured max property name padding")
330+
var fracturedColonBeforePropNamePadding = pflag.Bool("fractured-colon-before-prop-name-padding", defaults.ColonBeforePropNamePadding, "Fractured: place colon before prop-name padding")
331+
var fracturedTableCommaPlacement = pflag.String("fractured-table-comma-placement", "before-padding-except-numbers", "Fractured table comma placement: before-padding, after-padding, before-padding-except-numbers")
332+
var fracturedMinCompactArrayRowItems = pflag.Int("fractured-min-compact-array-row-items", defaults.MinCompactArrayRowItems, "Fractured min compact array row items")
333+
var fracturedAlwaysExpandDepth = pflag.Int("fractured-always-expand-depth", defaults.AlwaysExpandDepth, "Fractured always-expand depth")
334+
var fracturedNestedBracketPadding = pflag.Bool("fractured-nested-bracket-padding", defaults.NestedBracketPadding, "Fractured nested bracket padding")
335+
var fracturedSimpleBracketPadding = pflag.Bool("fractured-simple-bracket-padding", defaults.SimpleBracketPadding, "Fractured simple bracket padding")
336+
var fracturedColonPadding = pflag.Bool("fractured-colon-padding", defaults.ColonPadding, "Fractured colon padding")
337+
var fracturedCommaPadding = pflag.Bool("fractured-comma-padding", defaults.CommaPadding, "Fractured comma padding")
338+
var fracturedCommentPadding = pflag.Bool("fractured-comment-padding", defaults.CommentPadding, "Fractured comment padding")
339+
var fracturedNumberListAlignment = pflag.String("fractured-number-list-alignment", "decimal", "Fractured number list alignment: left, right, decimal, normalize")
340+
var fracturedIndentSpaces = pflag.Int("fractured-indent-spaces", defaults.IndentSpaces, "Fractured indent spaces")
341+
var fracturedUseTabToIndent = pflag.Bool("fractured-use-tab-to-indent", defaults.UseTabToIndent, "Fractured use tabs to indent")
342+
var fracturedPrefixString = pflag.String("fractured-prefix-string", defaults.PrefixString, "Fractured prefix string (use \\t for tabs)")
343+
var fracturedCommentPolicy = pflag.String("fractured-comment-policy", "error", "Fractured comment policy: error, remove, preserve")
344+
var fracturedPreserveBlankLines = pflag.Bool("fractured-preserve-blank-lines", defaults.PreserveBlankLines, "Fractured preserve blank lines")
345+
var fracturedAllowTrailingCommas = pflag.Bool("fractured-allow-trailing-commas", defaults.AllowTrailingCommas, "Fractured allow trailing commas")
346+
196347
var help = pflag.BoolP("help", "h", false, "Show help message")
197348
var version = pflag.Bool("version", false, "Print version information")
198349

@@ -217,6 +368,35 @@ func main() {
217368
os.Exit(1)
218369
}
219370

371+
fracturedOptions, err := buildFracturedOptions(fracturedFlagValues{
372+
jsonEolStyle: *fracturedJsonEolStyle,
373+
maxTotalLineLength: *fracturedMaxTotalLineLength,
374+
maxInlineComplexity: *fracturedMaxInlineComplexity,
375+
maxCompactArrayComplexity: *fracturedMaxCompactArrayComplexity,
376+
maxTableRowComplexity: *fracturedMaxTableRowComplexity,
377+
maxPropNamePadding: *fracturedMaxPropNamePadding,
378+
colonBeforePropNamePadding: *fracturedColonBeforePropNamePadding,
379+
tableCommaPlacement: *fracturedTableCommaPlacement,
380+
minCompactArrayRowItems: *fracturedMinCompactArrayRowItems,
381+
alwaysExpandDepth: *fracturedAlwaysExpandDepth,
382+
nestedBracketPadding: *fracturedNestedBracketPadding,
383+
simpleBracketPadding: *fracturedSimpleBracketPadding,
384+
colonPadding: *fracturedColonPadding,
385+
commaPadding: *fracturedCommaPadding,
386+
commentPadding: *fracturedCommentPadding,
387+
numberListAlignment: *fracturedNumberListAlignment,
388+
indentSpaces: *fracturedIndentSpaces,
389+
useTabToIndent: *fracturedUseTabToIndent,
390+
prefixString: *fracturedPrefixString,
391+
commentPolicy: *fracturedCommentPolicy,
392+
preserveBlankLines: *fracturedPreserveBlankLines,
393+
allowTrailingCommas: *fracturedAllowTrailingCommas,
394+
})
395+
if err != nil {
396+
fmt.Fprintf(os.Stderr, "ERROR: %v\n", err)
397+
os.Exit(1)
398+
}
399+
220400
args := pflag.Args()
221401
if len(args) == 0 {
222402
args = []string{"-"}
@@ -232,7 +412,7 @@ func main() {
232412
os.Exit(1)
233413
}
234414

235-
formatted, err := formatJSON(input, canonicalMode, lineMode, fracturedMode, *sortKeys)
415+
formatted, err := formatJSON(input, canonicalMode, lineMode, fracturedMode, *sortKeys, fracturedOptions)
236416
if err != nil {
237417
fmt.Fprintf(os.Stderr, "ERROR: unable to format JSON: %v\n", err)
238418
os.Exit(1)

cmd/jsonfmt/jsonfmt_test.go

Lines changed: 55 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ package main
33
import (
44
"strings"
55
"testing"
6+
7+
fracturedjson "github.com/FileFormatInfo/go-fractured-json"
68
)
79

810
func TestResolveModes(t *testing.T) {
@@ -52,7 +54,7 @@ func TestApplyEOL(t *testing.T) {
5254
}
5355

5456
func TestFormatJSONLine(t *testing.T) {
55-
out, err := formatJSON([]byte("{\n \"b\": 2,\n \"a\": 1\n}\n"), false, true, false, false)
57+
out, err := formatJSON([]byte("{\n \"b\": 2,\n \"a\": 1\n}\n"), false, true, false, false, fracturedjson.RecommendedOptions())
5658
if err != nil {
5759
t.Fatalf("formatJSON line error: %v", err)
5860
}
@@ -62,7 +64,7 @@ func TestFormatJSONLine(t *testing.T) {
6264
}
6365

6466
func TestFormatJSONCanonicalSortsKeys(t *testing.T) {
65-
out, err := formatJSON([]byte("{\"b\":2,\"a\":1}"), true, false, false, false)
67+
out, err := formatJSON([]byte("{\"b\":2,\"a\":1}"), true, false, false, false, fracturedjson.RecommendedOptions())
6668
if err != nil {
6769
t.Fatalf("formatJSON canonical error: %v", err)
6870
}
@@ -75,7 +77,7 @@ func TestFormatJSONCanonicalSortsKeys(t *testing.T) {
7577
}
7678

7779
func TestFormatJSONExpanded(t *testing.T) {
78-
out, err := formatJSON([]byte("{\"k\":\"v\"}"), false, false, false, false)
80+
out, err := formatJSON([]byte("{\"k\":\"v\"}"), false, false, false, false, fracturedjson.RecommendedOptions())
7981
if err != nil {
8082
t.Fatalf("formatJSON expanded error: %v", err)
8183
}
@@ -85,7 +87,7 @@ func TestFormatJSONExpanded(t *testing.T) {
8587
}
8688

8789
func TestFormatJSONFractured(t *testing.T) {
88-
out, err := formatJSON([]byte("{\"a\":1,\"b\":2}"), false, false, true, false)
90+
out, err := formatJSON([]byte("{\"a\":1,\"b\":2}"), false, false, true, false, fracturedjson.RecommendedOptions())
8991
if err != nil {
9092
t.Fatalf("formatJSON fractured error: %v", err)
9193
}
@@ -95,11 +97,59 @@ func TestFormatJSONFractured(t *testing.T) {
9597
}
9698

9799
func TestFormatJSONLineSortKeysCaseInsensitive(t *testing.T) {
98-
out, err := formatJSON([]byte("{\"b\":1,\"A\":2,\"a\":3,\"B\":4}"), false, true, false, true)
100+
out, err := formatJSON([]byte("{\"b\":1,\"A\":2,\"a\":3,\"B\":4}"), false, true, false, true, fracturedjson.RecommendedOptions())
99101
if err != nil {
100102
t.Fatalf("formatJSON line sort-keys error: %v", err)
101103
}
102104
if out != "{\"A\":2,\"a\":3,\"B\":4,\"b\":1}" {
103105
t.Fatalf("line sort-keys output = %q", out)
104106
}
105107
}
108+
109+
func TestBuildFracturedOptions(t *testing.T) {
110+
opts, err := buildFracturedOptions(fracturedFlagValues{
111+
jsonEolStyle: "lf",
112+
maxTotalLineLength: 80,
113+
maxInlineComplexity: 1,
114+
maxCompactArrayComplexity: 1,
115+
maxTableRowComplexity: 1,
116+
maxPropNamePadding: 8,
117+
colonBeforePropNamePadding: true,
118+
tableCommaPlacement: "after-padding",
119+
minCompactArrayRowItems: 2,
120+
alwaysExpandDepth: 0,
121+
nestedBracketPadding: false,
122+
simpleBracketPadding: true,
123+
colonPadding: false,
124+
commaPadding: false,
125+
commentPadding: false,
126+
numberListAlignment: "right",
127+
indentSpaces: 2,
128+
useTabToIndent: true,
129+
prefixString: "\\t",
130+
commentPolicy: "remove",
131+
preserveBlankLines: true,
132+
allowTrailingCommas: true,
133+
})
134+
if err != nil {
135+
t.Fatalf("buildFracturedOptions error: %v", err)
136+
}
137+
if opts.JsonEolStyle != fracturedjson.EolLF || opts.TableCommaPlacement != fracturedjson.CommaAfterPadding || opts.NumberListAlignment != fracturedjson.NumberRight || opts.CommentPolicy != fracturedjson.CommentRemove {
138+
t.Fatalf("unexpected enum mapping in options: %+v", opts)
139+
}
140+
if opts.PrefixString != "\t" {
141+
t.Fatalf("expected prefix string to decode to tab, got %q", opts.PrefixString)
142+
}
143+
}
144+
145+
func TestBuildFracturedOptionsInvalidEnum(t *testing.T) {
146+
_, err := buildFracturedOptions(fracturedFlagValues{
147+
jsonEolStyle: "bad",
148+
tableCommaPlacement: "before-padding-except-numbers",
149+
numberListAlignment: "decimal",
150+
commentPolicy: "error",
151+
})
152+
if err == nil {
153+
t.Fatalf("expected invalid enum error")
154+
}
155+
}

testdata/jsonfmt.txtar

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@ stdout '(?s)^\{\n "a": 1,\n "b": \{\n "x": 2,\n "y": 1\n \},\n "z": 3\
1010
exec jsonfmt --line --sort-keys jsonfmt_input_case.json
1111
stdout '^\{"A":2,"a":3,"B":4,"b":1\}$'
1212

13+
# fractured option passthrough
14+
exec jsonfmt --fractured-always-expand-depth=0 --fractured-indent-spaces=2 jsonfmt_input_nested.json
15+
stdout '(?s)^\{\n "x": \{"a": 1\}\n\}$'
16+
1317
# trailing newline and CRLF mode
1418
exec jsonfmt --line --trailing-newline --eol=crlf jsonfmt_input_array.json
1519
stdout '^\[\{"n":2,"s":"b"\},\{"n":1,"s":"a"\}\]\r\n$'
@@ -29,3 +33,6 @@ stderr 'use at most one of --canonical, --line, --fractured'
2933

3034
-- jsonfmt_input_case.json --
3135
{"b":1,"A":2,"a":3,"B":4}
36+
37+
-- jsonfmt_input_nested.json --
38+
{"x":{"a":1}}

0 commit comments

Comments
 (0)