diff --git a/.gitignore b/.gitignore index 800562bb..cc1e7c88 100644 --- a/.gitignore +++ b/.gitignore @@ -10,4 +10,6 @@ private/ # docs are generated in ci docs -*.tsbuildinfo \ No newline at end of file +coverage +.rollup.cache +*.tsbuildinfo diff --git a/packages/tl-utils/src/__snapshots__/diff.test.ts.snap b/packages/tl-utils/src/__snapshots__/diff.test.ts.snap new file mode 100644 index 00000000..feb1186d --- /dev/null +++ b/packages/tl-utils/src/__snapshots__/diff.test.ts.snap @@ -0,0 +1,574 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`generateTlSchemasDifference > shows added constructors 1`] = ` +{ + "classes": { + "added": [ + { + "arguments": [], + "id": 3847402009, + "kind": "class", + "name": "test2", + "type": "Test", + }, + ], + "modified": [], + "removed": [], + }, + "methods": { + "added": [], + "modified": [], + "removed": [], + }, + "unions": { + "added": [], + "modified": [ + { + "classes": { + "added": [ + { + "arguments": [], + "id": 3847402009, + "kind": "class", + "name": "test2", + "type": "Test", + }, + ], + "modified": [], + "removed": [], + }, + "methods": { + "added": [], + "modified": [], + "removed": [], + }, + "name": "Test", + }, + ], + "removed": [], + }, +} +`; + +exports[`generateTlSchemasDifference > shows added unions 1`] = ` +{ + "classes": { + "added": [ + { + "arguments": [], + "id": 3739166976, + "kind": "class", + "name": "test1", + "type": "Test1", + }, + ], + "modified": [ + { + "arguments": { + "added": [], + "modified": [ + { + "name": "foo", + "type": { + "new": "Foo", + "old": "int", + }, + }, + ], + "removed": [], + }, + "id": { + "new": 3348640942, + "old": 1331975629, + }, + "name": "test", + }, + ], + "removed": [], + }, + "methods": { + "added": [], + "modified": [], + "removed": [], + }, + "unions": { + "added": [ + { + "classes": [ + { + "arguments": [], + "id": 3739166976, + "kind": "class", + "name": "test1", + "type": "Test1", + }, + ], + "name": "Test1", + }, + ], + "modified": [], + "removed": [], + }, +} +`; + +exports[`generateTlSchemasDifference > shows modified constructors 1`] = ` +{ + "classes": { + "added": [], + "modified": [ + { + "arguments": { + "added": [], + "modified": [ + { + "name": "foo", + "type": { + "new": "Foo", + "old": "int", + }, + }, + ], + "removed": [], + }, + "id": { + "new": 3348640942, + "old": 1331975629, + }, + "name": "test", + }, + ], + "removed": [], + }, + "methods": { + "added": [], + "modified": [], + "removed": [], + }, + "unions": { + "added": [], + "modified": [], + "removed": [], + }, +} +`; + +exports[`generateTlSchemasDifference > shows modified methods 1`] = ` +{ + "classes": { + "added": [], + "modified": [], + "removed": [], + }, + "methods": { + "added": [], + "modified": [ + { + "id": { + "new": 3994885231, + "old": 471282454, + }, + "name": "test", + }, + ], + "removed": [], + }, + "unions": { + "added": [ + undefined, + ], + "modified": [], + "removed": [ + undefined, + ], + }, +} +`; + +exports[`generateTlSchemasDifference > shows modified unions 1`] = ` +{ + "classes": { + "added": [ + { + "arguments": [], + "id": 3847402009, + "kind": "class", + "name": "test2", + "type": "Test", + }, + ], + "modified": [ + { + "arguments": { + "added": [], + "modified": [ + { + "name": "foo", + "type": { + "new": "Foo", + "old": "int", + }, + }, + ], + "removed": [], + }, + "id": { + "new": 3348640942, + "old": 1331975629, + }, + "name": "test", + }, + ], + "removed": [ + { + "arguments": [], + "id": 1809692154, + "kind": "class", + "name": "test1", + "type": "Test", + }, + ], + }, + "methods": { + "added": [], + "modified": [], + "removed": [], + }, + "unions": { + "added": [], + "modified": [ + { + "classes": { + "added": [ + { + "arguments": [], + "id": 3847402009, + "kind": "class", + "name": "test2", + "type": "Test", + }, + ], + "modified": [], + "removed": [ + { + "arguments": [], + "id": 1809692154, + "kind": "class", + "name": "test1", + "type": "Test", + }, + ], + }, + "methods": { + "added": [], + "modified": [], + "removed": [], + }, + "name": "Test", + }, + ], + "removed": [], + }, +} +`; + +exports[`generateTlSchemasDifference > shows modified unions 2`] = ` +{ + "classes": { + "added": [ + { + "arguments": [ + { + "name": "foo", + "type": "Foo", + }, + ], + "id": 711487159, + "kind": "class", + "name": "test2", + "type": "Test", + }, + { + "arguments": [], + "id": 704164487, + "kind": "class", + "name": "test3", + "type": "Test", + }, + ], + "modified": [], + "removed": [ + { + "arguments": [ + { + "name": "foo", + "type": "int", + }, + ], + "id": 1331975629, + "kind": "class", + "name": "test", + "type": "Test", + }, + { + "arguments": [], + "id": 1809692154, + "kind": "class", + "name": "test1", + "type": "Test", + }, + ], + }, + "methods": { + "added": [], + "modified": [], + "removed": [], + }, + "unions": { + "added": [], + "modified": [ + { + "classes": { + "added": [ + { + "arguments": [ + { + "name": "foo", + "type": "Foo", + }, + ], + "id": 711487159, + "kind": "class", + "name": "test2", + "type": "Test", + }, + { + "arguments": [], + "id": 704164487, + "kind": "class", + "name": "test3", + "type": "Test", + }, + ], + "modified": [], + "removed": [ + { + "arguments": [ + { + "name": "foo", + "type": "int", + }, + ], + "id": 1331975629, + "kind": "class", + "name": "test", + "type": "Test", + }, + { + "arguments": [], + "id": 1809692154, + "kind": "class", + "name": "test1", + "type": "Test", + }, + ], + }, + "methods": { + "added": [], + "modified": [], + "removed": [], + }, + "name": "Test", + }, + ], + "removed": [], + }, +} +`; + +exports[`generateTlSchemasDifference > shows modified unions 3`] = ` +{ + "classes": { + "added": [], + "modified": [ + { + "id": { + "new": 1997819349, + "old": 471282454, + }, + "name": "test", + }, + { + "id": { + "new": 3739166976, + "old": 1809692154, + }, + "name": "test1", + }, + ], + "removed": [], + }, + "methods": { + "added": [], + "modified": [], + "removed": [], + }, + "unions": { + "added": [ + { + "classes": [ + { + "arguments": [], + "id": 1997819349, + "kind": "class", + "name": "test", + "type": "Test1", + }, + { + "arguments": [], + "id": 3739166976, + "kind": "class", + "name": "test1", + "type": "Test1", + }, + ], + "name": "Test1", + }, + ], + "modified": [], + "removed": [ + { + "classes": [ + { + "arguments": [], + "id": 471282454, + "kind": "class", + "name": "test", + "type": "Test", + }, + { + "arguments": [], + "id": 1809692154, + "kind": "class", + "name": "test1", + "type": "Test", + }, + ], + "name": "Test", + }, + ], + }, +} +`; + +exports[`generateTlSchemasDifference > shows removed constructors 1`] = ` +{ + "classes": { + "added": [], + "modified": [], + "removed": [ + { + "arguments": [], + "id": 3847402009, + "kind": "class", + "name": "test2", + "type": "Test", + }, + ], + }, + "methods": { + "added": [], + "modified": [], + "removed": [], + }, + "unions": { + "added": [], + "modified": [ + { + "classes": { + "added": [], + "modified": [], + "removed": [ + { + "arguments": [], + "id": 3847402009, + "kind": "class", + "name": "test2", + "type": "Test", + }, + ], + }, + "methods": { + "added": [], + "modified": [], + "removed": [], + }, + "name": "Test", + }, + ], + "removed": [], + }, +} +`; + +exports[`generateTlSchemasDifference > shows removed unions 1`] = ` +{ + "classes": { + "added": [], + "modified": [ + { + "arguments": { + "added": [], + "modified": [ + { + "name": "foo", + "type": { + "new": "Foo", + "old": "int", + }, + }, + ], + "removed": [], + }, + "id": { + "new": 3348640942, + "old": 1331975629, + }, + "name": "test", + }, + ], + "removed": [ + { + "arguments": [], + "id": 3739166976, + "kind": "class", + "name": "test1", + "type": "Test1", + }, + ], + }, + "methods": { + "added": [], + "modified": [], + "removed": [], + }, + "unions": { + "added": [], + "modified": [], + "removed": [ + { + "classes": [ + { + "arguments": [], + "id": 3739166976, + "kind": "class", + "name": "test1", + "type": "Test1", + }, + ], + "name": "Test1", + }, + ], + }, +} +`; diff --git a/packages/tl-utils/src/__snapshots__/utils.test.ts.snap b/packages/tl-utils/src/__snapshots__/utils.test.ts.snap new file mode 100644 index 00000000..e0737422 --- /dev/null +++ b/packages/tl-utils/src/__snapshots__/utils.test.ts.snap @@ -0,0 +1,31 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`groupTlEntriesByNamespace > should group entries correctly 1`] = ` +{ + "": [ + { + "arguments": [], + "id": 0, + "kind": "class", + "name": "bar", + "type": "Bar", + }, + ], + "foo": [ + { + "arguments": [], + "id": 0, + "kind": "class", + "name": "foo.bar", + "type": "FooBar", + }, + { + "arguments": [], + "id": 0, + "kind": "class", + "name": "foo.baz", + "type": "FooBaz", + }, + ], +} +`; diff --git a/packages/tl-utils/src/calculator.test.ts b/packages/tl-utils/src/calculator.test.ts index 249f2ec2..f19fe044 100644 --- a/packages/tl-utils/src/calculator.test.ts +++ b/packages/tl-utils/src/calculator.test.ts @@ -26,11 +26,15 @@ describe('calculateStaticSizes', () => { it('correctly skips constructors with predicated fields', () => { test( - 'help.promoData#8c39793f flags:# proxy:flags.0?true expires:int peer:Peer psa_type:flags.1?string psa_message:flags.2?string = help.PromoData;', + 'help.promoData#8c39793f flags:# expires:int psa_type:flags.1?int psa_message:flags.2?int = help.PromoData;', {}, ) }) + it('correctly skips constructors with generic fields', () => { + test('invokeWithLayer {T:X} = !X;', {}) + }) + it('correctly skips constructors with non-static fields', () => { test('help.promoData#8c39793f psa_type:string psa_message:string = help.PromoData;', {}) }) @@ -62,4 +66,33 @@ describe('calculateStaticSizes', () => { }, ) }) + + it('correctly handles differently sized union children', () => { + test( + 'peerUser user_id:int53 = Peer;\n' + + 'peerChannel channel_id:int53 access_hash:long = Peer;\n' + + 'help.promoData#8c39793f flags:# proxy:flags.0?true expires:int peer:Peer = help.PromoData;', + { + peerUser: 12, + peerChannel: 20, + }, + ) + }) + + it('correctly handles non static-sized union children', () => { + test( + 'peerUser user_id:int53 = Peer;\n' + + 'peerChannel channel_id:int53 access_hash:bytes = Peer;\n' + + 'help.promoData#8c39793f flags:# proxy:flags.0?true expires:int peer:Peer = help.PromoData;', + { + peerUser: 12, + }, + ) + test( + 'peerUser user_id:int53 access_hash:bytes = Peer;\n' + + 'peerChannel channel_id:int53 access_hash:bytes = Peer;\n' + + 'help.promoData#8c39793f flags:# proxy:flags.0?true expires:int peer:Peer = help.PromoData;', + {}, + ) + }) }) diff --git a/packages/tl-utils/src/codegen/__snapshots__/errors.test.ts.snap b/packages/tl-utils/src/codegen/__snapshots__/errors.test.ts.snap new file mode 100644 index 00000000..38fec916 --- /dev/null +++ b/packages/tl-utils/src/codegen/__snapshots__/errors.test.ts.snap @@ -0,0 +1,73 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`generateCodeForErrors > should correctly generate errors 1`] = ` +[ + "type MtErrorText = + | 'USER_NOT_FOUND' + | 'FLOOD_WAIT_%d' + + | (string & {}) // to keep hints + +interface MtErrorArgMap { + 'FLOOD_WAIT_%d': { duration: number }, + +} + +type RpcErrorWithArgs = + RpcError & { text: T } & (T extends keyof MtErrorArgMap ? (RpcError & MtErrorArgMap[T]) : {}); + +export class RpcError extends Error { + static BAD_REQUEST: 400; + + + readonly code: number; + readonly text: MtErrorText; + readonly unknown: boolean; + constructor(code: number, text: MtErrorText); + + is(text: T): this is RpcErrorWithArgs; + static is(err: unknown): err is RpcError; + static is(err: unknown, text: T): err is RpcErrorWithArgs; + static create(code: number, text: T): RpcErrorWithArgs; + static fromTl(obj: object): RpcError; +} +", + "const _descriptionsMap = JSON.parse('{\\"FLOOD_WAIT_%d\\":\\"Wait %d seconds\\"}') +class RpcError extends Error { + constructor(code, text, description) { + super(description || 'Unknown RPC error: [' + code + ':' + text + ']'); + this.code = code; + this.text = text; + } + + static is(err, text) { return err.constructor === RpcError && (!text || err.text === text); } + is(text) { return this.text === text; } +} +RpcError.fromTl = function (obj) { + if (obj.errorMessage in _descriptionsMap) { + return new RpcError(obj.errorCode, obj.errorMessage, _descriptionsMap[obj.errorMessage]); + } + + var err = new RpcError(obj.errorCode, obj.errorMessage); + var match; + if ((match=err.text.match(/^FLOOD_WAIT_(\\\\d+)$/))!=null){ err.text = 'FLOOD_WAIT_%d'; err.duration = parseInt(match[1]) } + + else return err + + err.message = _descriptionsMap[err.text]; + return err +} +RpcError.create = function(code, text) { + var desc = _descriptionsMap[text]; + var err = new RpcError(code, text, desc); + if (!desc) { + err.unknown = true; + } + return err; +} +RpcError.BAD_REQUEST = 400; + +exports.RpcError = RpcError; +", +] +`; diff --git a/packages/tl-utils/src/codegen/__snapshots__/reader.test.ts.snap b/packages/tl-utils/src/codegen/__snapshots__/reader.test.ts.snap new file mode 100644 index 00000000..a38c8854 --- /dev/null +++ b/packages/tl-utils/src/codegen/__snapshots__/reader.test.ts.snap @@ -0,0 +1,90 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`generateReaderCodeForTlEntries > doesn't generate code for methods by default 1`] = ` +"var m={ +471282454:function(r){return{_:'test'}}, +}" +`; + +exports[`generateReaderCodeForTlEntries > generates code for methods if asked to 1`] = ` +"var m={ +471282454:function(r){return{_:'test'}}, +2119910527:function(r){return{_:'test2'}}, +}" +`; + +exports[`generateReaderCodeForTlEntries > generates code for multiple entries 1`] = ` +"var m={ +471282454:function(r){return{_:'test'}}, +2119910527:function(r){return{_:'test2'}}, +}" +`; + +exports[`generateReaderCodeForTlEntries > method return readers > doesn't include Bool parsing 1`] = ` +"var m={ +_results:{ +}, +}" +`; + +exports[`generateReaderCodeForTlEntries > method return readers > includes primitive return type parsing info 1`] = ` +"var m={ +1809692154:function(r){return{_:'test1'}}, +_results:{ +'test':function(r){return r.int()}, +}, +}" +`; + +exports[`generateReaderCodeForTlEntries > method return readers > includes primitive vectors return type parsing info 1`] = ` +"var m={ +_results:{ +'test':function(r){return r.vector(r.int)}, +}, +}" +`; + +exports[`generateReaderCodeForTlEntries > method return readers > includes primitive vectors return type parsing info 2`] = ` +"var m={ +_results:{ +'test':function(r){return r.vector(r.int)}, +}, +}" +`; + +exports[`generateReaderCodeForTlEntries > updates readers used in bare vectors 1`] = ` +"var m={ +471282454:function(r=this){return{_:'test'}}, +3562390222:function(r){return{_:'test2',a:r.vector(m[471282454],1),}}, +}" +`; + +exports[`generateReaderCodeForTlEntry > generates code for bare types 1`] = `"1945237724:function(r){return{_:'msg_container',messages:r.vector(m[155834844],1),}},"`; + +exports[`generateReaderCodeForTlEntry > generates code for bare types 2`] = `"2924480661:function(r){return{_:'future_salts',salts:r.vector(m[155834844]),current:r.object(),}},"`; + +exports[`generateReaderCodeForTlEntry > generates code for bare types 3`] = `"2924480661:function(r){return{_:'future_salts',salts:r.vector(m[155834844],1),current:m[155834844](r),}},"`; + +exports[`generateReaderCodeForTlEntry > generates code for constructors with arguments before flags field 1`] = `"2262925665:function(r){var id=r.long(),flags=r.uint();return{_:'poll',id:id,quiz:!!(flags&8),question:r.string(),}},"`; + +exports[`generateReaderCodeForTlEntry > generates code for constructors with generics 1`] = `"3667594509:function(r){return{_:'invokeWithLayer',layer:r.int(),query:r.object(),}},"`; + +exports[`generateReaderCodeForTlEntry > generates code for constructors with multiple flags fields 1`] = `"1041346555:function(r){var flags=r.uint(),pts=r.int(),timeout=flags&2?r.int():void 0,flags2=r.uint();return{_:'updates.channelDifferenceEmpty',final:!!(flags&1),pts:pts,timeout:timeout,canDeleteChannel:!!(flags2&1),}},"`; + +exports[`generateReaderCodeForTlEntry > generates code for constructors with optional arguments 1`] = `"1041346555:function(r){var flags=r.uint();return{_:'updates.channelDifferenceEmpty',final:!!(flags&1),pts:r.int(),timeout:flags&2?r.int():void 0,}},"`; + +exports[`generateReaderCodeForTlEntry > generates code for constructors with optional vector arguments 1`] = `"2338894028:function(r){var flags=r.uint();return{_:'messages.getWebPagePreview',message:r.string(),entities:flags&8?r.vector(r.object):void 0,}},"`; + +exports[`generateReaderCodeForTlEntry > generates code for constructors with simple arguments 1`] = `"2299280777:function(r){return{_:'inputBotInlineMessageID',dcId:r.int(),id:r.long(),accessHash:r.long(),}},"`; + +exports[`generateReaderCodeForTlEntry > generates code for constructors with simple arguments 2`] = `"341499403:function(r){return{_:'contact',userId:r.long(),mutual:r.boolean(),}},"`; + +exports[`generateReaderCodeForTlEntry > generates code for constructors with simple arguments 3`] = `"2933316530:function(r){return{_:'maskCoords',n:r.int(),x:r.double(),y:r.double(),zoom:r.double(),}},"`; + +exports[`generateReaderCodeForTlEntry > generates code for constructors with true flags 1`] = `"649453030:function(r){var flags=r.uint();return{_:'messages.messageEditData',caption:!!(flags&1),}},"`; + +exports[`generateReaderCodeForTlEntry > generates code for constructors with vector arguments 1`] = `"2131196633:function(r){return{_:'contacts.resolvedPeer',peer:r.object(),chats:r.vector(r.object),users:r.vector(r.object),}},"`; + +exports[`generateReaderCodeForTlEntry > generates code for constructors without arguments 1`] = `"2875595611:function(r){return{_:'topPeerCategoryBotsPM'}},"`; + +exports[`generateReaderCodeForTlEntry > generates code with raw flags for constructors with flags 1`] = `"1554225816:function(r){var flags=r.uint(),flags2=r.uint();return{_:'test',flags:flags,flags2:flags2,}},"`; diff --git a/packages/tl-utils/src/codegen/__snapshots__/types.test.ts.snap b/packages/tl-utils/src/codegen/__snapshots__/types.test.ts.snap new file mode 100644 index 00000000..b152a01d --- /dev/null +++ b/packages/tl-utils/src/codegen/__snapshots__/types.test.ts.snap @@ -0,0 +1,292 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`generateTypescriptDefinitionsForTlEntry > comments > adds return type comments 1`] = ` +"/** + * This is a test method + * + * RPC method returns {@link tl.TypeTest} + */ +interface RawTestRequest { + _: 'test'; +}" +`; + +exports[`generateTypescriptDefinitionsForTlEntry > comments > adds return type comments 2`] = ` +"/** + * This is a test method + * + * RPC method returns {@link tl.TypeTest} + */ +interface RawTestRequest { + _: 'test'; +}" +`; + +exports[`generateTypescriptDefinitionsForTlEntry > comments > adds return type comments 3`] = ` +"/** + * RPC method returns {@link tl.TypeTest} array + */ +interface RawTestRequest { + _: 'test'; +}" +`; + +exports[`generateTypescriptDefinitionsForTlEntry > comments > adds tdlib style comments 1`] = ` +"/** + * This is a test constructor + */ +interface RawTest { + _: 'test'; + /** + * Some field + */ + field: number; +}" +`; + +exports[`generateTypescriptDefinitionsForTlEntry > comments > adds tl style comments 1`] = ` +"/** + * This is a test constructor + */ +interface RawTest { + _: 'test'; +}" +`; + +exports[`generateTypescriptDefinitionsForTlEntry > comments > adds usage info comments 1`] = ` +"/** + * RPC method returns {@link tl.TypeTest} + * + * This method is **not** available for bots + * + * This method *may* throw one of these errors: FOO, BAR + */ +interface RawTestRequest { + _: 'test'; +}" +`; + +exports[`generateTypescriptDefinitionsForTlEntry > comments > adds usage info comments 2`] = ` +"/** + * RPC method returns {@link tl.TypeTest} + * + * This method is **not** available for normal users + */ +interface RawTestBotRequest { + _: 'testBot'; +}" +`; + +exports[`generateTypescriptDefinitionsForTlEntry > comments > should not break @link tags 1`] = ` +"/** + * This is a test constructor with a very long comment + * {@link whatever} more text + */ +interface RawTest { + _: 'test'; +}" +`; + +exports[`generateTypescriptDefinitionsForTlEntry > comments > wraps long comments 1`] = ` +"/** + * This is a test constructor with a very very very very very + * very very very long comment + */ +interface RawTest { + _: 'test'; +}" +`; + +exports[`generateTypescriptDefinitionsForTlEntry > comments > wraps long comments 2`] = ` +"/** + * This is a test method with a very very very very very very + * very very long comment + * + * RPC method returns {@link tl.TypeTest} + */ +interface RawTestRequest { + _: 'test'; +}" +`; + +exports[`generateTypescriptDefinitionsForTlEntry > generates code with raw flags for constructors with flags 1`] = ` +"interface RawTest { + _: 'test'; + flags: number; + flags2: number; +}" +`; + +exports[`generateTypescriptDefinitionsForTlEntry > ignores namespace for name 1`] = ` +"interface RawTest { + _: 'test.test'; +}" +`; + +exports[`generateTypescriptDefinitionsForTlEntry > marks optional fields as optional 1`] = ` +"interface RawTest { + _: 'test'; + a?: boolean; + b?: string; + c?: tl.TypeFoo; + d?: tl.namespace.TypeFoo[]; +}" +`; + +exports[`generateTypescriptDefinitionsForTlEntry > renames non-primitive types 1`] = ` +"interface RawTest { + _: 'test'; + foo: tl.TypeFoo; + bar: tl.TypeBar[]; + baz: tl.namespace.TypeBaz; + egg: tl.namespace.TypeEgg[]; +}" +`; + +exports[`generateTypescriptDefinitionsForTlEntry > replaces primitive types 1`] = ` +"interface RawTest { + _: 'test'; + a: number; + b: Long; + c: Double; + d: string; + e: Uint8Array; + f: boolean; + g: number[]; +}" +`; + +exports[`generateTypescriptDefinitionsForTlEntry > writes generic types 1`] = ` +"interface RawInvokeWithoutUpdatesRequest { + _: 'invokeWithoutUpdates'; + query: X; +}" +`; + +exports[`generateTypescriptDefinitionsForTlSchema > writes schemas with methods 1`] = ` +"interface RawTest { + _: 'test'; +} +/** + * RPC method returns {@link tl.TypeTest} + */ +interface RawGetTestRequest { + _: 'getTest'; +} +interface RpcCallReturn { + 'getTest': tl.TypeTest +} +type TypeTest = tl.RawTest +function isAnyTest(o: object): o is TypeTest +type RpcMethod = + | tl.RawGetTestRequest + +type TlObject = + | tl.RawTest + | tl.RawGetTestRequest" +`; + +exports[`generateTypescriptDefinitionsForTlSchema > writes schemas with methods 2`] = ` +"ns.isAnyTest = _isAny('Test'); +_types = JSON.parse('{\\"test\\":\\"Test\\"}');" +`; + +exports[`generateTypescriptDefinitionsForTlSchema > writes schemas with multi-unions 1`] = ` +"interface RawTest { + _: 'test'; +} +interface RawTest2 { + _: 'test2'; +} +interface RpcCallReturn { +} +type TypeTest = tl.RawTest | tl.RawTest2 +function isAnyTest(o: object): o is TypeTest + +type TlObject = + | tl.RawTest + | tl.RawTest2" +`; + +exports[`generateTypescriptDefinitionsForTlSchema > writes schemas with multi-unions 2`] = ` +"ns.isAnyTest = _isAny('Test'); +_types = JSON.parse('{\\"test\\":\\"Test\\",\\"test2\\":\\"Test\\"}');" +`; + +exports[`generateTypescriptDefinitionsForTlSchema > writes schemas with namespaces 1`] = ` +"interface RawTest { + _: 'test'; +} +interface RawTest2 { + _: 'test2'; +} +/** + * RPC method returns {@link tl.TypeTest} + */ +interface RawGetTestRequest { + _: 'getTest'; +} +interface RpcCallReturn extends test.RpcCallReturn { + 'getTest': tl.TypeTest +} +type TypeTest = tl.RawTest | tl.RawTest2 +function isAnyTest(o: object): o is TypeTest + +namespace test { + interface RawTest { + _: 'test.test'; + } + interface RawTest2 { + _: 'test.test2'; + } + /** + * RPC method returns {@link tl.test.TypeTest} + */ + interface RawGetTestRequest { + _: 'test.getTest'; + } + interface RpcCallReturn { + 'test.getTest': tl.test.TypeTest + } + type TypeTest = tl.test.RawTest | tl.test.RawTest2 + function isAnyTest(o: object): o is TypeTest +} +type RpcMethod = + | tl.RawGetTestRequest + | tl.test.RawGetTestRequest + +type TlObject = + | tl.RawTest + | tl.RawTest2 + | tl.test.RawTest + | tl.test.RawTest2 + | tl.RawGetTestRequest + | tl.test.RawGetTestRequest" +`; + +exports[`generateTypescriptDefinitionsForTlSchema > writes schemas with namespaces 2`] = ` +"ns.isAnyTest = _isAny('Test'); +ns.test = {}; +(function(ns){ +ns.isAnyTest = _isAny('test.Test'); +})(ns.test); +_types = JSON.parse('{\\"test\\":\\"Test\\",\\"test2\\":\\"Test\\",\\"test.test\\":\\"test.Test\\",\\"test.test2\\":\\"test.Test\\"}');" +`; + +exports[`generateTypescriptDefinitionsForTlSchema > writes simple schemas 1`] = ` +"interface RawTest { + _: 'test'; +} +interface RpcCallReturn { +} +type TypeTest = tl.RawTest +function isAnyTest(o: object): o is TypeTest + +type TlObject = + | tl.RawTest" +`; + +exports[`generateTypescriptDefinitionsForTlSchema > writes simple schemas 2`] = ` +"ns.isAnyTest = _isAny('Test'); +_types = JSON.parse('{\\"test\\":\\"Test\\"}');" +`; diff --git a/packages/tl-utils/src/codegen/__snapshots__/writer.test.ts.snap b/packages/tl-utils/src/codegen/__snapshots__/writer.test.ts.snap new file mode 100644 index 00000000..0de9f1c5 --- /dev/null +++ b/packages/tl-utils/src/codegen/__snapshots__/writer.test.ts.snap @@ -0,0 +1,60 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`generateWriterCodeForTlEntries > generates code for bare types 1`] = ` +"var m={ +'future_salt':function(w,v){w.uint(155834844);w.bytes(h(v,'salt'));}, +'future_salts':function(w,v){w.uint(2924480661);w.vector(m._bare[155834844],h(v,'salts'),1);m._bare[155834844](w,h(v,'current'));}, +_bare:{ +155834844:function(w=this,v){w.bytes(h(v,'salt'));}, +}, +}" +`; + +exports[`generateWriterCodeForTlEntries > should include prelude by default 1`] = ` +"function h(o,p){var q=o[p];if(q===void 0)throw Error('Object '+o._+' is missing required property '+p);return q} +var m={ +}" +`; + +exports[`generateWriterCodeForTlEntries > should include static sizes calculations 1`] = ` +"function h(o,p){var q=o[p];if(q===void 0)throw Error('Object '+o._+' is missing required property '+p);return q} +var m={ +'test1':function(w,v){w.uint(102026291);w.int(h(v,'foo'));w.int(h(v,'bar'));}, +'test2':function(w,v){w.uint(2926357645);w.int(h(v,'foo'));w.double(h(v,'bar'));}, +'test3':function(w,v){w.uint(3373702963);w.int(h(v,'foo'));w.bytes(h(v,'bar'));}, +_staticSize:{ +'test1':12, +'test2':16, +}, +}" +`; + +exports[`generateWriterCodeForTlEntry > automatically computes constructor ID if needed 1`] = `"'topPeerCategoryBotsPM':function(w){w.uint(2875595611);},"`; + +exports[`generateWriterCodeForTlEntry > generates code for bare vectors 1`] = `"'msg_container':function(w,v){w.uint(1945237724);w.vector(m._bare[155834844],h(v,'messages'),1);},"`; + +exports[`generateWriterCodeForTlEntry > generates code for bare vectors 2`] = `"'future_salts':function(w,v){w.uint(2924480661);w.vector(m._bare[155834844],h(v,'salts'));w.object(h(v,'current'));},"`; + +exports[`generateWriterCodeForTlEntry > generates code for constructors with generics 1`] = `"'invokeWithLayer':function(w,v){w.uint(3667594509);w.int(h(v,'layer'));w.object(h(v,'query'));},"`; + +exports[`generateWriterCodeForTlEntry > generates code for constructors with multiple fields using the same flag 1`] = `"'inputMediaPoll':function(w,v){w.uint(261416433);var flags=0;var _solution=v.solution!==undefined;var _solutionEntities=v.solutionEntities&&v.solutionEntities.length;var _flags_1=_solution||_solutionEntities;if(_flags_1)flags|=2;w.uint(flags);if(_flags_1)w.string(v.solution);if(_flags_1)w.vector(w.object,v.solutionEntities);},"`; + +exports[`generateWriterCodeForTlEntry > generates code for constructors with multiple flags fields 1`] = `"'updates.channelDifferenceEmpty':function(w,v){w.uint(1041346555);var flags=0;if(v.final===true)flags|=1;var _timeout=v.timeout!==undefined;if(_timeout)flags|=2;w.uint(flags);w.int(h(v,'pts'));if(_timeout)w.int(v.timeout);var flags2=0;if(v.canDeleteChannel===true)flags2|=1;w.uint(flags2);},"`; + +exports[`generateWriterCodeForTlEntry > generates code for constructors with optional arguments 1`] = `"'updates.channelDifferenceEmpty':function(w,v){w.uint(1041346555);var flags=0;if(v.final===true)flags|=1;var _timeout=v.timeout!==undefined;if(_timeout)flags|=2;w.uint(flags);w.int(h(v,'pts'));if(_timeout)w.int(v.timeout);},"`; + +exports[`generateWriterCodeForTlEntry > generates code for constructors with optional vector arguments 1`] = `"'messages.getWebPagePreview':function(w,v){w.uint(2338894028);var flags=0;var _entities=v.entities&&v.entities.length;if(_entities)flags|=8;w.uint(flags);w.string(h(v,'message'));if(_entities)w.vector(w.object,v.entities);},"`; + +exports[`generateWriterCodeForTlEntry > generates code for constructors with simple arguments 1`] = `"'inputBotInlineMessageID':function(w,v){w.uint(2299280777);w.int(h(v,'dcId'));w.long(h(v,'id'));w.long(h(v,'accessHash'));},"`; + +exports[`generateWriterCodeForTlEntry > generates code for constructors with simple arguments 2`] = `"'contact':function(w,v){w.uint(341499403);w.long(h(v,'userId'));w.boolean(h(v,'mutual'));},"`; + +exports[`generateWriterCodeForTlEntry > generates code for constructors with simple arguments 3`] = `"'maskCoords':function(w,v){w.uint(2933316530);w.int(h(v,'n'));w.double(h(v,'x'));w.double(h(v,'y'));w.double(h(v,'zoom'));},"`; + +exports[`generateWriterCodeForTlEntry > generates code for constructors with true flags 1`] = `"'messages.messageEditData':function(w,v){w.uint(649453030);var flags=0;if(v.caption===true)flags|=1;w.uint(flags);},"`; + +exports[`generateWriterCodeForTlEntry > generates code for constructors with vector arguments 1`] = `"'contacts.resolvedPeer':function(w,v){w.uint(2131196633);w.object(h(v,'peer'));w.vector(w.object,h(v,'chats'));w.vector(w.object,h(v,'users'));},"`; + +exports[`generateWriterCodeForTlEntry > generates code for constructors without arguments 1`] = `"'topPeerCategoryBotsPM':function(w){w.uint(2875595611);},"`; + +exports[`generateWriterCodeForTlEntry > generates code with raw flags for constructors with flags 1`] = `"'test':function(w,v){w.uint(1554225816);var flags=v.flags;w.uint(flags);var flags2=v.flags2;w.uint(flags2);},"`; diff --git a/packages/tl-utils/src/codegen/errors.test.ts b/packages/tl-utils/src/codegen/errors.test.ts new file mode 100644 index 00000000..3b35debf --- /dev/null +++ b/packages/tl-utils/src/codegen/errors.test.ts @@ -0,0 +1,20 @@ +import { describe, expect, it } from 'vitest' + +import { generateCodeForErrors } from './errors.js' + +describe('generateCodeForErrors', () => { + it('should correctly generate errors', () => { + expect( + generateCodeForErrors({ + base: { BAD_REQUEST: 400 }, + errors: { + USER_NOT_FOUND: { code: 400, name: 'USER_NOT_FOUND' }, + 'FLOOD_WAIT_%d': { code: 420, name: 'FLOOD_WAIT_%d', description: 'Wait %d seconds' }, + }, + throws: {}, + userOnly: {}, + botOnly: {}, + }), + ).toMatchSnapshot() + }) +}) diff --git a/packages/tl-utils/src/codegen/reader.test.ts b/packages/tl-utils/src/codegen/reader.test.ts index 2d27061f..8d45e309 100644 --- a/packages/tl-utils/src/codegen/reader.test.ts +++ b/packages/tl-utils/src/codegen/reader.test.ts @@ -1,178 +1,130 @@ import { describe, expect, it } from 'vitest' import { parseTlToEntries } from '../parse.js' -import { generateReaderCodeForTlEntry } from './reader.js' +import { generateReaderCodeForTlEntries, generateReaderCodeForTlEntry } from './reader.js' describe('generateReaderCodeForTlEntry', () => { - const test = (tl: string, ...js: string[]) => { - const entry = parseTlToEntries(tl).slice(-1)[0] - expect(generateReaderCodeForTlEntry(entry)).toEqual(`${entry.id}:function(r){${js.join('')}},`) + const test = (...tl: string[]) => { + const entry = parseTlToEntries(tl.join('\n')).slice(-1)[0] + expect(generateReaderCodeForTlEntry(entry)).toMatchSnapshot() } it('generates code for constructors without arguments', () => { - test('topPeerCategoryBotsPM#ab661b5b = TopPeerCategory;', "return{_:'topPeerCategoryBotsPM'}") + test('topPeerCategoryBotsPM#ab661b5b = TopPeerCategory;') }) it('generates code for constructors with simple arguments', () => { - test( - 'inputBotInlineMessageID#890c3d89 dc_id:int id:long access_hash:long = InputBotInlineMessageID;', - 'return{', - "_:'inputBotInlineMessageID',", - 'dcId:r.int(),', - 'id:r.long(),', - 'accessHash:r.long(),', - '}', - ) - test( - 'contact#145ade0b user_id:long mutual:Bool = Contact;', - 'return{', - "_:'contact',", - 'userId:r.long(),', - 'mutual:r.boolean(),', - '}', - ) - test( - 'maskCoords#aed6dbb2 n:int x:double y:double zoom:double = MaskCoords;', - 'return{', - "_:'maskCoords',", - 'n:r.int(),', - 'x:r.double(),', - 'y:r.double(),', - 'zoom:r.double(),', - '}', - ) + test('inputBotInlineMessageID#890c3d89 dc_id:int id:long access_hash:long = InputBotInlineMessageID;') + test('contact#145ade0b user_id:long mutual:Bool = Contact;') + test('maskCoords#aed6dbb2 n:int x:double y:double zoom:double = MaskCoords;') }) it('generates code for constructors with true flags', () => { - test( - 'messages.messageEditData#26b5dde6 flags:# caption:flags.0?true = messages.MessageEditData;', - 'var flags=r.uint();', - 'return{', - "_:'messages.messageEditData',", - 'caption:!!(flags&1),', - '}', - ) + test('messages.messageEditData#26b5dde6 flags:# caption:flags.0?true = messages.MessageEditData;') }) it('generates code for constructors with optional arguments', () => { test( 'updates.channelDifferenceEmpty#3e11affb flags:# final:flags.0?true pts:int timeout:flags.1?int = updates.ChannelDifference;', - 'var flags=r.uint();', - 'return{', - "_:'updates.channelDifferenceEmpty',", - 'final:!!(flags&1),', - 'pts:r.int(),', - 'timeout:flags&2?r.int():void 0,', - '}', ) }) it('generates code for constructors with arguments before flags field', () => { - test( - 'poll#86e18161 id:long flags:# quiz:flags.3?true question:string = Poll;', - 'var id=r.long(),', - 'flags=r.uint();', - 'return{', - "_:'poll',", - 'id:id,', - 'quiz:!!(flags&8),', - 'question:r.string(),', - '}', - ) + test('poll#86e18161 id:long flags:# quiz:flags.3?true question:string = Poll;') }) it('generates code for constructors with multiple flags fields', () => { test( 'updates.channelDifferenceEmpty#3e11affb flags:# final:flags.0?true pts:int timeout:flags.1?int flags2:# can_delete_channel:flags2.0?true = updates.ChannelDifference;', - 'var flags=r.uint(),', - 'pts=r.int(),', - 'timeout=flags&2?r.int():void 0,', - 'flags2=r.uint();', - 'return{', - "_:'updates.channelDifferenceEmpty',", - 'final:!!(flags&1),', - 'pts:pts,', - 'timeout:timeout,', - 'canDeleteChannel:!!(flags2&1),', - '}', ) }) it('generates code for constructors with vector arguments', () => { - test( - 'contacts.resolvedPeer#7f077ad9 peer:Peer chats:Vector users:Vector = contacts.ResolvedPeer;', - 'return{', - "_:'contacts.resolvedPeer',", - 'peer:r.object(),', - 'chats:r.vector(r.object),', - 'users:r.vector(r.object),', - '}', - ) + test('contacts.resolvedPeer#7f077ad9 peer:Peer chats:Vector users:Vector = contacts.ResolvedPeer;') }) it('generates code for constructors with optional vector arguments', () => { test( 'messages.getWebPagePreview#8b68b0cc flags:# message:string entities:flags.3?Vector = MessageMedia;', - 'var flags=r.uint();', - 'return{', - "_:'messages.getWebPagePreview',", - 'message:r.string(),', - 'entities:flags&8?r.vector(r.object):void 0,', - '}', ) }) it('generates code for constructors with generics', () => { - test( - 'invokeWithLayer#da9b0d0d {X:Type} layer:int query:!X = X;', - 'return{', - "_:'invokeWithLayer',", - 'layer:r.int(),', - 'query:r.object(),', - '}', - ) + test('invokeWithLayer#da9b0d0d {X:Type} layer:int query:!X = X;') }) it('generates code for bare types', () => { + test('message#0949d9dc = Message;', 'msg_container#73f1f8dc messages:vector<%Message> = MessageContainer;') test( - 'message#0949d9dc = Message;\n' + 'msg_container#73f1f8dc messages:vector<%Message> = MessageContainer;', - 'return{', - "_:'msg_container',", - 'messages:r.vector(m[155834844],1),', - '}', + 'future_salt#0949d9dc = FutureSalt;', + 'future_salts#ae500895 salts:Vector current:FutureSalt = FutureSalts;', ) test( - 'future_salt#0949d9dc = FutureSalt;\n' + - 'future_salts#ae500895 salts:Vector current:FutureSalt = FutureSalts;', - 'return{', - "_:'future_salts',", - 'salts:r.vector(m[155834844]),', - 'current:r.object(),', - '}', - ) - test( - 'future_salt#0949d9dc = FutureSalt;\n' + - 'future_salts#ae500895 salts:vector current:future_salt = FutureSalts;', - 'return{', - "_:'future_salts',", - 'salts:r.vector(m[155834844],1),', - 'current:m[155834844](r),', - '}', + 'future_salt#0949d9dc = FutureSalt;\n', + 'future_salts#ae500895 salts:vector current:future_salt = FutureSalts;', ) }) it('generates code with raw flags for constructors with flags', () => { const entry = parseTlToEntries('test flags:# flags2:# = Test;')[0] - expect(generateReaderCodeForTlEntry(entry, { includeFlags: true })).toEqual( - `${entry.id}:function(r){${[ - 'var flags=r.uint(),', - 'flags2=r.uint();', - 'return{', - "_:'test',", - 'flags:flags,', - 'flags2:flags2,', - '}', - ].join('')}},`, - ) + expect(generateReaderCodeForTlEntry(entry, { includeFlags: true })).toMatchSnapshot() + }) +}) + +describe('generateReaderCodeForTlEntries', () => { + it('generates code for multiple entries', () => { + const entries = parseTlToEntries('test = Test;\ntest2 = Test2;\n') + + expect(generateReaderCodeForTlEntries(entries)).toMatchSnapshot() + }) + + it("doesn't generate code for methods by default", () => { + const entries = parseTlToEntries(['test = Test;', '---functions---', 'test2 = Test2;'].join('\n')) + + expect(generateReaderCodeForTlEntries(entries)).toMatchSnapshot() + }) + + it('generates code for methods if asked to', () => { + const entries = parseTlToEntries(['test = Test;', '---functions---', 'test2 = Test2;'].join('\n')) + + expect(generateReaderCodeForTlEntries(entries, { includeMethods: true })).toMatchSnapshot() + }) + + it('updates readers used in bare vectors', () => { + const entries = parseTlToEntries(['test = Test;', 'test2 a:vector = Test2;'].join('\n')) + + expect(generateReaderCodeForTlEntries(entries)).toMatchSnapshot() + }) + + describe('method return readers', () => { + it('includes primitive return type parsing info', () => { + const entries = parseTlToEntries(['test1 = Test;', '---functions---', 'test = int;'].join('\n')) + + expect(generateReaderCodeForTlEntries(entries, { includeMethodResults: true })).toMatchSnapshot() + }) + + it('includes primitive vectors return type parsing info', () => { + const entries = parseTlToEntries(['---functions---', 'test = Vector;'].join('\n'), { + parseMethodTypes: true, + }) + + expect(generateReaderCodeForTlEntries(entries, { includeMethodResults: true })).toMatchSnapshot() + }) + + it('includes primitive vectors return type parsing info', () => { + const entries = parseTlToEntries(['---functions---', 'test = Vector;'].join('\n'), { + parseMethodTypes: true, + }) + + expect(generateReaderCodeForTlEntries(entries, { includeMethodResults: true })).toMatchSnapshot() + }) + + it("doesn't include Bool parsing", () => { + const entries = parseTlToEntries(['---functions---', 'test = Bool;'].join('\n'), { + parseMethodTypes: true, + }) + + expect(generateReaderCodeForTlEntries(entries, { includeMethodResults: true })).toMatchSnapshot() + }) }) }) diff --git a/packages/tl-utils/src/codegen/types.test.ts b/packages/tl-utils/src/codegen/types.test.ts index be421d3e..ec17fb87 100644 --- a/packages/tl-utils/src/codegen/types.test.ts +++ b/packages/tl-utils/src/codegen/types.test.ts @@ -5,161 +5,89 @@ import { parseFullTlSchema } from '../schema.js' import { generateTypescriptDefinitionsForTlEntry, generateTypescriptDefinitionsForTlSchema } from './types.js' describe('generateTypescriptDefinitionsForTlEntry', () => { - const test = (tl: string, ...ts: string[]) => { - const entry = parseTlToEntries(tl)[0] - expect(generateTypescriptDefinitionsForTlEntry(entry)).toEqual(ts.join('\n')) + const test = (...tl: string[]) => { + const entry = parseTlToEntries(tl.join('\n'), { parseMethodTypes: true })[0] + expect(generateTypescriptDefinitionsForTlEntry(entry)).toMatchSnapshot() } it('replaces primitive types', () => { - test( - 'test a:int b:long c:double d:string e:bytes f:Bool g:vector = Test;', - 'interface RawTest {', - " _: 'test';", - ' a: number;', - ' b: Long;', - ' c: Double;', - ' d: string;', - ' e: Uint8Array;', - ' f: boolean;', - ' g: number[];', - '}', - ) + test('test a:int b:long c:double d:string e:bytes f:Bool g:vector = Test;') }) it('ignores namespace for name', () => { - test('test.test = Test;', 'interface RawTest {', " _: 'test.test';", '}') + test('test.test = Test;') }) it('renames non-primitive types', () => { - test( - 'test foo:Foo bar:vector baz:namespace.Baz egg:vector = Test;', - 'interface RawTest {', - " _: 'test';", - ' foo: tl.TypeFoo;', - ' bar: tl.TypeBar[];', - ' baz: tl.namespace.TypeBaz;', - ' egg: tl.namespace.TypeEgg[];', - '}', - ) + test('test foo:Foo bar:vector baz:namespace.Baz egg:vector = Test;') }) it('marks optional fields as optional', () => { - test( - 'test flags:# a:flags.0?true b:flags.1?string c:flags.2?Foo d:flags.3?vector = Test;', - 'interface RawTest {', - " _: 'test';", - ' a?: boolean;', - ' b?: string;', - ' c?: tl.TypeFoo;', - ' d?: tl.namespace.TypeFoo[];', - '}', - ) + test('test flags:# a:flags.0?true b:flags.1?string c:flags.2?Foo d:flags.3?vector = Test;') }) describe('comments', () => { it('adds tl style comments', () => { - test( - '// This is a test constructor\n' + 'test = Test;', - '/**', - ' * This is a test constructor', - ' */', - 'interface RawTest {', - " _: 'test';", - '}', - ) - test( - '---functions---\n' + '// This is a test method\n' + 'test = Test;', - '/**', - ' * This is a test method', - ' * ', - ' * RPC method returns {@link tl.TypeTest}', - ' */', - 'interface RawTestRequest {', - " _: 'test';", - '}', + test('// This is a test constructor', 'test = Test;') + }) + + it('adds return type comments', () => { + test('---functions---', '// This is a test method', 'test = Test;') + test('---functions---', '// This is a test method', 'test = Test;') + test('---functions---\n', '// This is a test method\n', 'test = Vector;') + }) + + it('adds usage info comments', () => { + const entries = parseTlToEntries('---functions---\ntest = Test;\ntestBot = Test;') + const [result, resultBot] = entries.map((it) => + generateTypescriptDefinitionsForTlEntry(it, 'tl.', { + base: {}, + errors: {}, + throws: { test: ['FOO', 'BAR'] }, + userOnly: { test: 1 }, + botOnly: { testBot: 1 }, + }), ) + + expect(result).toMatchSnapshot() + expect(resultBot).toMatchSnapshot() }) it('adds tdlib style comments', () => { - test( - '// @description This is a test constructor\n' + '// @field Some field\n' + 'test field:int = Test;', - '/**', - ' * This is a test constructor', - ' */', - 'interface RawTest {', - " _: 'test';", - ' /**', - ' * Some field', - ' */', - ' field: number;', - '}', - ) + test('// @description This is a test constructor', '// @field Some field', 'test field:int = Test;') }) it('wraps long comments', () => { test( - '// This is a test constructor with a very very very very very very very very long comment\n' + - 'test = Test;', - '/**', - ' * This is a test constructor with a very very very very very', - ' * very very very long comment', - ' */', - 'interface RawTest {', - " _: 'test';", - '}', + '// This is a test constructor with a very very very very very very very very long comment', + 'test = Test;', ) test( - '---functions---\n' + - '// This is a test method with a very very very very very very very very long comment\n' + - 'test = Test;', - '/**', - ' * This is a test method with a very very very very very very', - ' * very very long comment', - ' * ', - ' * RPC method returns {@link tl.TypeTest}', - ' */', - 'interface RawTestRequest {', - " _: 'test';", - '}', + '---functions---', + '// This is a test method with a very very very very very very very very long comment', + 'test = Test;', ) }) it('should not break @link tags', () => { - test( - '// This is a test constructor with a very long comment {@link whatever} more text\n' + 'test = Test;', - '/**', - ' * This is a test constructor with a very long comment', - ' * {@link whatever} more text', - ' */', - 'interface RawTest {', - " _: 'test';", - '}', - ) + test('// This is a test constructor with a very long comment {@link whatever} more text', 'test = Test;') }) }) it('writes generic types', () => { - test( - '---functions---\ninvokeWithoutUpdates#bf9459b7 {X:Type} query:!X = X;', - 'interface RawInvokeWithoutUpdatesRequest {', - " _: 'invokeWithoutUpdates';", - ' query: X;', - '}', - ) + test('---functions---\ninvokeWithoutUpdates#bf9459b7 {X:Type} query:!X = X;') }) it('generates code with raw flags for constructors with flags', () => { const entry = parseTlToEntries('test flags:# flags2:# = Test;')[0] - expect(generateTypescriptDefinitionsForTlEntry(entry, undefined, undefined, true)).toEqual( - ['interface RawTest {', " _: 'test';", ' flags: number;', ' flags2: number;', '}'].join('\n'), - ) + expect(generateTypescriptDefinitionsForTlEntry(entry, undefined, undefined, true)).toMatchSnapshot() }) }) describe('generateTypescriptDefinitionsForTlSchema', () => { - const test = (tl: string, ts: string[], js: string[]) => { - const entries = parseTlToEntries(tl) + const test = (...tl: string[]) => { + const entries = parseTlToEntries(tl.join('\n')) const schema = parseFullTlSchema(entries) let [codeTs, codeJs] = generateTypescriptDefinitionsForTlSchema(schema, 0) @@ -172,152 +100,31 @@ describe('generateTypescriptDefinitionsForTlSchema', () => { // skip prelude codeJs = codeJs.substring(codeJs.indexOf('ns.LAYER = 0;') + 14, codeJs.length - 15) - expect(codeTs.trim()).toEqual(ts.join('\n')) - expect(codeJs.trim()).toEqual(js.join('\n')) + expect(codeTs.trim()).toMatchSnapshot() + expect(codeJs.trim()).toMatchSnapshot() } it('writes simple schemas', () => { - test( - 'test = Test;', - [ - 'interface RawTest {', - " _: 'test';", - '}', - 'interface RpcCallReturn {', - '}', - 'type TypeTest = tl.RawTest', - 'function isAnyTest(o: object): o is TypeTest', - '', - 'type TlObject =', - ' | tl.RawTest', - ], - ["ns.isAnyTest = _isAny('Test');", '_types = JSON.parse(\'{"test":"Test"}\');'], - ) + test('test = Test;') }) it('writes schemas with multi-unions', () => { - test( - 'test = Test;\ntest2 = Test;', - [ - 'interface RawTest {', - " _: 'test';", - '}', - 'interface RawTest2 {', - " _: 'test2';", - '}', - 'interface RpcCallReturn {', - '}', - 'type TypeTest = tl.RawTest | tl.RawTest2', - 'function isAnyTest(o: object): o is TypeTest', - '', - 'type TlObject =', - ' | tl.RawTest', - ' | tl.RawTest2', - ], - ["ns.isAnyTest = _isAny('Test');", '_types = JSON.parse(\'{"test":"Test","test2":"Test"}\');'], - ) + test('test = Test;\ntest2 = Test;') }) it('writes schemas with methods', () => { - test( - 'test = Test;\n---functions---\ngetTest = Test;', - [ - 'interface RawTest {', - " _: 'test';", - '}', - '/**', - ' * RPC method returns {@link tl.TypeTest}', - ' */', - 'interface RawGetTestRequest {', - " _: 'getTest';", - '}', - 'interface RpcCallReturn {', - " 'getTest': tl.TypeTest", - '}', - 'type TypeTest = tl.RawTest', - 'function isAnyTest(o: object): o is TypeTest', - 'type RpcMethod =', - ' | tl.RawGetTestRequest', - '', - 'type TlObject =', - ' | tl.RawTest', - ' | tl.RawGetTestRequest', - ], - ["ns.isAnyTest = _isAny('Test');", '_types = JSON.parse(\'{"test":"Test"}\');'], - ) + test('test = Test;\n---functions---\ngetTest = Test;') }) it('writes schemas with namespaces', () => { test( - 'test = Test;\n' + - 'test2 = Test;\n' + - 'test.test = test.Test;\n' + - 'test.test2 = test.Test;\n' + - '---functions---\n' + - 'getTest = Test;\n' + - 'test.getTest = test.Test;', - [ - ` -interface RawTest { - _: 'test'; -} -interface RawTest2 { - _: 'test2'; -} -/** - * RPC method returns {@link tl.TypeTest} - */ -interface RawGetTestRequest { - _: 'getTest'; -} -interface RpcCallReturn extends test.RpcCallReturn { - 'getTest': tl.TypeTest -} -type TypeTest = tl.RawTest | tl.RawTest2 -function isAnyTest(o: object): o is TypeTest - -namespace test { - interface RawTest { - _: 'test.test'; - } - interface RawTest2 { - _: 'test.test2'; - } - /** - * RPC method returns {@link tl.test.TypeTest} - */ - interface RawGetTestRequest { - _: 'test.getTest'; - } - interface RpcCallReturn { - 'test.getTest': tl.test.TypeTest - } - type TypeTest = tl.test.RawTest | tl.test.RawTest2 - function isAnyTest(o: object): o is TypeTest -} -type RpcMethod = - | tl.RawGetTestRequest - | tl.test.RawGetTestRequest - -type TlObject = - | tl.RawTest - | tl.RawTest2 - | tl.test.RawTest - | tl.test.RawTest2 - | tl.RawGetTestRequest - | tl.test.RawGetTestRequest -`.trim(), - ], - [ - ` -ns.isAnyTest = _isAny('Test'); -ns.test = {}; -(function(ns){ -ns.isAnyTest = _isAny('test.Test'); -})(ns.test); -_types = JSON.parse('{"test":"Test","test2":"Test","test.test":"test.Test","test.test2":"test.Test"}'); -`.trim(), - ], + 'test = Test;\n', + 'test2 = Test;\n', + 'test.test = test.Test;\n', + 'test.test2 = test.Test;\n', + '---functions---\n', + 'getTest = Test;\n', + 'test.getTest = test.Test;', ) }) }) diff --git a/packages/tl-utils/src/codegen/types.ts b/packages/tl-utils/src/codegen/types.ts index 8aeeb644..91aad97c 100644 --- a/packages/tl-utils/src/codegen/types.ts +++ b/packages/tl-utils/src/codegen/types.ts @@ -111,6 +111,8 @@ export function generateTypescriptDefinitionsForTlEntry( if (errors) { if (errors.userOnly[entry.name]) { comment += '\n\nThis method is **not** available for bots' + } else if (errors.botOnly[entry.name]) { + comment += '\n\nThis method is **not** available for normal users' } if (errors.throws[entry.name]) { @@ -126,10 +128,20 @@ export function generateTypescriptDefinitionsForTlEntry( if (entry.generics?.length) { genericsString = '<' entry.generics.forEach((it, idx) => { - const tsType = it.type === 'Type' ? 'tl.TlObject' : fullTypeName(it.type, baseNamespace) + /* c8 ignore next 3 */ + if (it.type !== 'Type') { + throw new Error('Only Type generics are supported') + } + + const tsType = `${baseNamespace}TlObject` genericsIndex[it.name] = 1 - if (idx !== 0) genericsString += ', ' + + /* c8 ignore next 3 */ + if (idx !== 0) { + throw new Error('Multiple generics are not supported') + } + genericsString += `${it.name} extends ${tsType} = ${tsType}` }) genericsString += '>' diff --git a/packages/tl-utils/src/codegen/utils.test.ts b/packages/tl-utils/src/codegen/utils.test.ts new file mode 100644 index 00000000..62b8b0ec --- /dev/null +++ b/packages/tl-utils/src/codegen/utils.test.ts @@ -0,0 +1,57 @@ +import { describe, expect, it } from 'vitest' + +import { camelToPascal, indent, jsComment, snakeToCamel } from './utils.js' + +describe('snakeToCamel', () => { + it('should convert snake_case to camelCase', () => { + expect(snakeToCamel('snake_case')).toEqual('snakeCase') + }) + + it('should correctly handle numbers', () => { + expect(snakeToCamel('snake_case_123')).toEqual('snakeCase123') + expect(snakeToCamel('snake_case123')).toEqual('snakeCase123') + }) +}) + +describe('camelToPascal', () => { + it('should convert camelCase to PascalCase', () => { + expect(camelToPascal('camelCase')).toEqual('CamelCase') + }) + + it('should correctly handle numbers', () => { + expect(camelToPascal('camelCase123')).toEqual('CamelCase123') + }) +}) + +describe('jsComment', () => { + it('should format comments correctly', () => { + expect(jsComment('This is a comment')).toEqual('/**\n * This is a comment\n */') + }) + + it('should wrap long comments correctly', () => { + expect(jsComment('This is a very long comment which should be wrapped around here')).toEqual( + '/**\n * This is a very long comment which should be wrapped around\n * here\n */', + ) + }) + + it('should not break up links', () => { + expect(jsComment('This is a very long comment that wraps nearby a {@link link} yeah')).toEqual( + '/**\n * This is a very long comment that wraps nearby a {@link link}\n * yeah\n */', + ) + expect(jsComment('This is a very long comment that wraps nearby this {@link link} yeah')).toEqual( + '/**\n * This is a very long comment that wraps nearby this\n * {@link link} yeah\n */', + ) + }) +}) + +describe('indent', () => { + it('should indent correctly', () => { + expect(indent(4, 'This is a comment')).toEqual(' This is a comment') + }) + + it('should indent correctly with multiple lines', () => { + expect(indent(4, 'This is a comment\nThis is another comment')).toEqual( + ' This is a comment\n This is another comment', + ) + }) +}) diff --git a/packages/tl-utils/src/codegen/writer.test.ts b/packages/tl-utils/src/codegen/writer.test.ts index 2d4957dc..bf924f88 100644 --- a/packages/tl-utils/src/codegen/writer.test.ts +++ b/packages/tl-utils/src/codegen/writer.test.ts @@ -4,11 +4,9 @@ import { parseTlToEntries } from '../parse.js' import { generateWriterCodeForTlEntries, generateWriterCodeForTlEntry } from './writer.js' describe('generateWriterCodeForTlEntry', () => { - const test = (tl: string, ...js: string[]) => { - const entry = parseTlToEntries(tl).slice(-1)[0] - expect(generateWriterCodeForTlEntry(entry)).toEqual( - `'${entry.name}':function(w${entry.arguments.length ? ',v' : ''}){w.uint(${entry.id});${js.join('')}},`, - ) + const test = (...tl: string[]) => { + const entry = parseTlToEntries(tl.join('\n')).slice(-1)[0] + expect(generateWriterCodeForTlEntry(entry)).toMatchSnapshot() } it('generates code for constructors without arguments', () => { @@ -16,148 +14,104 @@ describe('generateWriterCodeForTlEntry', () => { }) it('generates code for constructors with simple arguments', () => { - test( - 'inputBotInlineMessageID#890c3d89 dc_id:int id:long access_hash:long = InputBotInlineMessageID;', - "w.int(h(v,'dcId'));", - "w.long(h(v,'id'));", - "w.long(h(v,'accessHash'));", - ) - test( - 'contact#145ade0b user_id:long mutual:Bool = Contact;', - "w.long(h(v,'userId'));", - "w.boolean(h(v,'mutual'));", - ) - test( - 'maskCoords#aed6dbb2 n:int x:double y:double zoom:double = MaskCoords;', - "w.int(h(v,'n'));", - "w.double(h(v,'x'));", - "w.double(h(v,'y'));", - "w.double(h(v,'zoom'));", - ) + test('inputBotInlineMessageID#890c3d89 dc_id:int id:long access_hash:long = InputBotInlineMessageID;') + test('contact#145ade0b user_id:long mutual:Bool = Contact;') + test('maskCoords#aed6dbb2 n:int x:double y:double zoom:double = MaskCoords;') }) it('generates code for constructors with true flags', () => { - test( - 'messages.messageEditData#26b5dde6 flags:# caption:flags.0?true = messages.MessageEditData;', - 'var flags=0;', - 'if(v.caption===true)flags|=1;', - 'w.uint(flags);', - ) + test('messages.messageEditData#26b5dde6 flags:# caption:flags.0?true = messages.MessageEditData;') }) it('generates code for constructors with optional arguments', () => { test( 'updates.channelDifferenceEmpty#3e11affb flags:# final:flags.0?true pts:int timeout:flags.1?int = updates.ChannelDifference;', - 'var flags=0;', - 'if(v.final===true)flags|=1;', - 'var _timeout=v.timeout!==undefined;', - 'if(_timeout)flags|=2;', - 'w.uint(flags);', - "w.int(h(v,'pts'));", - 'if(_timeout)w.int(v.timeout);', ) }) it('generates code for constructors with multiple flags fields', () => { test( 'updates.channelDifferenceEmpty#3e11affb flags:# final:flags.0?true pts:int timeout:flags.1?int flags2:# can_delete_channel:flags2.0?true = updates.ChannelDifference;', - 'var flags=0;', - 'if(v.final===true)flags|=1;', - 'var _timeout=v.timeout!==undefined;', - 'if(_timeout)flags|=2;', - 'w.uint(flags);', - "w.int(h(v,'pts'));", - 'if(_timeout)w.int(v.timeout);', - 'var flags2=0;', - 'if(v.canDeleteChannel===true)flags2|=1;', - 'w.uint(flags2);', ) }) it('generates code for constructors with multiple fields using the same flag', () => { test( 'inputMediaPoll#f94e5f1 flags:# solution:flags.1?string solution_entities:flags.1?Vector = InputMedia;', - 'var flags=0;', - 'var _solution=v.solution!==undefined;', - 'var _solutionEntities=v.solutionEntities&&v.solutionEntities.length;', - 'var _flags_1=_solution||_solutionEntities;', - 'if(_flags_1)flags|=2;', - 'w.uint(flags);', - 'if(_flags_1)w.string(v.solution);', - 'if(_flags_1)w.vector(w.object,v.solutionEntities);', ) }) it('generates code for constructors with vector arguments', () => { - test( - 'contacts.resolvedPeer#7f077ad9 peer:Peer chats:Vector users:Vector = contacts.ResolvedPeer;', - "w.object(h(v,'peer'));", - "w.vector(w.object,h(v,'chats'));", - "w.vector(w.object,h(v,'users'));", - ) + test('contacts.resolvedPeer#7f077ad9 peer:Peer chats:Vector users:Vector = contacts.ResolvedPeer;') }) it('generates code for constructors with optional vector arguments', () => { test( 'messages.getWebPagePreview#8b68b0cc flags:# message:string entities:flags.3?Vector = MessageMedia;', - 'var flags=0;', - 'var _entities=v.entities&&v.entities.length;', - 'if(_entities)flags|=8;', - 'w.uint(flags);', - "w.string(h(v,'message'));", - 'if(_entities)w.vector(w.object,v.entities);', ) }) it('generates code for constructors with generics', () => { - test( - 'invokeWithLayer#da9b0d0d {X:Type} layer:int query:!X = X;', - "w.int(h(v,'layer'));", - "w.object(h(v,'query'));", - ) + test('invokeWithLayer#da9b0d0d {X:Type} layer:int query:!X = X;') }) it('generates code for bare vectors', () => { + test('message#0949d9dc = Message;\n' + 'msg_container#73f1f8dc messages:vector<%Message> = MessageContainer;') test( - 'message#0949d9dc = Message;\n' + 'msg_container#73f1f8dc messages:vector<%Message> = MessageContainer;', - "w.vector(m._bare[155834844],h(v,'messages'),1);", - ) - test( - 'future_salt#0949d9dc = FutureSalt;\n' + - 'future_salts#ae500895 salts:Vector current:FutureSalt = FutureSalts;', - "w.vector(m._bare[155834844],h(v,'salts'));", - "w.object(h(v,'current'));", + 'future_salt#0949d9dc = FutureSalt;', + 'future_salts#ae500895 salts:Vector current:FutureSalt = FutureSalts;', ) }) + it('generates code with raw flags for constructors with flags', () => { + const entry = parseTlToEntries('test flags:# flags2:# = Test;')[0] + expect(generateWriterCodeForTlEntry(entry, { includeFlags: true })).toMatchSnapshot() + }) + + it('automatically computes constructor ID if needed', () => { + const entry = parseTlToEntries('topPeerCategoryBotsPM#ab661b5b = TopPeerCategory;')[0] + entry.id = 0 + + expect(generateWriterCodeForTlEntry(entry)).toMatchSnapshot() + }) + + it('throws for invalid bit index', () => { + const entry = parseTlToEntries('test flags:# field:flags.33?true = Test;')[0] + expect(() => generateWriterCodeForTlEntry(entry, { includeFlags: true })).toThrow() + }) +}) + +describe('generateWriterCodeForTlEntries', () => { it('generates code for bare types', () => { const entries = parseTlToEntries( 'future_salt#0949d9dc salt:bytes = FutureSalt;\n' + 'future_salts#ae500895 salts:vector current:future_salt = FutureSalts;', ) - expect(generateWriterCodeForTlEntries(entries, { includePrelude: false })).toEqual( - ` - var m={ - 'future_salt':function(w,v){w.uint(155834844);w.bytes(h(v,'salt'));}, - 'future_salts':function(w,v){w.uint(2924480661);w.vector(m._bare[155834844],h(v,'salts'),1);m._bare[155834844](w,h(v,'current'));}, - _bare:{ - 155834844:function(w=this,v){w.bytes(h(v,'salt'));}, - }, - }`.replace(/^\s+/gm, ''), - ) + expect(generateWriterCodeForTlEntries(entries, { includePrelude: false })).toMatchSnapshot() }) - it('generates code with raw flags for constructors with flags', () => { - const entry = parseTlToEntries('test flags:# flags2:# = Test;')[0] - expect(generateWriterCodeForTlEntry(entry, { includeFlags: true })).toEqual( - `'${entry.name}':function(w,v){${[ - `w.uint(${entry.id});`, - 'var flags=v.flags;', - 'w.uint(flags);', - 'var flags2=v.flags2;', - 'w.uint(flags2);', - ].join('')}},`, + it('throws when a bare type is not available', () => { + const entries = parseTlToEntries( + 'future_salts#ae500895 salts:vector current:FutureSalt = FutureSalts;', ) + entries[0].arguments[1].typeModifiers = { isBareUnion: true } + + expect(() => generateWriterCodeForTlEntries(entries, { includePrelude: false })).toThrow() + }) + + it('should include prelude by default', () => { + expect(generateWriterCodeForTlEntries([])).toMatchSnapshot() + }) + + it('should include static sizes calculations', () => { + const entries = parseTlToEntries( + 'test1 foo:int bar:int = Test;\n' + + 'test2 foo:int bar:double = Test;\n' + + 'test3 foo:int bar:bytes = Test;\n', // should not be included + ) + const code = generateWriterCodeForTlEntries(entries, { includeStaticSizes: true }) + + expect(code).toMatchSnapshot() }) }) diff --git a/packages/tl-utils/src/codegen/writer.ts b/packages/tl-utils/src/codegen/writer.ts index ae3622fa..c49626f8 100644 --- a/packages/tl-utils/src/codegen/writer.ts +++ b/packages/tl-utils/src/codegen/writer.ts @@ -204,11 +204,7 @@ export function generateWriterCodeForTlEntries(entries: TlEntry[], params = DEFA ret += '_bare:{\n' Object.keys(usedAsBareIds).forEach((id) => { - const entry = entries.find((e) => e.id === parseInt(id)) - - if (!entry) { - return - } + const entry = entries.find((e) => e.id === parseInt(id))! ret += generateWriterCodeForTlEntry(entry, { diff --git a/packages/tl-utils/src/diff.test.ts b/packages/tl-utils/src/diff.test.ts index dbb95bf7..2732411d 100644 --- a/packages/tl-utils/src/diff.test.ts +++ b/packages/tl-utils/src/diff.test.ts @@ -6,7 +6,7 @@ import { parseFullTlSchema } from './schema.js' import { TlEntryDiff, TlSchemaDiff } from './types.js' describe('generateTlEntriesDifference', () => { - const test = (tl: string[], expected: TlEntryDiff) => { + const test = (tl: string[], expected?: TlEntryDiff) => { const e = parseTlToEntries(tl.join('\n')) const res = generateTlEntriesDifference(e[0], e[1]) expect(res).toEqual(expected) @@ -99,281 +99,70 @@ describe('generateTlEntriesDifference', () => { }, }) }) + + it('shows args comments diff', () => { + test(['// @description a @foo Foo\ntest foo:int = Test;', '// @description a @foo Bar\ntest foo:int = Test;'], { + name: 'test', + arguments: { + added: [], + removed: [], + modified: [ + { + name: 'foo', + comment: { + old: 'Foo', + new: 'Bar', + }, + }, + ], + }, + }) + }) + + it('throws on incompatible entries', () => { + expect(() => test(['test1 = Test;', 'test2 = Test;'])).toThrow() + expect(() => test(['test = Test1;', 'test = Test2;'])).toThrow() + }) }) describe('generateTlSchemasDifference', () => { - const test = (tl1: string[], tl2: string[], expected: Partial) => { + const test = (tl1: string[], tl2: string[]) => { const a = parseFullTlSchema(parseTlToEntries(tl1.join('\n'))) const b = parseFullTlSchema(parseTlToEntries(tl2.join('\n'))) const res: Partial = generateTlSchemasDifference(a, b) - if (!('methods' in expected)) delete res.methods - if (!('classes' in expected)) delete res.classes - if (!('unions' in expected)) delete res.unions - - expect(res).toEqual(expected) + expect(res).toMatchSnapshot() } it('shows added constructors', () => { - test(['test1 = Test;'], ['test1 = Test;', 'test2 = Test;'], { - classes: { - added: [ - { - kind: 'class', - name: 'test2', - id: 3847402009, - type: 'Test', - arguments: [], - }, - ], - removed: [], - modified: [], - }, - }) + test(['test1 = Test;'], ['test1 = Test;', 'test2 = Test;']) }) it('shows removed constructors', () => { - test(['test1 = Test;', 'test2 = Test;'], ['test1 = Test;'], { - classes: { - removed: [ - { - kind: 'class', - name: 'test2', - id: 3847402009, - type: 'Test', - arguments: [], - }, - ], - added: [], - modified: [], - }, - }) + test(['test1 = Test;', 'test2 = Test;'], ['test1 = Test;']) }) it('shows modified constructors', () => { - test(['test foo:int = Test;'], ['test foo:Foo = Test;'], { - classes: { - removed: [], - added: [], - modified: [ - { - name: 'test', - arguments: { - added: [], - removed: [], - modified: [ - { - name: 'foo', - type: { - old: 'int', - new: 'Foo', - }, - }, - ], - }, - id: { - new: 3348640942, - old: 1331975629, - }, - }, - ], - }, - }) + test(['test foo:int = Test;'], ['test foo:Foo = Test;']) }) it('shows removed unions', () => { - test(['test foo:int = Test;', 'test1 = Test1;'], ['test foo:Foo = Test;'], { - unions: { - removed: [ - { - name: 'Test1', - classes: [ - { - kind: 'class', - name: 'test1', - id: 3739166976, - type: 'Test1', - arguments: [], - }, - ], - }, - ], - added: [], - modified: [], - }, - }) + test(['test foo:int = Test;', 'test1 = Test1;'], ['test foo:Foo = Test;']) }) it('shows added unions', () => { - test(['test foo:int = Test;'], ['test foo:Foo = Test;', 'test1 = Test1;'], { - unions: { - added: [ - { - name: 'Test1', - classes: [ - { - kind: 'class', - name: 'test1', - id: 3739166976, - type: 'Test1', - arguments: [], - }, - ], - }, - ], - removed: [], - modified: [], - }, - }) + test(['test foo:int = Test;'], ['test foo:Foo = Test;', 'test1 = Test1;']) }) it('shows modified unions', () => { - test(['test foo:int = Test;', 'test1 = Test;'], ['test foo:Foo = Test;', 'test2 = Test;'], { - unions: { - added: [], - removed: [], - modified: [ - { - name: 'Test', - classes: { - added: [ - { - kind: 'class', - name: 'test2', - id: 3847402009, - type: 'Test', - arguments: [], - }, - ], - removed: [ - { - kind: 'class', - name: 'test1', - id: 1809692154, - type: 'Test', - arguments: [], - }, - ], - modified: [], - }, - methods: { - added: [], - removed: [], - modified: [], - }, - }, - ], - }, - }) + test(['test foo:int = Test;', 'test1 = Test;'], ['test foo:Foo = Test;', 'test2 = Test;']) - test(['test foo:int = Test;', 'test1 = Test;'], ['test2 foo:Foo = Test;', 'test3 = Test;'], { - unions: { - added: [], - removed: [], - modified: [ - { - name: 'Test', - classes: { - added: [ - { - kind: 'class', - name: 'test2', - id: 711487159, - type: 'Test', - arguments: [ - { - name: 'foo', - type: 'Foo', - }, - ], - }, - { - kind: 'class', - name: 'test3', - id: 704164487, - type: 'Test', - arguments: [], - }, - ], - removed: [ - { - kind: 'class', - name: 'test', - id: 1331975629, - type: 'Test', - arguments: [ - { - name: 'foo', - type: 'int', - }, - ], - }, - { - kind: 'class', - name: 'test1', - id: 1809692154, - type: 'Test', - arguments: [], - }, - ], - modified: [], - }, - methods: { - added: [], - removed: [], - modified: [], - }, - }, - ], - }, - }) + test(['test foo:int = Test;', 'test1 = Test;'], ['test2 foo:Foo = Test;', 'test3 = Test;']) - test(['test = Test;', 'test1 = Test;'], ['test = Test1;', 'test1 = Test1;'], { - unions: { - added: [ - { - name: 'Test1', - classes: [ - { - kind: 'class', - name: 'test', - id: 1997819349, - type: 'Test1', - arguments: [], - }, - { - kind: 'class', - name: 'test1', - id: 3739166976, - type: 'Test1', - arguments: [], - }, - ], - }, - ], - removed: [ - { - name: 'Test', - classes: [ - { - kind: 'class', - name: 'test', - id: 471282454, - type: 'Test', - arguments: [], - }, - { - kind: 'class', - name: 'test1', - id: 1809692154, - type: 'Test', - arguments: [], - }, - ], - }, - ], - modified: [], - }, - }) + test(['test = Test;', 'test1 = Test;'], ['test = Test1;', 'test1 = Test1;']) + }) + + it('shows modified methods', () => { + test(['---functions---', 'test = Test;'], ['---functions---', 'test = Test2;']) }) }) diff --git a/packages/tl-utils/src/diff.ts b/packages/tl-utils/src/diff.ts index 5d1ef2b0..1426838e 100644 --- a/packages/tl-utils/src/diff.ts +++ b/packages/tl-utils/src/diff.ts @@ -1,4 +1,3 @@ -import { computeConstructorIdFromEntry } from './ctor-id.js' import { TlArgument, TlArgumentDiff, TlEntry, TlEntryDiff, TlFullSchema, TlSchemaDiff } from './types.js' import { stringifyArgumentType } from './utils.js' @@ -25,11 +24,13 @@ export function generateTlEntriesDifference(a: TlEntry, b: TlEntry): TlEntryDiff } if (a.id !== b.id) { - let oldId = a.id - let newId = b.id + const oldId = a.id + const newId = b.id - if (oldId === 0) oldId = computeConstructorIdFromEntry(a) - if (newId === 0) newId = computeConstructorIdFromEntry(b) + /* c8 ignore next 3 */ + if (oldId === 0 || newId === 0) { + throw new Error('Entry ID cannot be 0') + } if (oldId !== newId) { diff.id = { diff --git a/packages/tl-utils/src/parse.test.ts b/packages/tl-utils/src/parse.test.ts index 454bf755..b2d3af2a 100644 --- a/packages/tl-utils/src/parse.test.ts +++ b/packages/tl-utils/src/parse.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it } from 'vitest' +import { describe, expect, it, vi } from 'vitest' import { parseTlToEntries } from './parse.js' import { TlEntry } from './types.js' @@ -441,4 +441,56 @@ users.getUsers id:Vector = Vector; { prefix: 'mt_' }, ) }) + + it('correctly handles ---types---', () => { + test('---functions---\n' + 'testFn = Test;\n' + '---types---\n' + 'testType = Test;', [ + { + arguments: [], + id: 3671236185, + kind: 'method', + name: 'testFn', + type: 'Test', + }, + { + arguments: [], + id: 922588822, + kind: 'class', + name: 'testType', + type: 'Test', + }, + ]) + }) + + describe('errors', () => { + it('correctly throws errors if panicOnError is set', () => { + const invalidSchema = 'some invalid schema 🥴' + + expect(() => parseTlToEntries(invalidSchema, { panicOnError: true })).toThrow(invalidSchema) + }) + + it('gracefully continues if onError set', () => { + const schema = 'some invalid line 🥴\ntest = Test;' + const onError = vi.fn() + + const res = parseTlToEntries(schema, { onError }) + + expect(res).toEqual([ + { + arguments: [], + id: 471282454, + kind: 'class', + name: 'test', + type: 'Test', + }, + ]) + expect(onError).toHaveBeenCalledOnce() + }) + + it('correctly handles incorrect %Foo usage', () => { + expect(() => parseTlToEntries('test foo:%Foo = Test;', { panicOnError: true })).toThrow('not found') + expect(() => + parseTlToEntries('foo = Foo;\nbar = Foo;\ntest foo:%Foo = Test;', { panicOnError: true }), + ).toThrow('more than one') + }) + }) }) diff --git a/packages/tl-utils/src/parse.ts b/packages/tl-utils/src/parse.ts index 14f29b26..2127d989 100644 --- a/packages/tl-utils/src/parse.ts +++ b/packages/tl-utils/src/parse.ts @@ -66,6 +66,17 @@ export function parseTlToEntries( let currentComment = '' const prefix = params.prefix ?? '' + const handleError = (err: Error, entryIdx: number) => { + if (params.panicOnError) { + throw err + } else if (params.onError) { + params.onError(err, '', entryIdx) + /* c8 ignore next 3 */ + } else { + console.warn(err) + } + } + lines.forEach((line, idx) => { line = line.trim() @@ -108,15 +119,7 @@ export function parseTlToEntries( const match = SINGLE_REGEX.exec(line) if (!match) { - const err = new Error(`Failed to parse line ${idx + 1}: ${line}`) - - if (params.panicOnError) { - throw err - } else if (params.onError) { - params.onError(err, line, idx + 1) - } else { - console.warn(err) - } + handleError(new Error(`Failed to parse line ${idx + 1}: ${line}`), idx + 1) return } @@ -214,6 +217,8 @@ export function parseTlToEntries( params.onOrphanComment(currentComment) } + if (params.forIdComputation) return ret + // post-process: // - add return type ctor id for methods // - find arguments where type is not a union and put corresponding modifiers @@ -240,21 +245,19 @@ export function parseTlToEntries( return } - if (type in unions && arg.typeModifiers?.isBareUnion) { - if (unions[type].length !== 1) { - const err = new Error( - `Union ${type} has more than one entry, cannot use it like %${type} (found in ${entry.name}#${arg.name})`, + if (arg.typeModifiers?.isBareUnion) { + if (!(type in unions)) { + handleError(new Error(`Union ${type} not found (found in ${entry.name}#${arg.name})`), entryIdx) + } else if (unions[type].length !== 1) { + handleError( + new Error( + `Union ${type} has more than one entry, cannot use it like %${type} (found in ${entry.name}#${arg.name})`, + ), + entryIdx, ) - - if (params.panicOnError) { - throw err - } else if (params.onError) { - params.onError(err, '', entryIdx) - } else { - console.warn(err) - } + } else { + arg.typeModifiers.constructorId = unions[type][0].id } - arg.typeModifiers.constructorId = unions[type][0].id } else if (type in entries) { if (!arg.typeModifiers) arg.typeModifiers = {} arg.typeModifiers.isBareType = true diff --git a/packages/tl-utils/src/utils.test.ts b/packages/tl-utils/src/utils.test.ts new file mode 100644 index 00000000..80234750 --- /dev/null +++ b/packages/tl-utils/src/utils.test.ts @@ -0,0 +1,105 @@ +import { describe, expect, it } from 'vitest' + +import { + groupTlEntriesByNamespace, + parseArgumentType, + parseTdlibStyleComment, + splitNameToNamespace, + stringifyArgumentType, +} from './utils.js' + +describe('splitNameToNamespace', () => { + it('should split names correctly', () => { + expect(splitNameToNamespace('foo.bar')).toEqual(['foo', 'bar']) + }) + + it('should return null for namespace if there is none', () => { + expect(splitNameToNamespace('foo')).toEqual([null, 'foo']) + }) +}) + +describe('parseTdlibStyleComment', () => { + it('should parse comments correctly', () => { + expect(parseTdlibStyleComment('@foo Foo description @bar Bar description')).toEqual({ + foo: 'Foo description', + bar: 'Bar description', + }) + }) +}) + +describe('groupTlEntriesByNamespace', () => { + it('should group entries correctly', () => { + expect( + groupTlEntriesByNamespace([ + { + kind: 'class', + id: 0, + name: 'foo.bar', + type: 'FooBar', + arguments: [], + }, + { + kind: 'class', + id: 0, + name: 'foo.baz', + type: 'FooBaz', + arguments: [], + }, + { + kind: 'class', + id: 0, + name: 'bar', + type: 'Bar', + arguments: [], + }, + ]), + ).toMatchSnapshot() + }) +}) + +describe('stringifyArgumentType', () => { + it('should keep type as is if there are no modifiers', () => { + expect(stringifyArgumentType('Foo')).toEqual('Foo') + }) + + it('should stringify bare types', () => { + expect(stringifyArgumentType('Foo', { isBareUnion: true })).toEqual('%Foo') + expect(stringifyArgumentType('foo', { isBareType: true })).toEqual('foo') + }) + + it('should stringify vectors', () => { + expect(stringifyArgumentType('Foo', { isVector: true })).toEqual('Vector') + expect(stringifyArgumentType('Foo', { isVector: true, isBareUnion: true })).toEqual('Vector<%Foo>') + expect(stringifyArgumentType('Foo', { isBareVector: true })).toEqual('vector') + }) + + it('should stringify predicates', () => { + expect(stringifyArgumentType('Foo', { predicate: 'foo' })).toEqual('foo?Foo') + expect(stringifyArgumentType('Foo', { predicate: 'foo', isVector: true })).toEqual('foo?Vector') + }) +}) + +describe('parseArgumentType', () => { + it('should parse bare types', () => { + expect(parseArgumentType('%Foo')).toEqual(['Foo', { isBareUnion: true }]) + + // not enough info to derive that + expect(parseArgumentType('foo')).toEqual([ + 'foo', + { + /* isBareType: true */ + }, + ]) + }) + + it('should parse vectors', () => { + expect(parseArgumentType('Vector')).toEqual(['Foo', { isVector: true }]) + expect(parseArgumentType('Vector<%Foo>')).toEqual(['Foo', { isVector: true, isBareUnion: true }]) + expect(parseArgumentType('vector')).toEqual(['Foo', { isBareVector: true }]) + }) + + it('should parse predicates', () => { + expect(parseArgumentType('foo?Foo')).toEqual(['Foo', { predicate: 'foo' }]) + expect(parseArgumentType('foo?Vector')).toEqual(['Foo', { predicate: 'foo', isVector: true }]) + }) +}) diff --git a/packages/wasm/src/index.ts b/packages/wasm/src/index.ts index 52867d14..a2c516e0 100644 --- a/packages/wasm/src/index.ts +++ b/packages/wasm/src/index.ts @@ -27,6 +27,8 @@ function getUint8Memory() { return cachedUint8Memory } +/* c8 ignore start */ + /** * Init the WASM blob synchronously (e.g. by passing a `WebAssembly.Module` instance) */ @@ -43,6 +45,8 @@ export function initSync(module: SyncInitInput): void { initCommon() } +/* c8 ignore end */ + /** * Init the WASM blob asynchronously (e.g. by passing a URL to the WASM file) * @@ -98,6 +102,7 @@ export function gunzip(bytes: Uint8Array): Uint8Array { const ret = wasm.libdeflate_gzip_decompress(decompressor, inputPtr, bytes.length, outputPtr, size) + /* c8 ignore next 3 */ if (ret === -1) throw new Error('gunzip error -- bad data') if (ret === -2) throw new Error('gunzip error -- short output') if (ret === -3) throw new Error('gunzip error -- short input') // should never happen diff --git a/packages/wasm/src/init.ts b/packages/wasm/src/init.ts index 794bf8f9..da6bedca 100644 --- a/packages/wasm/src/init.ts +++ b/packages/wasm/src/init.ts @@ -13,6 +13,7 @@ export async function loadWasmBinary(input?: InitInput): Promise