@workos/oagen-emitters 0.4.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/ci.yml +1 -1
- package/.github/workflows/lint.yml +1 -1
- package/.github/workflows/release-please.yml +2 -2
- package/.github/workflows/release.yml +1 -1
- package/.husky/pre-push +11 -0
- package/.node-version +1 -1
- package/.release-please-manifest.json +1 -1
- package/CHANGELOG.md +15 -0
- package/README.md +35 -224
- package/dist/index.d.mts +9 -1
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +2 -15234
- package/dist/plugin-Dws9b6T7.mjs +21441 -0
- package/dist/plugin-Dws9b6T7.mjs.map +1 -0
- package/dist/plugin.d.mts +7 -0
- package/dist/plugin.d.mts.map +1 -0
- package/dist/plugin.mjs +2 -0
- package/docs/sdk-architecture/dotnet.md +5 -5
- package/oagen.config.ts +5 -373
- package/package.json +17 -41
- package/smoke/sdk-dotnet.ts +11 -5
- package/smoke/sdk-elixir.ts +11 -5
- package/smoke/sdk-go.ts +10 -4
- package/smoke/sdk-kotlin.ts +11 -5
- package/smoke/sdk-node.ts +11 -5
- package/smoke/sdk-php.ts +9 -4
- package/smoke/sdk-python.ts +10 -4
- package/smoke/sdk-ruby.ts +10 -4
- package/smoke/sdk-rust.ts +11 -5
- package/src/dotnet/index.ts +9 -7
- package/src/dotnet/manifest.ts +5 -11
- package/src/dotnet/models.ts +58 -82
- package/src/dotnet/naming.ts +44 -6
- package/src/dotnet/resources.ts +350 -29
- package/src/dotnet/tests.ts +44 -24
- package/src/dotnet/type-map.ts +44 -17
- package/src/dotnet/wrappers.ts +21 -10
- package/src/go/client.ts +35 -3
- package/src/go/enums.ts +4 -0
- package/src/go/index.ts +13 -8
- package/src/go/manifest.ts +5 -11
- package/src/go/models.ts +6 -1
- package/src/go/resources.ts +534 -73
- package/src/go/tests.ts +39 -3
- package/src/go/type-map.ts +8 -3
- package/src/go/wrappers.ts +79 -21
- package/src/index.ts +14 -0
- package/src/kotlin/client.ts +7 -2
- package/src/kotlin/enums.ts +30 -3
- package/src/kotlin/index.ts +3 -3
- package/src/kotlin/manifest.ts +9 -15
- package/src/kotlin/models.ts +97 -6
- package/src/kotlin/naming.ts +7 -1
- package/src/kotlin/resources.ts +370 -39
- package/src/kotlin/tests.ts +120 -6
- package/src/node/client.ts +38 -11
- package/src/node/field-plan.ts +12 -14
- package/src/node/fixtures.ts +39 -3
- package/src/node/index.ts +3 -3
- package/src/node/manifest.ts +4 -11
- package/src/node/models.ts +281 -37
- package/src/node/resources.ts +156 -52
- package/src/node/tests.ts +76 -27
- package/src/node/type-map.ts +1 -31
- package/src/node/utils.ts +96 -6
- package/src/node/wrappers.ts +31 -1
- package/src/php/index.ts +3 -3
- package/src/php/manifest.ts +5 -11
- package/src/php/models.ts +0 -33
- package/src/php/resources.ts +199 -18
- package/src/php/tests.ts +26 -2
- package/src/php/type-map.ts +16 -2
- package/src/php/wrappers.ts +6 -2
- package/src/plugin.ts +50 -0
- package/src/python/client.ts +13 -3
- package/src/python/enums.ts +28 -3
- package/src/python/index.ts +38 -30
- package/src/python/manifest.ts +5 -12
- package/src/python/models.ts +138 -1
- package/src/python/resources.ts +234 -17
- package/src/python/tests.ts +260 -16
- package/src/python/type-map.ts +16 -2
- package/src/ruby/client.ts +238 -0
- package/src/ruby/enums.ts +149 -0
- package/src/ruby/index.ts +93 -0
- package/src/ruby/manifest.ts +28 -0
- package/src/ruby/models.ts +360 -0
- package/src/ruby/naming.ts +187 -0
- package/src/ruby/rbi.ts +313 -0
- package/src/ruby/resources.ts +799 -0
- package/src/ruby/tests.ts +459 -0
- package/src/ruby/type-map.ts +97 -0
- package/src/ruby/wrappers.ts +161 -0
- package/src/shared/model-utils.ts +131 -7
- package/src/shared/naming-utils.ts +36 -0
- package/src/shared/non-spec-services.ts +13 -0
- package/src/shared/resolved-ops.ts +75 -1
- package/test/dotnet/client.test.ts +2 -2
- package/test/dotnet/manifest.test.ts +13 -12
- package/test/dotnet/models.test.ts +7 -9
- package/test/dotnet/resources.test.ts +135 -3
- package/test/dotnet/tests.test.ts +5 -5
- package/test/entrypoint.test.ts +89 -0
- package/test/go/client.test.ts +6 -6
- package/test/go/resources.test.ts +156 -7
- package/test/kotlin/models.test.ts +1 -1
- package/test/kotlin/resources.test.ts +210 -0
- package/test/node/models.test.ts +134 -1
- package/test/node/resources.test.ts +134 -26
- package/test/node/utils.test.ts +140 -0
- package/test/php/models.test.ts +5 -4
- package/test/php/resources.test.ts +66 -1
- package/test/plugin.test.ts +50 -0
- package/test/python/client.test.ts +56 -0
- package/test/python/manifest.test.ts +7 -7
- package/test/python/models.test.ts +99 -0
- package/test/python/resources.test.ts +294 -0
- package/test/python/tests.test.ts +91 -0
- package/test/ruby/client.test.ts +81 -0
- package/test/ruby/resources.test.ts +386 -0
- package/test/shared/resolved-ops.test.ts +122 -0
- package/tsconfig.json +1 -0
- package/tsdown.config.ts +1 -1
- package/dist/index.mjs.map +0 -1
- package/scripts/generate-php.js +0 -13
- package/scripts/git-push-with-published-oagen.sh +0 -21
|
@@ -614,4 +614,298 @@ describe('generateResources', () => {
|
|
|
614
614
|
// Deprecated query param without description
|
|
615
615
|
expect(content).toContain('legacy_param: (deprecated)');
|
|
616
616
|
});
|
|
617
|
+
|
|
618
|
+
it('generates parameter group dataclasses, union kwargs, and isinstance dispatch', () => {
|
|
619
|
+
const models: Model[] = [
|
|
620
|
+
{
|
|
621
|
+
name: 'Widget',
|
|
622
|
+
fields: [{ name: 'id', type: { kind: 'primitive', type: 'string' }, required: true }],
|
|
623
|
+
},
|
|
624
|
+
];
|
|
625
|
+
|
|
626
|
+
const services: Service[] = [
|
|
627
|
+
{
|
|
628
|
+
name: 'Widgets',
|
|
629
|
+
operations: [
|
|
630
|
+
{
|
|
631
|
+
name: 'listWidgets',
|
|
632
|
+
httpMethod: 'get',
|
|
633
|
+
path: '/widgets',
|
|
634
|
+
pathParams: [],
|
|
635
|
+
queryParams: [
|
|
636
|
+
{
|
|
637
|
+
name: 'limit',
|
|
638
|
+
type: { kind: 'primitive', type: 'integer' },
|
|
639
|
+
required: false,
|
|
640
|
+
},
|
|
641
|
+
{
|
|
642
|
+
name: 'after',
|
|
643
|
+
type: { kind: 'primitive', type: 'string' },
|
|
644
|
+
required: false,
|
|
645
|
+
},
|
|
646
|
+
{
|
|
647
|
+
name: 'order',
|
|
648
|
+
type: { kind: 'primitive', type: 'string' },
|
|
649
|
+
required: false,
|
|
650
|
+
},
|
|
651
|
+
{
|
|
652
|
+
name: 'parent_resource_id',
|
|
653
|
+
type: { kind: 'primitive', type: 'string' },
|
|
654
|
+
required: false,
|
|
655
|
+
},
|
|
656
|
+
{
|
|
657
|
+
name: 'parent_resource_type_slug',
|
|
658
|
+
type: { kind: 'primitive', type: 'string' },
|
|
659
|
+
required: false,
|
|
660
|
+
},
|
|
661
|
+
{
|
|
662
|
+
name: 'parent_resource_external_id',
|
|
663
|
+
type: { kind: 'primitive', type: 'string' },
|
|
664
|
+
required: false,
|
|
665
|
+
},
|
|
666
|
+
],
|
|
667
|
+
headerParams: [],
|
|
668
|
+
response: { kind: 'model', name: 'WidgetList' },
|
|
669
|
+
errors: [],
|
|
670
|
+
injectIdempotencyKey: false,
|
|
671
|
+
pagination: {
|
|
672
|
+
strategy: 'cursor',
|
|
673
|
+
param: 'after',
|
|
674
|
+
dataPath: 'data',
|
|
675
|
+
itemType: { kind: 'model', name: 'Widget' },
|
|
676
|
+
},
|
|
677
|
+
parameterGroups: [
|
|
678
|
+
{
|
|
679
|
+
name: 'parent_resource',
|
|
680
|
+
optional: false,
|
|
681
|
+
variants: [
|
|
682
|
+
{
|
|
683
|
+
name: 'by_id',
|
|
684
|
+
parameters: [
|
|
685
|
+
{
|
|
686
|
+
name: 'parent_resource_id',
|
|
687
|
+
type: { kind: 'primitive', type: 'string' },
|
|
688
|
+
required: true,
|
|
689
|
+
},
|
|
690
|
+
],
|
|
691
|
+
},
|
|
692
|
+
{
|
|
693
|
+
name: 'by_external_id',
|
|
694
|
+
parameters: [
|
|
695
|
+
{
|
|
696
|
+
name: 'parent_resource_type_slug',
|
|
697
|
+
type: { kind: 'primitive', type: 'string' },
|
|
698
|
+
required: true,
|
|
699
|
+
},
|
|
700
|
+
{
|
|
701
|
+
name: 'parent_resource_external_id',
|
|
702
|
+
type: { kind: 'primitive', type: 'string' },
|
|
703
|
+
required: true,
|
|
704
|
+
},
|
|
705
|
+
],
|
|
706
|
+
},
|
|
707
|
+
],
|
|
708
|
+
},
|
|
709
|
+
],
|
|
710
|
+
},
|
|
711
|
+
],
|
|
712
|
+
},
|
|
713
|
+
];
|
|
714
|
+
|
|
715
|
+
const ctxWithServices: EmitterContext = {
|
|
716
|
+
...ctx,
|
|
717
|
+
spec: { ...emptySpec, services, models },
|
|
718
|
+
};
|
|
719
|
+
|
|
720
|
+
const files = generateResources(services, ctxWithServices);
|
|
721
|
+
expect(files.length).toBe(1);
|
|
722
|
+
const content = files[0].content;
|
|
723
|
+
|
|
724
|
+
// dataclass import should be present
|
|
725
|
+
expect(content).toContain('from dataclasses import dataclass');
|
|
726
|
+
|
|
727
|
+
// Variant dataclass definitions
|
|
728
|
+
expect(content).toContain('@dataclass');
|
|
729
|
+
expect(content).toContain('class ParentResourceById:');
|
|
730
|
+
expect(content).toContain(' parent_resource_id: str');
|
|
731
|
+
expect(content).toContain('class ParentResourceByExternalId:');
|
|
732
|
+
expect(content).toContain(' parent_resource_type_slug: str');
|
|
733
|
+
expect(content).toContain(' parent_resource_external_id: str');
|
|
734
|
+
|
|
735
|
+
// Method signature should have the union kwarg, not individual grouped params
|
|
736
|
+
expect(content).toContain('parent_resource: Union[ParentResourceById, ParentResourceByExternalId],');
|
|
737
|
+
// Grouped params should NOT appear as individual kwargs
|
|
738
|
+
expect(content).not.toMatch(/^\s+parent_resource_id: str,$/m);
|
|
739
|
+
expect(content).not.toMatch(/^\s+parent_resource_type_slug: str,$/m);
|
|
740
|
+
expect(content).not.toMatch(/^\s+parent_resource_external_id: str,$/m);
|
|
741
|
+
|
|
742
|
+
// isinstance dispatch in method body
|
|
743
|
+
expect(content).toContain('if isinstance(parent_resource, ParentResourceById):');
|
|
744
|
+
expect(content).toContain('params["parent_resource_id"] = parent_resource.parent_resource_id');
|
|
745
|
+
expect(content).toContain('elif isinstance(parent_resource, ParentResourceByExternalId):');
|
|
746
|
+
expect(content).toContain('params["parent_resource_type_slug"] = parent_resource.parent_resource_type_slug');
|
|
747
|
+
expect(content).toContain('params["parent_resource_external_id"] = parent_resource.parent_resource_external_id');
|
|
748
|
+
|
|
749
|
+
// Docstring should document the group parameter
|
|
750
|
+
expect(content).toContain(
|
|
751
|
+
'parent_resource: Identifies the parent resource. One of: ParentResourceById, ParentResourceByExternalId.',
|
|
752
|
+
);
|
|
753
|
+
});
|
|
754
|
+
|
|
755
|
+
it('generates optional parameter group with Optional[Union[...]] = None', () => {
|
|
756
|
+
const models: Model[] = [
|
|
757
|
+
{
|
|
758
|
+
name: 'Thing',
|
|
759
|
+
fields: [{ name: 'id', type: { kind: 'primitive', type: 'string' }, required: true }],
|
|
760
|
+
},
|
|
761
|
+
];
|
|
762
|
+
|
|
763
|
+
const services: Service[] = [
|
|
764
|
+
{
|
|
765
|
+
name: 'Things',
|
|
766
|
+
operations: [
|
|
767
|
+
{
|
|
768
|
+
name: 'getThing',
|
|
769
|
+
httpMethod: 'get',
|
|
770
|
+
path: '/things/{id}',
|
|
771
|
+
pathParams: [{ name: 'id', type: { kind: 'primitive', type: 'string' }, required: true }],
|
|
772
|
+
queryParams: [
|
|
773
|
+
{
|
|
774
|
+
name: 'scope_id',
|
|
775
|
+
type: { kind: 'primitive', type: 'string' },
|
|
776
|
+
required: false,
|
|
777
|
+
},
|
|
778
|
+
{
|
|
779
|
+
name: 'scope_name',
|
|
780
|
+
type: { kind: 'primitive', type: 'string' },
|
|
781
|
+
required: false,
|
|
782
|
+
},
|
|
783
|
+
],
|
|
784
|
+
headerParams: [],
|
|
785
|
+
response: { kind: 'model', name: 'Thing' },
|
|
786
|
+
errors: [],
|
|
787
|
+
injectIdempotencyKey: false,
|
|
788
|
+
parameterGroups: [
|
|
789
|
+
{
|
|
790
|
+
name: 'scope',
|
|
791
|
+
optional: true,
|
|
792
|
+
variants: [
|
|
793
|
+
{
|
|
794
|
+
name: 'by_id',
|
|
795
|
+
parameters: [
|
|
796
|
+
{
|
|
797
|
+
name: 'scope_id',
|
|
798
|
+
type: { kind: 'primitive', type: 'string' },
|
|
799
|
+
required: true,
|
|
800
|
+
},
|
|
801
|
+
],
|
|
802
|
+
},
|
|
803
|
+
{
|
|
804
|
+
name: 'by_name',
|
|
805
|
+
parameters: [
|
|
806
|
+
{
|
|
807
|
+
name: 'scope_name',
|
|
808
|
+
type: { kind: 'primitive', type: 'string' },
|
|
809
|
+
required: true,
|
|
810
|
+
},
|
|
811
|
+
],
|
|
812
|
+
},
|
|
813
|
+
],
|
|
814
|
+
},
|
|
815
|
+
],
|
|
816
|
+
},
|
|
817
|
+
],
|
|
818
|
+
},
|
|
819
|
+
];
|
|
820
|
+
|
|
821
|
+
const ctxWithServices: EmitterContext = {
|
|
822
|
+
...ctx,
|
|
823
|
+
spec: { ...emptySpec, services, models },
|
|
824
|
+
};
|
|
825
|
+
|
|
826
|
+
const files = generateResources(services, ctxWithServices);
|
|
827
|
+
const content = files[0].content;
|
|
828
|
+
|
|
829
|
+
// Optional group should use Optional[Union[...]] = None
|
|
830
|
+
expect(content).toContain('scope: Optional[Union[ScopeById, ScopeByName]] = None,');
|
|
831
|
+
|
|
832
|
+
// Dataclass definitions
|
|
833
|
+
expect(content).toContain('class ScopeById:');
|
|
834
|
+
expect(content).toContain(' scope_id: str');
|
|
835
|
+
expect(content).toContain('class ScopeByName:');
|
|
836
|
+
expect(content).toContain(' scope_name: str');
|
|
837
|
+
|
|
838
|
+
// isinstance dispatch in the non-paginated GET body
|
|
839
|
+
expect(content).toContain('if isinstance(scope, ScopeById):');
|
|
840
|
+
expect(content).toContain('params["scope_id"] = scope.scope_id');
|
|
841
|
+
expect(content).toContain('elif isinstance(scope, ScopeByName):');
|
|
842
|
+
expect(content).toContain('params["scope_name"] = scope.scope_name');
|
|
843
|
+
});
|
|
844
|
+
|
|
845
|
+
it('uses body model field types for parameter group dataclasses', () => {
|
|
846
|
+
const models: Model[] = [
|
|
847
|
+
{
|
|
848
|
+
name: 'OrganizationMembership',
|
|
849
|
+
fields: [{ name: 'id', type: { kind: 'primitive', type: 'string' }, required: true }],
|
|
850
|
+
},
|
|
851
|
+
{
|
|
852
|
+
name: 'CreateOrganizationMembershipRequest',
|
|
853
|
+
fields: [
|
|
854
|
+
{ name: 'user_id', type: { kind: 'primitive', type: 'string' }, required: true },
|
|
855
|
+
{ name: 'organization_id', type: { kind: 'primitive', type: 'string' }, required: true },
|
|
856
|
+
{ name: 'role_slug', type: { kind: 'primitive', type: 'string' }, required: false },
|
|
857
|
+
{
|
|
858
|
+
name: 'role_slugs',
|
|
859
|
+
type: { kind: 'array', items: { kind: 'primitive', type: 'string' } },
|
|
860
|
+
required: false,
|
|
861
|
+
},
|
|
862
|
+
],
|
|
863
|
+
},
|
|
864
|
+
];
|
|
865
|
+
|
|
866
|
+
const services: Service[] = [
|
|
867
|
+
{
|
|
868
|
+
name: 'UserManagement',
|
|
869
|
+
operations: [
|
|
870
|
+
{
|
|
871
|
+
name: 'createOrganizationMembership',
|
|
872
|
+
httpMethod: 'post',
|
|
873
|
+
path: '/user_management/organization_memberships',
|
|
874
|
+
pathParams: [],
|
|
875
|
+
queryParams: [],
|
|
876
|
+
headerParams: [],
|
|
877
|
+
requestBody: { kind: 'model', name: 'CreateOrganizationMembershipRequest' },
|
|
878
|
+
response: { kind: 'model', name: 'OrganizationMembership' },
|
|
879
|
+
errors: [],
|
|
880
|
+
injectIdempotencyKey: false,
|
|
881
|
+
parameterGroups: [
|
|
882
|
+
{
|
|
883
|
+
name: 'role',
|
|
884
|
+
optional: true,
|
|
885
|
+
variants: [
|
|
886
|
+
{
|
|
887
|
+
name: 'single',
|
|
888
|
+
parameters: [{ name: 'role_slug', type: { kind: 'primitive', type: 'string' }, required: false }],
|
|
889
|
+
},
|
|
890
|
+
{
|
|
891
|
+
name: 'multiple',
|
|
892
|
+
parameters: [{ name: 'role_slugs', type: { kind: 'primitive', type: 'string' }, required: false }],
|
|
893
|
+
},
|
|
894
|
+
],
|
|
895
|
+
},
|
|
896
|
+
],
|
|
897
|
+
},
|
|
898
|
+
],
|
|
899
|
+
},
|
|
900
|
+
];
|
|
901
|
+
|
|
902
|
+
const ctxWithServices: EmitterContext = {
|
|
903
|
+
...ctx,
|
|
904
|
+
spec: { ...emptySpec, services, models },
|
|
905
|
+
};
|
|
906
|
+
|
|
907
|
+
const files = generateResources(services, ctxWithServices);
|
|
908
|
+
expect(files[0].content).toContain('class RoleMultiple:');
|
|
909
|
+
expect(files[0].content).toContain(' role_slugs: List[str]');
|
|
910
|
+
});
|
|
617
911
|
});
|
|
@@ -36,6 +36,7 @@ const services: Service[] = [
|
|
|
36
36
|
queryParams: [],
|
|
37
37
|
headerParams: [],
|
|
38
38
|
response: { kind: 'primitive', type: 'unknown' },
|
|
39
|
+
successResponses: [{ statusCode: 202, type: { kind: 'primitive', type: 'unknown' } }],
|
|
39
40
|
errors: [],
|
|
40
41
|
injectIdempotencyKey: false,
|
|
41
42
|
},
|
|
@@ -77,6 +78,7 @@ describe('generateTests', () => {
|
|
|
77
78
|
expect(content).toContain('class TestOrganizations:');
|
|
78
79
|
expect(content).toContain('def test_get_organization(');
|
|
79
80
|
expect(content).toContain('def test_delete_organization(');
|
|
81
|
+
expect(content).toContain('httpx_mock.add_response(status_code=202, content=b"\\n")');
|
|
80
82
|
expect(content).toContain('assert result is None');
|
|
81
83
|
expect(content).toContain('isinstance(result, Organization)');
|
|
82
84
|
});
|
|
@@ -107,6 +109,93 @@ describe('generateTests', () => {
|
|
|
107
109
|
expect(data).toHaveProperty('name');
|
|
108
110
|
});
|
|
109
111
|
|
|
112
|
+
it('generates discriminator dispatch tests for dispatcher models', () => {
|
|
113
|
+
const discriminatorModel: any = {
|
|
114
|
+
name: 'EventSchema',
|
|
115
|
+
fields: [],
|
|
116
|
+
discriminator: {
|
|
117
|
+
property: 'event',
|
|
118
|
+
mapping: {
|
|
119
|
+
'user.created': 'UserCreated',
|
|
120
|
+
'dsync.user.created': 'DsyncUserCreated',
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
const discModels: Model[] = [
|
|
126
|
+
discriminatorModel,
|
|
127
|
+
{
|
|
128
|
+
name: 'UserCreated',
|
|
129
|
+
fields: [
|
|
130
|
+
{ name: 'id', type: { kind: 'primitive', type: 'string' }, required: true },
|
|
131
|
+
{ name: 'event', type: { kind: 'literal', value: 'user.created' }, required: true },
|
|
132
|
+
{ name: 'data', type: { kind: 'primitive', type: 'unknown' }, required: true },
|
|
133
|
+
],
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
name: 'DsyncUserCreated',
|
|
137
|
+
fields: [
|
|
138
|
+
{ name: 'id', type: { kind: 'primitive', type: 'string' }, required: true },
|
|
139
|
+
{ name: 'event', type: { kind: 'literal', value: 'dsync.user.created' }, required: true },
|
|
140
|
+
{ name: 'data', type: { kind: 'primitive', type: 'unknown' }, required: true },
|
|
141
|
+
],
|
|
142
|
+
},
|
|
143
|
+
];
|
|
144
|
+
|
|
145
|
+
const discServices: Service[] = [
|
|
146
|
+
{
|
|
147
|
+
name: 'Events',
|
|
148
|
+
operations: [
|
|
149
|
+
{
|
|
150
|
+
name: 'listEvents',
|
|
151
|
+
httpMethod: 'get',
|
|
152
|
+
path: '/events',
|
|
153
|
+
pathParams: [],
|
|
154
|
+
queryParams: [],
|
|
155
|
+
headerParams: [],
|
|
156
|
+
response: { kind: 'model', name: 'EventSchema' },
|
|
157
|
+
errors: [],
|
|
158
|
+
injectIdempotencyKey: false,
|
|
159
|
+
pagination: {
|
|
160
|
+
strategy: 'cursor',
|
|
161
|
+
param: 'after',
|
|
162
|
+
dataPath: 'data',
|
|
163
|
+
itemType: { kind: 'model', name: 'EventSchema' },
|
|
164
|
+
},
|
|
165
|
+
},
|
|
166
|
+
],
|
|
167
|
+
},
|
|
168
|
+
];
|
|
169
|
+
|
|
170
|
+
const discSpec: ApiSpec = {
|
|
171
|
+
...spec,
|
|
172
|
+
models: discModels,
|
|
173
|
+
services: discServices,
|
|
174
|
+
enums: [],
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
const files = generateTests(discSpec, { ...ctx, spec: discSpec });
|
|
178
|
+
const roundTripTest = files.find((f) => f.path === 'tests/test_models_round_trip.py');
|
|
179
|
+
expect(roundTripTest).toBeDefined();
|
|
180
|
+
|
|
181
|
+
const content = roundTripTest!.content;
|
|
182
|
+
expect(content).toContain('class TestDiscriminatorDispatch:');
|
|
183
|
+
expect(content).toContain('def test_event_schema_dispatches_known_variant(self)');
|
|
184
|
+
expect(content).toContain('def test_event_schema_returns_unknown_for_unrecognized_type(self)');
|
|
185
|
+
expect(content).toContain('isinstance(result, EventSchemaUnknown)');
|
|
186
|
+
expect(content).toContain('def test_event_schema_raises_on_missing_discriminator(self)');
|
|
187
|
+
expect(content).toContain('def test_event_schema_raises_on_none_discriminator(self)');
|
|
188
|
+
expect(content).toContain('pytest.raises(Exception)');
|
|
189
|
+
|
|
190
|
+
// Service test should exercise discriminated union dispatch through pagination
|
|
191
|
+
const serviceTest = files.find((f) => f.path === 'tests/test_events.py');
|
|
192
|
+
expect(serviceTest).toBeDefined();
|
|
193
|
+
const svcContent = serviceTest!.content;
|
|
194
|
+
expect(svcContent).toContain('load_fixture("dsync_user_created.json")');
|
|
195
|
+
expect(svcContent).toContain('isinstance(page.data[0], DsyncUserCreated)');
|
|
196
|
+
expect(svcContent).toContain('assert len(page.data) == 1');
|
|
197
|
+
});
|
|
198
|
+
|
|
110
199
|
it('generates model edge-case and query/pagination regression tests', () => {
|
|
111
200
|
const edgeModels: Model[] = [
|
|
112
201
|
{
|
|
@@ -188,6 +277,8 @@ describe('generateTests', () => {
|
|
|
188
277
|
const roundTripTest = files.find((f) => f.path === 'tests/test_models_round_trip.py');
|
|
189
278
|
|
|
190
279
|
expect(serviceTest).toBeDefined();
|
|
280
|
+
expect(serviceTest!.content).toContain('assert len(page.data) == 1');
|
|
281
|
+
expect(serviceTest!.content).toContain('assert isinstance(page.data[0], Organization)');
|
|
191
282
|
expect(serviceTest!.content).toContain('def test_list_organizations_empty_page(');
|
|
192
283
|
expect(serviceTest!.content).toContain('def test_list_organizations_encodes_query_params(');
|
|
193
284
|
expect(serviceTest!.content).toContain('assert request.url.params["email"] == "value email/test"');
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import type { EmitterContext, ApiSpec, Service, Model } from '@workos/oagen';
|
|
3
|
+
import { defaultSdkBehavior } from '@workos/oagen';
|
|
4
|
+
import { generateClient } from '../../src/ruby/client.js';
|
|
5
|
+
|
|
6
|
+
const models: Model[] = [
|
|
7
|
+
{
|
|
8
|
+
name: 'Organization',
|
|
9
|
+
fields: [{ name: 'id', type: { kind: 'primitive', type: 'string' }, required: true }],
|
|
10
|
+
},
|
|
11
|
+
];
|
|
12
|
+
|
|
13
|
+
const services: Service[] = [
|
|
14
|
+
{
|
|
15
|
+
name: 'Organizations',
|
|
16
|
+
operations: [
|
|
17
|
+
{
|
|
18
|
+
name: 'listOrganizations',
|
|
19
|
+
httpMethod: 'get',
|
|
20
|
+
path: '/organizations',
|
|
21
|
+
pathParams: [],
|
|
22
|
+
queryParams: [],
|
|
23
|
+
headerParams: [],
|
|
24
|
+
response: { kind: 'model', name: 'Organization' },
|
|
25
|
+
errors: [],
|
|
26
|
+
injectIdempotencyKey: false,
|
|
27
|
+
},
|
|
28
|
+
],
|
|
29
|
+
},
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
const emptySpec: ApiSpec = {
|
|
33
|
+
name: 'Test',
|
|
34
|
+
version: '1.0.0',
|
|
35
|
+
baseUrl: 'https://api.example.com',
|
|
36
|
+
services,
|
|
37
|
+
models,
|
|
38
|
+
enums: [],
|
|
39
|
+
sdk: defaultSdkBehavior(),
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const ctx: EmitterContext = {
|
|
43
|
+
namespace: 'workos',
|
|
44
|
+
namespacePascal: 'WorkOS',
|
|
45
|
+
spec: emptySpec,
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
describe('generateClient (ruby)', () => {
|
|
49
|
+
it('generates inflections, main entry, and client files', () => {
|
|
50
|
+
const result = generateClient(emptySpec, ctx);
|
|
51
|
+
|
|
52
|
+
expect(result).toHaveLength(3);
|
|
53
|
+
expect(result[0].path).toBe('lib/workos/inflections.rb');
|
|
54
|
+
expect(result[1].path).toBe('lib/workos.rb');
|
|
55
|
+
expect(result[2].path).toBe('lib/workos/client.rb');
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('ignores inflections.rb in Zeitwerk loader', () => {
|
|
59
|
+
const result = generateClient(emptySpec, ctx);
|
|
60
|
+
const mainEntry = result[1].content;
|
|
61
|
+
|
|
62
|
+
expect(mainEntry).toContain('loader.ignore("#{__dir__}/workos/inflections.rb")');
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('ignores errors.rb in Zeitwerk loader', () => {
|
|
66
|
+
const result = generateClient(emptySpec, ctx);
|
|
67
|
+
const mainEntry = result[1].content;
|
|
68
|
+
|
|
69
|
+
expect(mainEntry).toContain('loader.ignore("#{__dir__}/workos/errors.rb")');
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('requires inflections before loader.setup', () => {
|
|
73
|
+
const result = generateClient(emptySpec, ctx);
|
|
74
|
+
const mainEntry = result[1].content;
|
|
75
|
+
|
|
76
|
+
const requireIdx = mainEntry.indexOf("require_relative 'workos/inflections'");
|
|
77
|
+
const setupIdx = mainEntry.indexOf('loader.setup');
|
|
78
|
+
expect(requireIdx).toBeGreaterThan(-1);
|
|
79
|
+
expect(setupIdx).toBeGreaterThan(requireIdx);
|
|
80
|
+
});
|
|
81
|
+
});
|