Skip to content

Commit

Permalink
Merge pull request #625 from cosmology-tech/feat/json-patch-proto
Browse files Browse the repository at this point in the history
Feat/json patch proto
  • Loading branch information
Zetazzz committed Jun 13, 2024
2 parents 5ac6e67 + f761921 commit 9f05dd0
Show file tree
Hide file tree
Showing 13 changed files with 390 additions and 20 deletions.
55 changes: 55 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ The following blockchain libraries (generated by Telescope) are available via np
- [Amino Encoding](#amino-encoding)
- [Prototypes Options](#prototypes-options)
- [Prototypes Methods](#prototypes-methods)
- [Enums Methods](#enums-options)
- [LCD Client Options](#lcd-client-options)
- [RPC Client Options](#rpc-client-options)
- [Stargate Client Options](#stargate-client-options)
Expand Down Expand Up @@ -79,6 +80,7 @@ The following blockchain libraries (generated by Telescope) are available via np
- [RPC Client Classes](#rpc-client-classes)
- [Instant RPC Methods](#instant-rpc-methods)
- [Manually registering types](#manually-registering-types)
- [JSON Patch Protos](#json-patch-protos)
- [CosmWasm](#cosmwasm)
- [Dependencies](#dependencies)
- [Troubleshooting](#troubleshooting)
Expand Down Expand Up @@ -314,6 +316,7 @@ telescope({
| `prototypes.addTypeUrlToObjects` | add typeUrl field to generated Decoders | `true` |
| `prototypes.enableRegistryLoader` | generate Registry loader to *.registry.ts files | `true` |
| `prototypes.enableMessageComposer` | generate MessageComposer to *.registry.ts files | `true` |
| `prototypes.patch` | An object mapping filenames to an array of `Operation` to be applied as patches to proto files during generation. See [JSON Patch Protos](#json-patch-protos) | `undefined`|

### Prototypes Methods

Expand All @@ -327,6 +330,13 @@ telescope({
| `prototypes.methods.fromSDK` | boolean to enable `fromSDK` method on proto objects | `false` |
| `prototypes.methods.toSDK` | boolean to enable `toSDK` method on proto objects | `false` |

### Enums Options

| option | description | defaults |
| ------------------------------------- | --------------------------------------------------------------- | ---------- |
| `enums.useCustomNames` | Enables the usage of custom names for enums if specified through proto options or annotations, allowing for more descriptive or project-specific naming conventions. | `false` |


### LCD Client Options

| option | description | defaults |
Expand Down Expand Up @@ -986,6 +996,51 @@ export const getCustomSigningClient = async ({ rpcEndpoint, signer }: { rpcEndpo
};
```

## JSON Patch Protos

The `prototypes.patch` configuration within the options object allows for dynamic modifications to protobuf definitions during code generation. This feature is designed to apply specific changes to proto files without altering the original source. By using JSON Patch operations such as `replace` and `add`, developers can customize the generated output to better fit project requirements when upstream SDK PRs are lagging or not in production.

Patches are specified as arrays of `Operation`s, where each operation is defined by:
- `op`: The operation type (`add` or `replace`).
- `path`: The JSON path to the target field, optionally prefixed with `@` to denote paths derived automatically from the package name, simplifying navigation within the proto file's structure.
- `value`: The new value to be set at the target location specified by the path.

Here is how these patches can be defined within the prototypes configuration:

```json
{
"prototypes": {
"patch": {
"cosmwasm/wasm/v1/types.proto": [
{
"op": "replace",
"path": "@/AccessType/valuesOptions/ACCESS_TYPE_UNSPECIFIED/(gogoproto.enumvalue_customname)",
"value": "UnspecifiedAccess"
},
{
"op": "replace",
"path": "@/AccessType/valuesOptions/ACCESS_TYPE_NOBODY/(gogoproto.enumvalue_customname)",
"value": "NobodyAccess"
},
{
"op": "add",
"path": "@/AccessType/values/ACCESS_TYPE_SUPER_FUN",
"value": 4
},
{
"op": "add",
"path": "@/AccessType/valuesOptions/ACCESS_TYPE_SUPER_FUN",
"value": {
"(gogoproto.enumvalue_customname)": "SuperFunAccessType"
}
}
]
}
}
}
```


## CosmWasm

Generate TypeScript SDKs for your CosmWasm smart contracts by using the `cosmwasm` option on `TelescopeOptions`. The `cosmwasm` option is actually a direct reference to the `TSBuilderInput` object, for the most up-to-date documentation, visit [@cosmwasm/ts-codegen](https://github.com/CosmWasm/ts-codegen).
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`createProtoEnum 1`] = `
"/** AccessType permission types */
export enum NullValue {
/** ACCESS_TYPE_UNSPECIFIED - AccessTypeUnspecified placeholder for empty value */
ACCESS_TYPE_UNSPECIFIED = 0,
/** ACCESS_TYPE_NOBODY - AccessTypeNobody forbidden */
ACCESS_TYPE_NOBODY = 1,
/** ACCESS_TYPE_ONLY_ADDRESS - AccessTypeOnlyAddress restricted to an address */
ACCESS_TYPE_ONLY_ADDRESS = 2,
/** ACCESS_TYPE_EVERYBODY - AccessTypeEverybody unrestricted */
ACCESS_TYPE_EVERYBODY = 3,
ACCESS_TYPE_SUPER_FUN = 4,
UNRECOGNIZED = -1,
}"
`;

exports[`createProtoEnumFromJSON 1`] = `
"export function accessTypeFromJSON(object: any): AccessType {
switch (object) {
case 0:
case "UnspecifiedAccess":
return AccessType.ACCESS_TYPE_UNSPECIFIED;
case 1:
case "NobodyAccess":
return AccessType.ACCESS_TYPE_NOBODY;
case 2:
case "OnlyAddressAccess":
return AccessType.ACCESS_TYPE_ONLY_ADDRESS;
case 3:
case "EverybodyAccess":
return AccessType.ACCESS_TYPE_EVERYBODY;
case 4:
case "SuperFunAccessType":
return AccessType.ACCESS_TYPE_SUPER_FUN;
case -1:
case "UNRECOGNIZED":
default:
return AccessType.UNRECOGNIZED;
}
}"
`;

exports[`createProtoEnumToJSON 1`] = `
"export function accessTypeToJSON(object: AccessType): string {
switch (object) {
case AccessType.ACCESS_TYPE_UNSPECIFIED:
return "UnspecifiedAccess";
case AccessType.ACCESS_TYPE_NOBODY:
return "NobodyAccess";
case AccessType.ACCESS_TYPE_ONLY_ADDRESS:
return "OnlyAddressAccess";
case AccessType.ACCESS_TYPE_EVERYBODY:
return "EverybodyAccess";
case AccessType.ACCESS_TYPE_SUPER_FUN:
return "SuperFunAccessType";
case AccessType.UNRECOGNIZED:
default:
return "UNRECOGNIZED";
}
}"
`;
5 changes: 3 additions & 2 deletions packages/ast/src/encoding/proto/proto.enum.custom.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@ import {
createProtoEnumFromJSON,
createProtoEnumToJSON,
} from "./enums";
import { getNestedProto } from "@cosmology/utils";
import {
getNestedProto
} from "@cosmology/utils";
import { ProtoParseContext } from "@cosmology/ast";
import { ProtoRoot } from "@cosmology/types";
import {
expectCode,
getTestProtoStore
Expand Down
80 changes: 80 additions & 0 deletions packages/ast/src/encoding/proto/proto.json.patch.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import {
createProtoEnum,
createProtoEnumFromJSON,
createProtoEnumToJSON,
} from "./enums";
import {
getNestedProto,
convertPackageNameToNestedJSONPath as convertPackage
} from "@cosmology/utils";
import { ProtoParseContext } from "@cosmology/ast";
import {
expectCode,
getTestProtoStore
} from "../../../test-utils";

const cp = convertPackage('cosmwasm.wasm.v1');
const store = getTestProtoStore({
prototypes: {
patch: {
'cosmwasm/wasm/v1/types.proto': [
{ op: 'replace', path: '@/AccessType/valuesOptions/ACCESS_TYPE_NOBODY/(gogoproto.enumvalue_customname)', value: 'NobodyAccess' },
{ op: 'replace', path: '@/AccessType/valuesOptions/ACCESS_TYPE_UNSPECIFIED/(gogoproto.enumvalue_customname)', value: 'UnspecifiedAccess' },
{ op: 'replace', path: cp + '/AccessType/valuesOptions/ACCESS_TYPE_EVERYBODY/(gogoproto.enumvalue_customname)', value: 'EverybodyAccess' },
{ op: 'replace', path: cp + '/AccessType/valuesOptions/ACCESS_TYPE_ONLY_ADDRESS/(gogoproto.enumvalue_customname)', value: 'OnlyAddressAccess' },
{
op: "add",
path: "/root/nested/cosmwasm/nested/wasm/nested/v1/nested/AccessType/values/ACCESS_TYPE_SUPER_FUN",
value: 4
},
{
op: "add",
path: "/root/nested/cosmwasm/nested/wasm/nested/v1/nested/AccessType/valuesOptions/ACCESS_TYPE_SUPER_FUN",
value: {
"(gogoproto.enumvalue_customname)": "SuperFunAccessType"
}
}
]
}
},
enums: {
useCustomNames: true
}
});
store.traverseAll();

it("createProtoEnum", async () => {
const ref = store.findProto('cosmwasm/wasm/v1/types.proto');
const context = new ProtoParseContext(ref, store, store.options);
expectCode(
createProtoEnum(
context,
"NullValue",
getNestedProto(ref.traversed).AccessType
)
);
});

it("createProtoEnumFromJSON", async () => {
const ref = store.findProto('cosmwasm/wasm/v1/types.proto');
const context = new ProtoParseContext(ref, store, store.options);
expectCode(
createProtoEnumFromJSON(
context,
"AccessType",
getNestedProto(ref.traversed).AccessType
)
);
});

it("createProtoEnumToJSON", async () => {
const ref = store.findProto('cosmwasm/wasm/v1/types.proto');
const context = new ProtoParseContext(ref, store, store.options);
expectCode(
createProtoEnumToJSON(
context,
"AccessType",
getNestedProto(ref.traversed).AccessType
)
);
});
3 changes: 2 additions & 1 deletion packages/parser/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -77,8 +77,9 @@
"@cosmology/types": "^1.7.0",
"@cosmology/utils": "^1.7.0",
"dotty": "0.1.2",
"fast-json-patch": "3.1.1",
"glob": "8.0.3",
"minimatch": "5.1.0",
"mkdirp": "3.0.0"
}
}
}
47 changes: 32 additions & 15 deletions packages/parser/src/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { sync as glob } from 'glob';
import { parse } from '@cosmology/protobufjs';
import { readFileSync } from 'fs';
import { join, resolve as pathResolve } from 'path';
import { ALLOWED_RPC_SERVICES, ProtoDep, ProtoField, ProtoRef, ProtoServiceMethod, ProtoType, TelescopeOptions, ENUM_PROTO2_DEFAULT, ENUM_PROTO3_DEFAULT } from '@cosmology/types';
import { ProtoDep, ProtoRef, ProtoServiceMethod, TelescopeOptions, ENUM_PROTO2_DEFAULT, ENUM_PROTO3_DEFAULT } from '@cosmology/types';
import { createTypeUrlTypeMap, getNestedProto, getPackageAndNestedFromStr, isRefIncluded, isRefExcluded } from './';
import { parseFullyTraversedProtoImports, symbolsToImportNames, traverse } from './traverse';
import { lookupAny, lookupAnyFromImports } from './lookup';
Expand All @@ -17,6 +17,8 @@ import google_field_mask from './native/field_mask';
import google_struct from './native/struct';
import google_wrappers from './native/wrappers';
import { ProtoResolver } from './resolver';
import { applyPatch } from 'fast-json-patch';
import { convertPackageNameToNestedJSONPath } from '@cosmology/utils';

const GOOGLE_PROTOS = [
['google/protobuf/any.proto', google_any],
Expand Down Expand Up @@ -62,8 +64,8 @@ export class ProtoStore implements IProtoStore {
_symbols: TraversalSymbol[] = [];

_enumValueMapping: Record<string, {
syntax: string;
valueSet: Set<number>;
syntax: string;
valueSet: Set<number>;
}> = {};

constructor(protoDirs: string[] = [], options: TelescopeOptions = defaultTelescopeOptions) {
Expand Down Expand Up @@ -102,11 +104,26 @@ export class ProtoStore implements IProtoStore {
processProtos(contents: { absolute: string, filename: string, content: string }[]) {
return contents.map(({ absolute, filename, content }) => {
try {
const proto = parseProto(content, this.options.prototypes.parser);
let protoJson = parseProto(content, this.options.prototypes.parser);
if (this.options.prototypes.patch && this.options.prototypes.patch[filename]) {
const ops = this.options.prototypes.patch[filename] ?? [];
try {
const result = applyPatch(protoJson, ops.map(op => {
if (op.path.startsWith('@')) {
op.path = convertPackageNameToNestedJSONPath(protoJson.package) + op.path.substring(1);
}
return op;
}));
protoJson = result.newDocument;
} catch (e2) {
console.error('JSON Patch error on proto: ' + filename);
}

}
return {
absolute,
filename,
proto,
proto: protoJson,
};
} catch (e) {
console.error(`${filename} has a proto syntax error`)
Expand Down Expand Up @@ -221,11 +238,11 @@ export class ProtoStore implements IProtoStore {

this.protos = this.getProtos().map((ref: ProtoRef) => {
const isHardExcluded = this.options?.prototypes?.excluded?.hardProtos && isRefExcluded(ref, {
protos: this.options?.prototypes?.excluded?.hardProtos
protos: this.options?.prototypes?.excluded?.hardProtos
})

if(isHardExcluded){
return null;
if (isHardExcluded) {
return null;
}

if (!actualFiles.has(ref.filename)) {
Expand Down Expand Up @@ -339,7 +356,7 @@ export class ProtoStore implements IProtoStore {
return packages;
}

setEnumValues(pkg: string, name: string, protoSyntex:string, values: number[]) {
setEnumValues(pkg: string, name: string, protoSyntex: string, values: number[]) {
this._enumValueMapping[`${pkg}.${name}`] = {
syntax: protoSyntex,
valueSet: new Set(values)
Expand All @@ -350,13 +367,13 @@ export class ProtoStore implements IProtoStore {
const enumObj = this._enumValueMapping[`${pkg}.${name}`];

if (enumObj?.syntax === 'proto2') {
if(enumObj?.valueSet?.has(ENUM_PROTO2_DEFAULT)){
return ENUM_PROTO2_DEFAULT;
}
if (enumObj?.valueSet?.has(ENUM_PROTO2_DEFAULT)) {
return ENUM_PROTO2_DEFAULT;
}
} else {
if(enumObj?.valueSet?.has(ENUM_PROTO3_DEFAULT)){
return ENUM_PROTO3_DEFAULT;
}
if (enumObj?.valueSet?.has(ENUM_PROTO3_DEFAULT)) {
return ENUM_PROTO3_DEFAULT;
}
}

return Math.min(...Array.from(enumObj?.valueSet ?? []));
Expand Down
Loading

0 comments on commit 9f05dd0

Please sign in to comment.