From 2948cc771d6dfe4bd50e9be6df3b393f45d3bb9a Mon Sep 17 00:00:00 2001 From: DonislawDev Date: Tue, 8 Sep 2026 22:42:47 +0200 Subject: [PATCH] format: one name for one setting, and one place for a picture side Twenty four formats declare seventy two settings under thirty nine names, and thirteen of those names are written by more than one format. Nothing compared them. Two of the clusters were already held together by a shared package - the container axes and the text encoding - and the third, the largest, was not: width and height were written out from scratch in ten packages, and the function reading them stood in all ten as well, the same twelve lines differing only in the format named inside its message. internal/format/imagedim is that third package. What it owns is what cannot legitimately differ: the name, that it is a whole number, that the smallest side is one pixel, and that the number counts pixels. What each format still supplies is what was measured to differ for a reason - the largest side, the sentence, and the default that only SVG declares. The sentences are NOT unified: an icon is not a picture and an SVG only says how wide it is, so three of the four wordings would have become wrong. The reader unifies on the best of the ten rather than the most common. Nine built a string with fmt.Errorf and named the format inside it. SVG returned format.PropertyValueError, the same type the registry itself raises, so what reports it asks for the parts by name instead of parsing a sentence. The wording is SVG's, unchanged. Two guards keep the shape without forcing a false one. The first asks that a name written by two formats describes the same kind of setting, in Kind and Unit only - Max, Default and Min diverge for measured reasons, so asking about them would redden on the legitimate case. The second asks that the one deliberate homonym is still one, from both sides, because an excuse that outlives its case reads as a rule. entry_format is that homonym: a log means the shape of a log line by it where a container means the format of the files it holds, which is why the guard banning it outright was deleted on 2026-09-01. The range check under the ten was unreachable. The registry refuses first in engine.go and recipe/target.go, the only two production callers - png with width 999999 comes back as exit 4 with the sentence built from the declaration - and nothing in internal/guard reached a generator with a value outside its range. It is kept for the reason textenc.Parse gives about its own branches, and keeping it moved the burden to a test, so it now has one, with a control that it did not become a wall. Measured after: tfg formats --json identical byte for byte, all twenty four declaration hashes unchanged, full suite green, gofmt and vet clean. Co-Authored-By: Claude Opus 5 --- internal/format/avif/avif.go | 34 ++---- internal/format/bmp/bmp.go | 35 ++---- internal/format/gif/gif.go | 34 ++---- internal/format/ico/ico.go | 35 ++---- internal/format/imagedim/imagedim.go | 122 ++++++++++++++++++++ internal/format/jpg/jpg.go | 34 ++---- internal/format/jxl/jxl.go | 34 ++---- internal/format/png/png.go | 36 ++---- internal/format/svgfile/svg.go | 54 +++------ internal/format/tiff/tiff.go | 35 ++---- internal/format/webp/webp.go | 35 ++---- internal/guard/layers_test.go | 22 ++-- internal/guard/picturesides_test.go | 89 +++++++++++++++ internal/guard/settingmeaning_test.go | 156 ++++++++++++++++++++++++++ 14 files changed, 455 insertions(+), 300 deletions(-) create mode 100644 internal/format/imagedim/imagedim.go create mode 100644 internal/guard/picturesides_test.go create mode 100644 internal/guard/settingmeaning_test.go diff --git a/internal/format/avif/avif.go b/internal/format/avif/avif.go index aa740dd..9ad045a 100644 --- a/internal/format/avif/avif.go +++ b/internal/format/avif/avif.go @@ -48,6 +48,7 @@ import ( "github.com/donislawdev/TestingFilesGenerator/internal/core" "github.com/donislawdev/TestingFilesGenerator/internal/format" + "github.com/donislawdev/TestingFilesGenerator/internal/format/imagedim" ) const ( @@ -125,16 +126,10 @@ func init() { Label: format.LabelVisible, Oracle: "pillow", Properties: []format.Property{ - { - Name: "width", Kind: format.PropertyInt, - Min: minDimension, Max: maxDimension, Unit: "pixels", - Detail: "How wide the picture is. Left out, a size is chosen that fits the bytes you asked for.", - }, - { - Name: "height", Kind: format.PropertyInt, - Min: minDimension, Max: maxDimension, Unit: "pixels", - Detail: "How tall the picture is. Left out, a size is chosen that fits the bytes you asked for.", - }, + imagedim.Width(imagedim.Side{Largest: maxDimension, + Detail: "How wide the picture is. Left out, a size is chosen that fits the bytes you asked for."}), + imagedim.Height(imagedim.Side{Largest: maxDimension, + Detail: "How tall the picture is. Left out, a size is chosen that fits the bytes you asked for."}), { Name: "quality", Kind: format.PropertyInt, Min: minQuality, Max: maxQuality, Default: strconv.Itoa(defaultQuality), @@ -345,11 +340,11 @@ func measuredSize(r format.Request, label string, q int) (memo, error) { // namedSize handles a recipe that asked for a picture of its own size. func namedSize(r format.Request, label string, q int) (memo, error) { - w, err := dimension(r.Properties, "width", sizeLadder[0].width) + w, err := imagedim.Value("avif", imagedim.SettingWidth, r.Properties, maxDimension, sizeLadder[0].width) if err != nil { return memo{}, err } - h, err := dimension(r.Properties, "height", sizeLadder[0].height) + h, err := imagedim.Value("avif", imagedim.SettingHeight, r.Properties, maxDimension, sizeLadder[0].height) if err != nil { return memo{}, err } @@ -389,21 +384,6 @@ func checkJointLimits(w, h int) error { return nil } -func dimension(props map[string]string, key string, fallback int) (int, error) { - raw, ok := props[key] - if !ok || raw == "" { - return fallback, nil - } - n, err := strconv.Atoi(raw) - if err != nil { - return 0, fmt.Errorf("avif: %s must be a whole number of pixels, got %q", key, raw) - } - if n < minDimension || n > maxDimension { - return 0, fmt.Errorf("avif: %s must be between %d and %d pixels, got %d", key, minDimension, maxDimension, n) - } - return n, nil -} - func quality(props map[string]string) (int, error) { raw, ok := props["quality"] if !ok || raw == "" { diff --git a/internal/format/bmp/bmp.go b/internal/format/bmp/bmp.go index 153c3ee..ad46f96 100644 --- a/internal/format/bmp/bmp.go +++ b/internal/format/bmp/bmp.go @@ -18,10 +18,10 @@ import ( "image" "image/color" "io" - "strconv" "github.com/donislawdev/TestingFilesGenerator/internal/core" "github.com/donislawdev/TestingFilesGenerator/internal/format" + "github.com/donislawdev/TestingFilesGenerator/internal/format/imagedim" "github.com/donislawdev/TestingFilesGenerator/internal/format/imagelabel" ) @@ -85,16 +85,10 @@ func init() { Label: format.LabelVisible, Oracle: "pillow", Properties: []format.Property{ - { - Name: "width", Kind: format.PropertyInt, - Min: minDimension, Max: maxDimension, Unit: "pixels", - Detail: "How wide the picture is. Left out, the picture is sized to fill the bytes you asked for.", - }, - { - Name: "height", Kind: format.PropertyInt, - Min: minDimension, Max: maxDimension, Unit: "pixels", - Detail: "How tall the picture is. Left out, the picture is sized to fill the bytes you asked for.", - }, + imagedim.Width(imagedim.Side{Largest: maxDimension, + Detail: "How wide the picture is. Left out, the picture is sized to fill the bytes you asked for."}), + imagedim.Height(imagedim.Side{Largest: maxDimension, + Detail: "How tall the picture is. Left out, the picture is sized to fill the bytes you asked for."}), }, GeneratorVersion: generatorVersion, Generator: generator{}, @@ -195,11 +189,11 @@ func chooseSize(r format.Request) (int, int, error) { _, hSet := r.Properties["height"] if wSet || hSet { - w, err := dimension(r.Properties, "width", 0) + w, err := imagedim.Value("bmp", imagedim.SettingWidth, r.Properties, maxDimension, 0) if err != nil { return 0, 0, err } - h, err := dimension(r.Properties, "height", 0) + h, err := imagedim.Value("bmp", imagedim.SettingHeight, r.Properties, maxDimension, 0) if err != nil { return 0, 0, err } @@ -284,21 +278,6 @@ func isqrt(n uint64) uint64 { return x } -func dimension(props map[string]string, key string, fallback int) (int, error) { - raw, ok := props[key] - if !ok || raw == "" { - return fallback, nil - } - n, err := strconv.Atoi(raw) - if err != nil { - return 0, fmt.Errorf("bmp: %s must be a whole number of pixels, got %q", key, raw) - } - if n < minDimension || n > maxDimension { - return 0, fmt.Errorf("bmp: %s must be between %d and %d pixels, got %d", key, minDimension, maxDimension, n) - } - return n, nil -} - func (generator) Write(ctx context.Context, w io.Writer, p format.Plan) error { m, ok := p.Memo.(memo) if !ok { diff --git a/internal/format/gif/gif.go b/internal/format/gif/gif.go index 1fb4375..187427a 100644 --- a/internal/format/gif/gif.go +++ b/internal/format/gif/gif.go @@ -39,6 +39,7 @@ import ( "github.com/donislawdev/TestingFilesGenerator/internal/core" "github.com/donislawdev/TestingFilesGenerator/internal/format" + "github.com/donislawdev/TestingFilesGenerator/internal/format/imagedim" "github.com/donislawdev/TestingFilesGenerator/internal/format/imagelabel" ) @@ -123,16 +124,10 @@ func init() { Label: format.LabelVisible, Oracle: "pillow", Properties: []format.Property{ - { - Name: "width", Kind: format.PropertyInt, - Min: minDimension, Max: maxDimension, Unit: "pixels", - Detail: "How wide the picture is. Left out, a size is chosen that fits the bytes you asked for.", - }, - { - Name: "height", Kind: format.PropertyInt, - Min: minDimension, Max: maxDimension, Unit: "pixels", - Detail: "How tall the picture is. Left out, a size is chosen that fits the bytes you asked for.", - }, + imagedim.Width(imagedim.Side{Largest: maxDimension, + Detail: "How wide the picture is. Left out, a size is chosen that fits the bytes you asked for."}), + imagedim.Height(imagedim.Side{Largest: maxDimension, + Detail: "How tall the picture is. Left out, a size is chosen that fits the bytes you asked for."}), { Name: "frames", Kind: format.PropertyInt, Min: minFrames, Max: maxFrames, @@ -422,11 +417,11 @@ func chooseSize(r format.Request, label string) (memo, error) { } if wSet || hSet { - w, err := dimension(r.Properties, "width", 640) + w, err := imagedim.Value("gif", imagedim.SettingWidth, r.Properties, maxDimension, 640) if err != nil { return memo{}, err } - h, err := dimension(r.Properties, "height", 480) + h, err := imagedim.Value("gif", imagedim.SettingHeight, r.Properties, maxDimension, 480) if err != nil { return memo{}, err } @@ -475,21 +470,6 @@ func chooseSize(r format.Request, label string) (memo, error) { return smallest, nil } -func dimension(props map[string]string, key string, fallback int) (int, error) { - raw, ok := props[key] - if !ok || raw == "" { - return fallback, nil - } - n, err := strconv.Atoi(raw) - if err != nil { - return 0, fmt.Errorf("gif: %s must be a whole number of pixels, got %q", key, raw) - } - if n < minDimension || n > maxDimension { - return 0, fmt.Errorf("gif: %s must be between %d and %d pixels, got %d", key, minDimension, maxDimension, n) - } - return n, nil -} - func encodedBodySize(m memo) (int64, error) { holder := &tailHolder{w: io.Discard, keep: trailerSize} if err := encode(holder, m); err != nil { diff --git a/internal/format/ico/ico.go b/internal/format/ico/ico.go index 813ffd8..4af1f75 100644 --- a/internal/format/ico/ico.go +++ b/internal/format/ico/ico.go @@ -19,10 +19,10 @@ import ( "image/color" stdpng "image/png" "io" - "strconv" "github.com/donislawdev/TestingFilesGenerator/internal/core" "github.com/donislawdev/TestingFilesGenerator/internal/format" + "github.com/donislawdev/TestingFilesGenerator/internal/format/imagedim" "github.com/donislawdev/TestingFilesGenerator/internal/format/imagelabel" ) @@ -77,16 +77,10 @@ func init() { Label: format.LabelVisible, Oracle: "pillow", Properties: []format.Property{ - { - Name: "width", Kind: format.PropertyInt, - Min: minDimension, Max: maxDimension, Unit: "pixels", - Detail: "How wide the icon is. Left out, the largest standard icon size that fits is used.", - }, - { - Name: "height", Kind: format.PropertyInt, - Min: minDimension, Max: maxDimension, Unit: "pixels", - Detail: "How tall the icon is. Left out, the largest standard icon size that fits is used.", - }, + imagedim.Width(imagedim.Side{Largest: maxDimension, + Detail: "How wide the icon is. Left out, the largest standard icon size that fits is used."}), + imagedim.Height(imagedim.Side{Largest: maxDimension, + Detail: "How tall the icon is. Left out, the largest standard icon size that fits is used."}), { Name: "embed", Kind: format.PropertyChoice, Choices: []string{embedBMP, embedPNG}, @@ -221,11 +215,11 @@ func chooseSize(r format.Request, label, embed string) (int, int, error) { _, hSet := r.Properties["height"] if wSet || hSet { - w, err := dimension(r.Properties, "width", maxDimension) + w, err := imagedim.Value("ico", imagedim.SettingWidth, r.Properties, maxDimension, maxDimension) if err != nil { return 0, 0, err } - h, err := dimension(r.Properties, "height", maxDimension) + h, err := imagedim.Value("ico", imagedim.SettingHeight, r.Properties, maxDimension, maxDimension) if err != nil { return 0, 0, err } @@ -245,21 +239,6 @@ func chooseSize(r format.Request, label, embed string) (int, int, error) { return smallest, smallest, nil } -func dimension(props map[string]string, key string, fallback int) (int, error) { - raw, ok := props[key] - if !ok || raw == "" { - return fallback, nil - } - n, err := strconv.Atoi(raw) - if err != nil { - return 0, fmt.Errorf("ico: %s must be a whole number of pixels, got %q", key, raw) - } - if n < minDimension || n > maxDimension { - return 0, fmt.Errorf("ico: %s must be between %d and %d pixels, got %d - an icon stores each side in a single byte", key, minDimension, maxDimension, n) - } - return n, nil -} - func (generator) Write(ctx context.Context, w io.Writer, p format.Plan) error { m, ok := p.Memo.(memo) if !ok { diff --git a/internal/format/imagedim/imagedim.go b/internal/format/imagedim/imagedim.go new file mode 100644 index 0000000..82d22b7 --- /dev/null +++ b/internal/format/imagedim/imagedim.go @@ -0,0 +1,122 @@ +// Package imagedim is the width and height that every picture format shares. +// +// It lives here rather than inside one of them because ten formats ask the +// same question and used to answer it ten times. Measured on 2026-09-08: the +// two declarations were written out in full in all ten packages, and the +// function reading them - the same twelve lines, differing only in the format +// named in its message - stood in all ten as well. That is the shape thirteen +// packages had before one filler loop replaced their four, and the shape two +// containers had before the archive package replaced their two. +// +// What is shared is what cannot legitimately differ: the name, that it is a +// whole number, that the smallest picture is one pixel on a side, and that the +// number counts pixels. What each format supplies is what genuinely differs +// between them, which was measured before this package existed rather than +// guessed: +// +// - The largest side. Five formats reach 20000, AVIF and JXL 16384, WEBP +// 16383 and ICO 256, and each of those is the ceiling of the thing itself +// rather than a number somebody picked. +// - The sentence. An icon is not a picture and an SVG only SAYS how wide it +// is, so four different sentences are correct rather than four copies of +// one. A shared sentence would have made three of them wrong. +// - The default, which only SVG declares, because it is the only one that +// cannot work a size out from the bytes asked for. +// +// The shape is enforced by construction rather than by a check: a format +// cannot declare a width of a different kind or in a different unit, because +// it does not write those fields. What stops an ELEVENTH format declaring +// width its own way without using this package at all is +// TestOneSettingNameMeansOneKindOfSetting. +package imagedim + +import ( + "fmt" + "strconv" + + "github.com/donislawdev/TestingFilesGenerator/internal/format" +) + +// Setting names. Public names, so they are spelled once. +const ( + SettingWidth = "width" + SettingHeight = "height" +) + +const ( + // Smallest is the shortest side any picture can have. Zero is not a + // picture, and every format that has ever declared these agreed on one. + Smallest = 1 + // Unit is what the number counts, for the refusal and for a window's field. + Unit = "pixels" +) + +// Side is one dimension of a picture as one format takes it. +type Side struct { + // Largest is the longest side this format can encode. It is the ceiling of + // the format or of its encoder, not a limit chosen for comfort. + Largest int64 + // Default is what the format uses when nothing says otherwise, written the + // way a person writes it. Empty means the format works it out from the + // bytes it was asked for, which is what nine of the ten do. + Default string + // Detail is the one sentence a person reads, beside the field in a window + // and under the name in "tfg formats". It is per format on purpose - see + // the note above this package. + Detail string +} + +// Width is the declaration of the width setting for a format taking this side. +func Width(s Side) format.Property { return s.property(SettingWidth) } + +// Height is the declaration of the height setting for a format taking this side. +func Height(s Side) format.Property { return s.property(SettingHeight) } + +func (s Side) property(name string) format.Property { + return format.Property{ + Name: name, Kind: format.PropertyInt, + Min: Smallest, Max: s.Largest, Unit: Unit, + Default: s.Default, + Detail: s.Detail, + } +} + +// Value reads one side out of what a recipe stated, falling back to what the +// format works out for itself when nothing was said. +// +// The range is checked here as well as in the registry, and that is deliberate +// rather than left over. The registry refuses first on both surfaces - measured +// on 2026-09-08, where a width of 999999 comes back as exit 4 with the sentence +// built from the declaration, and nothing in internal/guard reaches a generator +// with a value outside its range. But this function is callable directly, a +// guard is such a caller, and a generator that trusts its input is one registry +// change away from encoding a picture nobody ordered. Same reasoning as the +// branches textenc.Parse keeps, and it costs one copy now rather than ten. +// +// The refusal is the structured one rather than a sentence, and that is the +// ten being unified on the BEST of them rather than on the most common. Nine +// built a string with fmt.Errorf and named the format inside it by hand. SVG +// returned format.PropertyValueError - the same type the registry itself +// raises, so what reports it can ask for the parts separately instead of +// parsing a sentence. The wording is SVG's, unchanged, because that is the one +// of the ten a reader could actually reach. +func Value(formatID, key string, props map[string]string, largest, fallback int) (int, error) { + raw, ok := props[key] + if !ok || raw == "" { + return fallback, nil + } + n, err := strconv.Atoi(raw) + if err != nil { + return 0, &format.PropertyValueError{ + Format: formatID, Key: key, Value: raw, + Reason: "it has to be a whole number of " + Unit, + } + } + if n < Smallest || n > largest { + return 0, &format.PropertyValueError{ + Format: formatID, Key: key, Value: raw, + Reason: fmt.Sprintf("it has to be between %d and %d", Smallest, largest), + } + } + return n, nil +} diff --git a/internal/format/jpg/jpg.go b/internal/format/jpg/jpg.go index 7c1a094..0738c28 100644 --- a/internal/format/jpg/jpg.go +++ b/internal/format/jpg/jpg.go @@ -23,6 +23,7 @@ import ( "github.com/donislawdev/TestingFilesGenerator/internal/core" "github.com/donislawdev/TestingFilesGenerator/internal/format" + "github.com/donislawdev/TestingFilesGenerator/internal/format/imagedim" "github.com/donislawdev/TestingFilesGenerator/internal/format/imagelabel" ) @@ -107,16 +108,10 @@ func init() { Label: format.LabelVisible, Oracle: "pillow", Properties: []format.Property{ - { - Name: "width", Kind: format.PropertyInt, - Min: minDimension, Max: maxDimension, Unit: "pixels", - Detail: "How wide the picture is. Left out, a size is chosen that fits the bytes you asked for.", - }, - { - Name: "height", Kind: format.PropertyInt, - Min: minDimension, Max: maxDimension, Unit: "pixels", - Detail: "How tall the picture is. Left out, a size is chosen that fits the bytes you asked for.", - }, + imagedim.Width(imagedim.Side{Largest: maxDimension, + Detail: "How wide the picture is. Left out, a size is chosen that fits the bytes you asked for."}), + imagedim.Height(imagedim.Side{Largest: maxDimension, + Detail: "How tall the picture is. Left out, a size is chosen that fits the bytes you asked for."}), { Name: "quality", Kind: format.PropertyInt, Min: minQuality, Max: maxQuality, Default: strconv.Itoa(defaultQuality), @@ -313,11 +308,11 @@ func chooseSize(r format.Request, label string, q int) (memo, error) { _, hSet := r.Properties["height"] if wSet || hSet { - w, err := dimension(r.Properties, "width", defaultWidth) + w, err := imagedim.Value("jpg", imagedim.SettingWidth, r.Properties, maxDimension, defaultWidth) if err != nil { return memo{}, err } - h, err := dimension(r.Properties, "height", defaultHeight) + h, err := imagedim.Value("jpg", imagedim.SettingHeight, r.Properties, maxDimension, defaultHeight) if err != nil { return memo{}, err } @@ -352,21 +347,6 @@ func chooseSize(r format.Request, label string, q int) (memo, error) { return smallest, nil } -func dimension(props map[string]string, key string, fallback int) (int, error) { - raw, ok := props[key] - if !ok || raw == "" { - return fallback, nil - } - n, err := strconv.Atoi(raw) - if err != nil { - return 0, fmt.Errorf("jpg: %s must be a whole number of pixels, got %q", key, raw) - } - if n < minDimension || n > maxDimension { - return 0, fmt.Errorf("jpg: %s must be between %d and %d pixels, got %d", key, minDimension, maxDimension, n) - } - return n, nil -} - func qualityOf(props map[string]string) (int, error) { raw, ok := props["quality"] if !ok || raw == "" { diff --git a/internal/format/jxl/jxl.go b/internal/format/jxl/jxl.go index 8b5ed0b..095d7ad 100644 --- a/internal/format/jxl/jxl.go +++ b/internal/format/jxl/jxl.go @@ -62,6 +62,7 @@ import ( "github.com/donislawdev/TestingFilesGenerator/internal/core" "github.com/donislawdev/TestingFilesGenerator/internal/format" + "github.com/donislawdev/TestingFilesGenerator/internal/format/imagedim" ) const ( @@ -168,16 +169,10 @@ func init() { Label: format.LabelVisible, Oracle: "pillow-jxl", Properties: []format.Property{ - { - Name: "width", Kind: format.PropertyInt, - Min: minDimension, Max: maxDimension, Unit: "pixels", - Detail: "How wide the picture is. Left out, a size is chosen that fits the bytes you asked for.", - }, - { - Name: "height", Kind: format.PropertyInt, - Min: minDimension, Max: maxDimension, Unit: "pixels", - Detail: "How tall the picture is. Left out, a size is chosen that fits the bytes you asked for.", - }, + imagedim.Width(imagedim.Side{Largest: maxDimension, + Detail: "How wide the picture is. Left out, a size is chosen that fits the bytes you asked for."}), + imagedim.Height(imagedim.Side{Largest: maxDimension, + Detail: "How tall the picture is. Left out, a size is chosen that fits the bytes you asked for."}), { Name: "quality", Kind: format.PropertyInt, Min: minQuality, Max: maxQuality, Default: strconv.Itoa(defaultQuality), @@ -387,11 +382,11 @@ func measuredSize(r format.Request, label string, q int) (memo, error) { // namedSize handles a recipe that asked for a picture of its own size. func namedSize(r format.Request, label string, q int) (memo, error) { - w, err := dimension(r.Properties, "width", sizeLadder[0].width) + w, err := imagedim.Value("jxl", imagedim.SettingWidth, r.Properties, maxDimension, sizeLadder[0].width) if err != nil { return memo{}, err } - h, err := dimension(r.Properties, "height", sizeLadder[0].height) + h, err := imagedim.Value("jxl", imagedim.SettingHeight, r.Properties, maxDimension, sizeLadder[0].height) if err != nil { return memo{}, err } @@ -431,21 +426,6 @@ func checkJointLimits(w, h int) error { return nil } -func dimension(props map[string]string, key string, fallback int) (int, error) { - raw, ok := props[key] - if !ok || raw == "" { - return fallback, nil - } - n, err := strconv.Atoi(raw) - if err != nil { - return 0, fmt.Errorf("jxl: %s must be a whole number of pixels, got %q", key, raw) - } - if n < minDimension || n > maxDimension { - return 0, fmt.Errorf("jxl: %s must be between %d and %d pixels, got %d", key, minDimension, maxDimension, n) - } - return n, nil -} - func quality(props map[string]string) (int, error) { raw, ok := props["quality"] if !ok || raw == "" { diff --git a/internal/format/png/png.go b/internal/format/png/png.go index 55ab2e6..c9003ba 100644 --- a/internal/format/png/png.go +++ b/internal/format/png/png.go @@ -14,10 +14,10 @@ import ( "image/color" stdpng "image/png" "io" - "strconv" "github.com/donislawdev/TestingFilesGenerator/internal/core" "github.com/donislawdev/TestingFilesGenerator/internal/format" + "github.com/donislawdev/TestingFilesGenerator/internal/format/imagedim" "github.com/donislawdev/TestingFilesGenerator/internal/format/imagelabel" ) @@ -53,7 +53,6 @@ const ( defaultWidth = 640 defaultHeight = 480 - minDimension = 1 maxDimension = 20000 // The picture is built in memory as one buffer and encoded twice, once @@ -91,16 +90,10 @@ func init() { Label: format.LabelVisible, Oracle: "pillow", Properties: []format.Property{ - { - Name: "width", Kind: format.PropertyInt, - Min: minDimension, Max: maxDimension, Unit: "pixels", - Detail: "How wide the picture is. Left out, a size is chosen that fits the bytes you asked for.", - }, - { - Name: "height", Kind: format.PropertyInt, - Min: minDimension, Max: maxDimension, Unit: "pixels", - Detail: "How tall the picture is. Left out, a size is chosen that fits the bytes you asked for.", - }, + imagedim.Width(imagedim.Side{Largest: maxDimension, + Detail: "How wide the picture is. Left out, a size is chosen that fits the bytes you asked for."}), + imagedim.Height(imagedim.Side{Largest: maxDimension, + Detail: "How tall the picture is. Left out, a size is chosen that fits the bytes you asked for."}), }, JointLimits: []format.JointLimit{{ Of: "width", By: "height", Max: maxPixels, @@ -344,11 +337,11 @@ func chooseSize(r format.Request, label string) (memo, error) { _, hSet := r.Properties["height"] if wSet || hSet { - w, err := dimension(r.Properties, "width", defaultWidth) + w, err := imagedim.Value("png", imagedim.SettingWidth, r.Properties, maxDimension, defaultWidth) if err != nil { return memo{}, err } - h, err := dimension(r.Properties, "height", defaultHeight) + h, err := imagedim.Value("png", imagedim.SettingHeight, r.Properties, maxDimension, defaultHeight) if err != nil { return memo{}, err } @@ -414,21 +407,6 @@ func chooseSize(r format.Request, label string) (memo, error) { return smallest, nil } -func dimension(props map[string]string, key string, fallback int) (int, error) { - raw, ok := props[key] - if !ok || raw == "" { - return fallback, nil - } - n, err := strconv.Atoi(raw) - if err != nil { - return 0, fmt.Errorf("png: %s must be a whole number of pixels, got %q", key, raw) - } - if n < minDimension || n > maxDimension { - return 0, fmt.Errorf("png: %s must be between %d and %d pixels, got %d", key, minDimension, maxDimension, n) - } - return n, nil -} - // picture builds the image. Deterministic from the seed, and compressible, so // that the encoded result is small next to any realistic requested size and // the padding chunk carries the difference. diff --git a/internal/format/svgfile/svg.go b/internal/format/svgfile/svg.go index b277d44..41644bd 100644 --- a/internal/format/svgfile/svg.go +++ b/internal/format/svgfile/svg.go @@ -17,6 +17,7 @@ import ( "github.com/donislawdev/TestingFilesGenerator/internal/core" "github.com/donislawdev/TestingFilesGenerator/internal/format" + "github.com/donislawdev/TestingFilesGenerator/internal/format/imagedim" ) // Measured on 2026-08-01, a comment holds arbitrary bytes to 1 MiB both after @@ -79,8 +80,12 @@ const ( // Width and Height name the two settings. Exported so that a guard presses // the key this format actually declares rather than a string spelled twice, // which is the same reason jsonfile exports the name of its layout setting. - Width = "width" - Height = "height" + // + // Taken from the package the picture formats share rather than spelled + // again here, so that a drawing and a photograph cannot end up naming the + // same setting differently. + Width = imagedim.SettingWidth + Height = imagedim.SettingHeight declaration = `` + "\n" rootClose = "\n" @@ -216,20 +221,14 @@ func init() { // later. Declaring none of them makes a recipe asking for one fail // loudly rather than quietly. Properties: []format.Property{ - { - Name: Width, Kind: format.PropertyInt, - Min: minDimension, Max: maxDimension, Unit: "pixels", + imagedim.Width(imagedim.Side{Largest: maxDimension, Default: strconv.Itoa(defaultWidth), Detail: "How wide the drawing says it is. Nothing is drawn into pixels here, " + - "so a large number costs a few bytes in the file and a great deal of memory in whatever opens it.", - }, - { - Name: Height, Kind: format.PropertyInt, - Min: minDimension, Max: maxDimension, Unit: "pixels", + "so a large number costs a few bytes in the file and a great deal of memory in whatever opens it."}), + imagedim.Height(imagedim.Side{Largest: maxDimension, Default: strconv.Itoa(defaultHeight), Detail: "How tall the drawing says it is. The label sits along the bottom edge, " + - "so a drawing shorter than that strip carries no visible label.", - }, + "so a drawing shorter than that strip carries no visible label."}), }, GeneratorVersion: generatorVersion, Generator: generator{}, @@ -245,39 +244,12 @@ type memo struct { height int } -// dimension reads one of the two size settings. -// -// The registry has already refused anything outside the declared range by the -// time a run reaches here, so the bounds below are a backstop for a caller -// that reaches the generator directly - the same belt the page count of PDF -// wears, and for the same reason. -func dimension(props map[string]string, name string, fallback int) (int, error) { - raw, ok := props[name] - if !ok || raw == "" { - return fallback, nil - } - n, err := strconv.Atoi(raw) - if err != nil { - return 0, &format.PropertyValueError{ - Format: "svg", Key: name, Value: raw, - Reason: "it has to be a whole number of pixels", - } - } - if n < minDimension || n > maxDimension { - return 0, &format.PropertyValueError{ - Format: "svg", Key: name, Value: raw, - Reason: fmt.Sprintf("it has to be between %d and %d", minDimension, maxDimension), - } - } - return n, nil -} - func (generator) Plan(r format.Request) (format.Plan, error) { - w, err := dimension(r.Properties, Width, defaultWidth) + w, err := imagedim.Value("svg", Width, r.Properties, maxDimension, defaultWidth) if err != nil { return format.Plan{}, err } - h, err := dimension(r.Properties, Height, defaultHeight) + h, err := imagedim.Value("svg", Height, r.Properties, maxDimension, defaultHeight) if err != nil { return format.Plan{}, err } diff --git a/internal/format/tiff/tiff.go b/internal/format/tiff/tiff.go index 220733b..52877cb 100644 --- a/internal/format/tiff/tiff.go +++ b/internal/format/tiff/tiff.go @@ -30,10 +30,10 @@ import ( "image" "image/color" "io" - "strconv" "github.com/donislawdev/TestingFilesGenerator/internal/core" "github.com/donislawdev/TestingFilesGenerator/internal/format" + "github.com/donislawdev/TestingFilesGenerator/internal/format/imagedim" "github.com/donislawdev/TestingFilesGenerator/internal/format/imagelabel" ) @@ -130,16 +130,10 @@ func init() { Label: format.LabelVisible, Oracle: "pillow", Properties: []format.Property{ - { - Name: "width", Kind: format.PropertyInt, - Min: minDimension, Max: maxDimension, Unit: "pixels", - Detail: "How wide the picture is. Left out, the picture is sized to fill the bytes you asked for.", - }, - { - Name: "height", Kind: format.PropertyInt, - Min: minDimension, Max: maxDimension, Unit: "pixels", - Detail: "How tall the picture is. Left out, the picture is sized to fill the bytes you asked for.", - }, + imagedim.Width(imagedim.Side{Largest: maxDimension, + Detail: "How wide the picture is. Left out, the picture is sized to fill the bytes you asked for."}), + imagedim.Height(imagedim.Side{Largest: maxDimension, + Detail: "How tall the picture is. Left out, the picture is sized to fill the bytes you asked for."}), }, GeneratorVersion: generatorVersion, Generator: generator{}, @@ -236,11 +230,11 @@ func chooseSize(r format.Request) (int, int, error) { _, hSet := r.Properties["height"] if wSet || hSet { - w, err := dimension(r.Properties, "width", 0) + w, err := imagedim.Value("tiff", imagedim.SettingWidth, r.Properties, maxDimension, 0) if err != nil { return 0, 0, err } - h, err := dimension(r.Properties, "height", 0) + h, err := imagedim.Value("tiff", imagedim.SettingHeight, r.Properties, maxDimension, 0) if err != nil { return 0, 0, err } @@ -323,21 +317,6 @@ func isqrt(n uint64) uint64 { return x } -func dimension(props map[string]string, key string, fallback int) (int, error) { - raw, ok := props[key] - if !ok || raw == "" { - return fallback, nil - } - n, err := strconv.Atoi(raw) - if err != nil { - return 0, fmt.Errorf("tiff: %s must be a whole number of pixels, got %q", key, raw) - } - if n < minDimension || n > maxDimension { - return 0, fmt.Errorf("tiff: %s must be between %d and %d pixels, got %d", key, minDimension, maxDimension, n) - } - return n, nil -} - func (generator) Write(ctx context.Context, w io.Writer, p format.Plan) error { m, ok := p.Memo.(memo) if !ok { diff --git a/internal/format/webp/webp.go b/internal/format/webp/webp.go index 8ca881c..95d4fc5 100644 --- a/internal/format/webp/webp.go +++ b/internal/format/webp/webp.go @@ -37,10 +37,10 @@ import ( "encoding/binary" "fmt" "io" - "strconv" "github.com/donislawdev/TestingFilesGenerator/internal/core" "github.com/donislawdev/TestingFilesGenerator/internal/format" + "github.com/donislawdev/TestingFilesGenerator/internal/format/imagedim" "github.com/donislawdev/TestingFilesGenerator/internal/format/imagelabel" ) @@ -102,16 +102,10 @@ func init() { Label: format.LabelVisible, Oracle: "pillow", Properties: []format.Property{ - { - Name: "width", Kind: format.PropertyInt, - Min: minDimension, Max: maxDimension, Unit: "pixels", - Detail: "How wide the picture is. Left out, the picture is sized to fill the bytes you asked for.", - }, - { - Name: "height", Kind: format.PropertyInt, - Min: minDimension, Max: maxDimension, Unit: "pixels", - Detail: "How tall the picture is. Left out, the picture is sized to fill the bytes you asked for.", - }, + imagedim.Width(imagedim.Side{Largest: maxDimension, + Detail: "How wide the picture is. Left out, the picture is sized to fill the bytes you asked for."}), + imagedim.Height(imagedim.Side{Largest: maxDimension, + Detail: "How tall the picture is. Left out, the picture is sized to fill the bytes you asked for."}), }, GeneratorVersion: generatorVersion, Generator: generator{}, @@ -230,11 +224,11 @@ func chooseSize(r format.Request) (int, int, error) { _, hSet := r.Properties["height"] if wSet || hSet { - w, err := dimension(r.Properties, "width", 0) + w, err := imagedim.Value("webp", imagedim.SettingWidth, r.Properties, maxDimension, 0) if err != nil { return 0, 0, err } - h, err := dimension(r.Properties, "height", 0) + h, err := imagedim.Value("webp", imagedim.SettingHeight, r.Properties, maxDimension, 0) if err != nil { return 0, 0, err } @@ -316,21 +310,6 @@ func isqrt(n uint64) uint64 { return x } -func dimension(props map[string]string, key string, fallback int) (int, error) { - raw, ok := props[key] - if !ok || raw == "" { - return fallback, nil - } - n, err := strconv.Atoi(raw) - if err != nil { - return 0, fmt.Errorf("webp: %s must be a whole number of pixels, got %q", key, raw) - } - if n < minDimension || n > maxDimension { - return 0, fmt.Errorf("webp: %s must be between %d and %d pixels, got %d", key, minDimension, maxDimension, n) - } - return n, nil -} - func (generator) Write(ctx context.Context, w io.Writer, p format.Plan) error { m, ok := p.Memo.(memo) if !ok { diff --git a/internal/guard/layers_test.go b/internal/guard/layers_test.go index 1740cf4..58cbd83 100644 --- a/internal/guard/layers_test.go +++ b/internal/guard/layers_test.go @@ -36,6 +36,7 @@ var layer = map[string]int{ "internal/format": 1, "internal/format/all": 1, "internal/format/imagelabel": 1, + "internal/format/imagedim": 1, "internal/format/textenc": 1, "internal/format/archive": 1, "internal/format/txt": 1, @@ -115,6 +116,7 @@ var sameLayerAllowed = map[string][]string{ "internal/format/wav", }, "internal/format/imagelabel": {"internal/format"}, + "internal/format/imagedim": {"internal/format"}, "internal/format/textenc": {"internal/format"}, "internal/format/archive": {"internal/format"}, "internal/format/txt": {"internal/format", "internal/format/imagelabel", "internal/format/textenc"}, @@ -124,23 +126,23 @@ var sameLayerAllowed = map[string][]string{ "internal/format/jsonfile": {"internal/format"}, "internal/format/xmlfile": {"internal/format", "internal/format/textenc"}, "internal/format/htmlfile": {"internal/format"}, - "internal/format/svgfile": {"internal/format"}, - "internal/format/bmp": {"internal/format", "internal/format/imagelabel"}, + "internal/format/svgfile": {"internal/format", "internal/format/imagedim"}, + "internal/format/bmp": {"internal/format", "internal/format/imagelabel", "internal/format/imagedim"}, "internal/format/opc": {"internal/format"}, "internal/format/docx": {"internal/format", "internal/format/opc"}, "internal/format/xlsx": {"internal/format", "internal/format/opc"}, "internal/format/pptx": {"internal/format", "internal/format/opc"}, - "internal/format/gif": {"internal/format", "internal/format/imagelabel"}, - "internal/format/ico": {"internal/format", "internal/format/imagelabel"}, - "internal/format/jpg": {"internal/format", "internal/format/imagelabel"}, - "internal/format/png": {"internal/format", "internal/format/imagelabel"}, + "internal/format/gif": {"internal/format", "internal/format/imagelabel", "internal/format/imagedim"}, + "internal/format/ico": {"internal/format", "internal/format/imagelabel", "internal/format/imagedim"}, + "internal/format/jpg": {"internal/format", "internal/format/imagelabel", "internal/format/imagedim"}, + "internal/format/png": {"internal/format", "internal/format/imagelabel", "internal/format/imagedim"}, "internal/format/pdf": {"internal/format", "internal/format/imagelabel"}, "internal/format/zip": {"internal/format", "internal/format/imagelabel", "internal/format/archive"}, "internal/format/targz": {"internal/format", "internal/format/archive"}, - "internal/format/tiff": {"internal/format", "internal/format/imagelabel"}, - "internal/format/webp": {"internal/format", "internal/format/imagelabel"}, - "internal/format/avif": {"internal/format", "internal/format/imagelabel"}, - "internal/format/jxl": {"internal/format", "internal/format/imagelabel"}, + "internal/format/tiff": {"internal/format", "internal/format/imagelabel", "internal/format/imagedim"}, + "internal/format/webp": {"internal/format", "internal/format/imagelabel", "internal/format/imagedim"}, + "internal/format/avif": {"internal/format", "internal/format/imagelabel", "internal/format/imagedim"}, + "internal/format/jxl": {"internal/format", "internal/format/imagelabel", "internal/format/imagedim"}, "internal/format/wav": {"internal/format", "internal/format/imagelabel"}, "internal/preset": {"internal/recipe"}, diff --git a/internal/guard/picturesides_test.go b/internal/guard/picturesides_test.go new file mode 100644 index 0000000..3ea625f --- /dev/null +++ b/internal/guard/picturesides_test.go @@ -0,0 +1,89 @@ +package guard + +import ( + "errors" + "testing" + + "github.com/donislawdev/TestingFilesGenerator/internal/format" + "github.com/donislawdev/TestingFilesGenerator/internal/format/imagedim" +) + +// The backstop under the ten picture formats has to refuse what it says it +// refuses. +// +// Ten formats used to carry their own copy of this and every copy checked the +// range. Measured on 2026-09-08, none of those checks could be reached: the +// registry refuses a width outside the declared bounds first, in engine.go and +// in recipe/target.go, which are the only two production callers, and a run +// asking png for a width of 999999 comes back as exit 4 with the sentence built +// from the declaration. Nothing in this package reached a generator with a +// value outside its range either. +// +// That is the shape this project calls a defence nothing can redden, and it had +// two honest endings. It was kept rather than deleted, for the reason +// textenc.Parse gives about its own branches - the function is callable +// directly and a generator that trusts its input is one registry change away +// from encoding a picture nobody ordered - and keeping it puts the burden here: +// a backstop with no test is a comment. This is the test, so the ten copies +// became one copy that something presses. +// +// What it does NOT claim is that a person can reach these. A person cannot, and +// the guards on the declaration are what prove the sentence they do get. +func TestThePictureSideBackstopRefusesWhatTheDeclarationWould(t *testing.T) { + const largest = 256 + + for _, c := range []struct { + name string + key string + value string + }{ + {"above the largest side", imagedim.SettingWidth, "257"}, + {"below the smallest side", imagedim.SettingHeight, "0"}, + {"a negative side", imagedim.SettingWidth, "-1"}, + {"not a number at all", imagedim.SettingHeight, "wide"}, + } { + t.Run(c.name, func(t *testing.T) { + got, err := imagedim.Value("ico", c.key, map[string]string{c.key: c.value}, largest, 32) + if err == nil { + t.Fatalf("%s=%q was accepted and came back as %d", c.key, c.value, got) + } + + // The structured refusal rather than a sentence, because whatever + // reports it asks for the parts by name. Nine of the ten built a + // string instead and the tenth did this. + var refused *format.PropertyValueError + if !errors.As(err, &refused) { + t.Fatalf("refused %s=%q with %T, which nothing can take apart: %v", c.key, c.value, err, err) + } + if refused.Format != "ico" { + t.Errorf("the refusal names %q rather than the format that asked", refused.Format) + } + if refused.Key != c.key { + t.Errorf("the refusal names the setting %q rather than %q", refused.Key, c.key) + } + if refused.Value != c.value { + t.Errorf("the refusal quotes %q rather than what was written, %q", refused.Value, c.value) + } + }) + } +} + +// A side inside the range is not refused, and that is the control. +// +// Without it the test above passes for a function that refuses everything, +// which would be a backstop that has become a wall - and a wall here means no +// picture format can be given a size at all. +func TestASideInsideTheRangeIsTakenAsWritten(t *testing.T) { + const largest = 256 + + for _, value := range []string{"1", "32", "256"} { + got, err := imagedim.Value("ico", imagedim.SettingWidth, + map[string]string{imagedim.SettingWidth: value}, largest, 99) + if err != nil { + t.Fatalf("width=%q is inside the range and was refused: %v", value, err) + } + if want := atoi(value); got != want { + t.Errorf("width=%q came back as %d", value, got) + } + } +} diff --git a/internal/guard/settingmeaning_test.go b/internal/guard/settingmeaning_test.go new file mode 100644 index 0000000..92e44ce --- /dev/null +++ b/internal/guard/settingmeaning_test.go @@ -0,0 +1,156 @@ +package guard + +import ( + "testing" + + "github.com/donislawdev/TestingFilesGenerator/internal/format" + _ "github.com/donislawdev/TestingFilesGenerator/internal/format/all" +) + +// declaredSetting is one format's declaration of one setting. +type declaredSetting struct { + format string + property format.Property +} + +// deliberateHomonyms are the names two formats spell alike and mean +// differently on purpose, with the reason each one is allowed. +// +// There is one, and it is here rather than absent because the guard that would +// have banned it was written on 2026-09-01 and deleted the same day for going +// red on it. "entry" is ordinary English and no package owns it - the note at +// the foot of archiveaxes_test.go carries that measurement in full. What this +// file refuses is an ACCIDENT wearing the same shape, so the deliberate case +// has to be sayable. +// +// A name written here is checked from both sides. It has to still be shared, +// and the formats sharing it have to still disagree, because an excuse that +// outlived its case is read by the next person as a rule. +var deliberateHomonyms = map[string]string{ + "entry_format": "log means the shape of a log line - apache-combined, nginx, syslog - " + + "where a container means the format of the files it holds", +} + +// One name has to describe one kind of setting. +// +// Twenty four formats declare seventy two settings between them under thirty +// nine names, and thirteen of those names are written by more than one format. +// Nothing compared them until 2026-09-08. What held them together was that +// somebody kept them alike by hand, which is the mechanism archiveaxes_test.go +// replaced for containers - and the one that let thirteen packages carry four +// different versions of a single filler loop. +// +// The drift this closes is quiet on both surfaces. "tfg formats" prints each +// declaration from the format that made it, and a window builds each field +// from that format's own Detail, so a width offered as free text by one format +// and as a bounded number by another draws two different fields with nothing +// said. A recipe moved between two formats then behaves differently for a +// reason nobody can see. +// +// What it asks is narrow, and the narrowness is measured rather than cautious. +// Three fields diverge legitimately today and are deliberately NOT asked about: +// +// - Max. width runs to 20000 for five formats, 16384 for AVIF and JXL, 16383 +// for WEBP and 256 for ICO, and each is the ceiling of the thing itself. +// - Default. quality is 60 for AVIF and 90 for JPG and JXL, and both carry +// weight: the AVIF ladder ceilings were measured at its default and the +// JPG minimum is the floor of an ordinary run at its own. +// - Min. columns starts at 2 for CSV and at 1 for XLSX. +// +// Kind and Unit are what is left, and they are where a typo lands: a unit of +// "px" beside nine of "pixels", or an eleventh picture format declaring width +// as free text. Neither moves a byte, and both would sit unnoticed until +// somebody read ten declarations side by side. +// +// Asked of every registered format and of every name any two of them share, so +// the fifteenth picture format and the twentieth text format are covered on the +// day they arrive rather than the day somebody remembers this file. +func TestOneSettingNameMeansOneKindOfSetting(t *testing.T) { + shared := settingsMoreThanOneFormatDeclares() + if len(shared) == 0 { + t.Fatal("no setting name is written by two formats, so this proved nothing") + } + + compared := 0 + for name, declared := range shared { + if _, deliberate := deliberateHomonyms[name]; deliberate { + continue + } + compared++ + first := declared[0] + for _, other := range declared[1:] { + if other.property.Kind != first.property.Kind { + t.Errorf("%q is a %s in %s and a %s in %s - one name has to describe one kind of setting, "+ + "or it is a homonym and belongs in deliberateHomonyms with the reason", + name, first.property.Kind, first.format, other.property.Kind, other.format) + } + if other.property.Unit != first.property.Unit { + t.Errorf("%q counts %q in %s and %q in %s - the same setting cannot count two things", + name, first.property.Unit, first.format, other.property.Unit, other.format) + } + } + } + + if compared == 0 { + t.Fatal("every shared name was excused as a homonym, so this proved nothing") + } +} + +// An excuse has to still describe the tree it excuses. +// +// The list above is the one place this guard can be told to look away, so it is +// the one place a stale entry turns into silence. Two ways it can go stale and +// they fail differently: the name stops being shared at all, which is an excuse +// with nothing behind it, or the formats sharing it come into agreement, which +// is an excuse still pointing at a case that has been fixed. Either way the +// next person reads it as a rule saying these names may drift. +// +// The same shape as the exception registry in notelemetry_test.go, for the same +// reason: consent that outlived its subject is consent nobody gave. +func TestEveryDeliberateHomonymStillNamesTwoMeanings(t *testing.T) { + shared := settingsMoreThanOneFormatDeclares() + + for name, why := range deliberateHomonyms { + declared, isShared := shared[name] + if !isShared { + t.Errorf("%q is excused as a homonym (%s) and no two formats declare it - "+ + "take the excuse off, an excuse with nothing behind it reads as a rule", name, why) + continue + } + if oneMeaning(declared) { + t.Errorf("%q is excused as a homonym (%s) and every format now declares it the same way - "+ + "take the excuse off, the case it names has been closed", name, why) + } + } +} + +// settingsMoreThanOneFormatDeclares is every setting name at least two +// registered formats write, with each declaration and the format that made it. +func settingsMoreThanOneFormatDeclares() map[string][]declaredSetting { + everywhere := map[string][]declaredSetting{} + for _, d := range format.All() { + for _, p := range d.Properties { + everywhere[p.Name] = append(everywhere[p.Name], declaredSetting{format: d.ID, property: p}) + } + } + + shared := make(map[string][]declaredSetting, len(everywhere)) + for name, declared := range everywhere { + if len(declared) > 1 { + shared[name] = declared + } + } + return shared +} + +// oneMeaning says whether every format declaring this name describes the same +// kind of setting by it. +func oneMeaning(declared []declaredSetting) bool { + first := declared[0].property + for _, other := range declared[1:] { + if other.property.Kind != first.Kind || other.property.Unit != first.Unit { + return false + } + } + return true +}