Design doc
diff --git a/etc/designs/20260116_buf_source_edit.md b/etc/designs/20260116_buf_source_edit.md new file mode 100644 index 0000000..55c386b --- /dev/null +++ b/etc/designs/20260116_buf_source_edit.md
@@ -0,0 +1,129 @@ +I want a new command, `buf source edit`, that allows editing of source files using the same logic as `buf format` (however, I want a new command). + +I want it to have the same first positional argument as `buf format`, and the following flags: + +``` +Flags: + --config string The buf.yaml file or data to use for configuration + -d, --diff Display diffs instead of rewriting files + --deprecate strings The prefix of the types (package, message, enum, extension, service, method) to deprecate. + When specified, all types under the prefix will have the `deprecated` option added to them. + --disable-symlinks Do not follow symlinks when reading sources or configuration from the local filesystem + By default, symlinks are followed in this CLI, but never followed on the Buf Schema Registry + --error-format string The format for build errors printed to stderr. Must be one of [text,json,msvs,junit,github-actions,gitlab-code-quality] (default "text") + --exclude-path strings Exclude specific files or directories, e.g. "proto/a/a.proto", "proto/a" + If specified multiple times, the union is taken + -h, --help help for format + --path strings Limit to specific files or directories, e.g. "proto/a/a.proto", "proto/a" + If specified multiple times, the union is taken +``` + +As opposed to `buf format`, there is no `--exit-code`, `-o`, or `-w` flags, and I've added the `--deprecate` flag. The behavior of `-w` is the default for `buf source edit`. + +Internally, this should just use the `bufformat` package, and largely copy the `buf format` command, however additional functional options should be added to the `bufformat` package to allow for things like deprecation. You'll likely want to have something like `func WithDeprecate(fqnPrefix string) FormatOption` that will result in `deprecated` options being added to any type that has the given fully-qualified name prefix. + +For example, if I had services, messages, and enums under `package foo.bar.baz` and I gave `foo.bar` as the `fqnPrefix`, this will result in `deprecated` options being set for the package, all services, all enums, all messages, all RPCs. We will be opinionated and choose *NOT* to deprecate enum values or fields within a message UNLESS they are explicitly specified - recursion does not hit enum values or fields. That is, if I specify a package `foo.bar.baz`, then all files, enums, messages, services, RPCs are deprecated, but not enum values or fields. If I specified `foo.bar.baz.SomeMessage.some_field`, then that field is deprecated. + +To implement that logic, you'll likely want to have `bufformat` only deprecate an enum value or field if `fqnPrefix == fqn`, as opposed to `strings.HasPrefix`. + +Also, optimally, you will break FQN into components, so that `foo.bar.b` does not match `foo.bar.baz`, but `foo.bar` does match. + +There is a lot of commonality between this command and `buf format`, like 90%. You may want to abstract it to a new package `cmd/buf/internal/command/internal`, given that really it's just a matter of what options you apply. The diff is that `buf format` allows exit code differences and a different output location, and defaults to printing to stdout instead of rewriting in place. Otherwise, this is all the same. + +Example before and after in the `foo.bar.baz` base described above: + +Before: + +```proto + +syntax = "proto3"; + +package foo.bar.baz; + +enum One { + ONE_UNSPECIFIED = 0; + ONE_ONE = 1; +} + +message Two { + enum Three { + THREE_UNSPECIFIED = 0; + THREE_ONE = 1; + } + message Four {} + + string id = 1; +} + +service Five { + rpc Six(Two) returns (Two); +} +``` + +After if `foo.bar` is specified for `--deprecate`: + +```proto + +syntax = "proto3"; + +package foo.bar.baz; + +option deprecated = true; + +enum One { + option deprecated = true; + ONE_UNSPECIFIED = 0; + ONE_ONE = 1; +} + +message Two { + option deprecated = true; + + enum Three { + option deprecated = true; + + THREE_UNSPECIFIED = 0; + THREE_ONE = 1; + } + message Four { + option deprecated = true; + } + + string id = 1; +} + +service Five { + option deprecated = true; + rpc Six(Two) returns (Two) { + option deprecated = true; + } +} +``` + +After if `foo.bar.Two.id` is specified for `--deprecate`: + +```proto + +syntax = "proto3"; + +package foo.bar.baz; + +enum One { + ONE_UNSPECIFIED = 0; + ONE_ONE = 1; +} + +message Two { + enum Three { + THREE_UNSPECIFIED = 0; + THREE_ONE = 1; + } + message Four {} + + string id = 1 [deprecated = true]; +} + +service Five { + rpc Six(Two) returns (Two); +} +```