Repository: stephenh/ts-proto (ts)

Issue:
unexpected changes in `camelToSnake()` at v1.176.1
## Probrem

I noticed that the behavior of the `camelToSnake()` changed with the fix in https://github.com/stephenh/ts-proto/pull/1046.

Specifically, the behavior has changed when the string to be converted contains numbers. 
For example, if the package name includes a version like `v1`, it is affected.

With the following code:

```proto
package sample.v1;
```

- Prior to version 1.176.0, the generated code was:

```ts
export const SAMPLE_V1_PACKAGE_NAME = "sample.v1";
```

- In version 1.176.1, the generated code is now:

```ts
export const SAMPLE_V_1_PACKAGE_NAME = "sample.v1";
```

As a result, the generated constant name has changed, causing build errors.

To clarify, these codes are generated when using the `nestJs=true` option.

Upon reviewing the PR, it seems this behavior change was not intended. 
This unexpected change in a patch version has caused a lot of confusion.

---

That said, the changes in https://github.com/stephenh/ts-proto/pull/1046 appear reasonable. 
Reverting the commit does not seem to be a good solution.

I reviewed the documentation and code of the `case-anything` package, which is used internally by `camelToSnake()`, and it seems difficult to control the handling of numbers externally.

Moreover, there is no set specification on how to handle numbers in snake_case, and there are multiple opinions on the matter. Therefore, I don't think the behavior in v1.176.1 is incorrect.

How do you plan to address this issue?

If the behavior in v1.176.1 is considered correct, I believe it should be clearly documented, and a minor version update should be made.

Sorry I can't offer any concrete suggestions for improvement.

I am always grateful for this package. Thank you very much.

---


<details>
<summary>Environment Used for Testing</summary>

<br>

The information about the environment I used for testing is as follows:

```
├── proto
│   └── sample
│       └── v1
│           └── sample.proto
├── buf.gen.yaml
```

buf.gen.yaml
```yaml
version: v1
plugins:
  - name: ts
    out: gen/ts
    opt:
      - nestJs=true
    path: node_modules/ts-proto/protoc-gen-ts_proto
```

sample.proto
```proto
syntax = "proto3";

package sample.v1;

service SampleService {
  rpc GetSample(GetSampleRequest) returns (GetSampleResponse) {}
}

message GetSampleRequest {
  string id = 1;
}

message GetSampleResponse {
  string id = 1;
  string name = 2;
}
```

</details>

Pull request:
fix: Fix snake casing numbers.
Fixes #1048 

Patch:
diff --git a/src/case.ts b/src/case.ts
index 09e27041..ef1b2eff 100644
--- a/src/case.ts
+++ b/src/case.ts
@@ -1,4 +1,4 @@
-import { camelCase as camelCaseAnything, snakeCase as snakeCaseAnything } from "case-anything";
+import { camelCase as camelCaseAnything } from "case-anything";
 
 import { Options } from "./options";
 
@@ -24,7 +24,14 @@ export function snakeToCamel(s: string): string {
 }
 
 export function camelToSnake(s: string): string {
-  return snakeCaseAnything(s).toUpperCase();
+  return (
+    s
+      // any number or little-char -> big char
+      .replace(/[a-z0-9]([A-Z])/g, (m) => m[0] + "_" + m[1])
+      // any multiple big char -> next word
+      .replace(/[A-Z]([A-Z][a-z])/g, (m) => m[0] + "_" + m.substring(1))
+      .toUpperCase()
+  );
 }
 
 export function capitalize(s: string): string {


Test patch:
diff --git a/tests/case-test.ts b/tests/case-test.ts
index e9666926..e701d2e2 100644
--- a/tests/case-test.ts
+++ b/tests/case-test.ts
@@ -59,6 +59,10 @@ describe("case", () => {
     expect(camelToSnake("getAPIValue")).toEqual("GET_API_VALUE");
   });
 
+  it("converts string to snake case respecting number separation, getV1Foo === GET_V1_FOO", () => {
+    expect(camelToSnake("getV1Foo")).toEqual("GET_V1_FOO");
+  });
+
   describe("getFieldJsonName", () => {
     it("keeps snake case when jsonName is probably not set", () => {
       expect(getFieldJsonName({ name: "foo_bar", jsonName: "fooBar" }, { snakeToCamel: [], useJsonName: false })).toBe(