diff --git a/.golangci.yml b/.golangci.yml index a0ca1d4..2e5eba0 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -77,6 +77,7 @@ linters-settings: - name: unhandled-error arguments: - "fmt.Println" + - "strings.Builder.WriteString" # TODO remove - name: deep-exit disabled: true diff --git a/README.md b/README.md index 3591699..6f130d2 100644 --- a/README.md +++ b/README.md @@ -31,23 +31,97 @@ google: gtm: account_id: 6099238525 - server: - container_id: 175348980 - workspace_id: 10 - measurement_id: GTM-5NWPR4QW - web: container_id: 175355532 - workspace_id: 23 measurement_id: GTM-57BHX34G + workspace_id: 23 + server: + container_id: 175348980 + measurement_id: GTM-5NWPR4QW + workspace_id: 10 - credentials_file: ./tmp/google_service_account_creds.json + request_quota: 15 + credentials_file: ./google_service_account_creds.json -events: + +typescript: packages: - - path: "github.com/foomo/sesamy-cli/_example/server" - output_path: "./_example/client/types.d.ts" - indent: "\t" + - path: 'github.com/username/repository/event' + events: + - Custom + - path: 'github.com/foomo/sesamy-go/event' + events: + - PageView + - SelectItem + output_path: '/path/to/index.ts' + +tagmanager: + packages: + - path: 'github.com/username/repository/event' + events: + - Custom + - path: 'github.com/foomo/sesamy-go/event' + events: + - AddPaymentInfo + - AddShippingInfo + - AddToCart + - AddToWishlist + - AdImpression + - BeginCheckout + - CampaignDetails + - Click + - EarnVirtualMoney + - FileDownload + - FormStart + - FormSubmit + - GenerateLead + - JoinGroup + - LevelEnd + - LevelStart + - LevelUp + - Login + - PageView + - PostScore + - Purchase + - Refund + - RemoveFromCart + - ScreenView + - Scroll + - Search + - SelectContent + - SelectItem + - SelectPromotion + - SessionStart + - Share + - SignUp + - SpendVirtualCurrency + - TutorialBegin + - TutorialComplete + - UnlockAchievement + - UserEngagement + - VideoComplete + - VideoProgress + - VideoStart + - ViewCart + - ViewItem + - ViewItemList + - ViewPromotion + - ViewSearchResults + prefixes: + client: '' + folder: '' + tags: + ga4_event: 'GA4 - ' + google_tag: '' + server_ga4_event: 'GA4 - ' + triggers: + client: '' + custom_event: 'Event - ' + variables: + constant: '' + event_model: 'dlv.eventModel.' + gt_event_settings: 'Event Settings - ' + gt_settings: 'Settings - ' ``` ## Caveats diff --git a/_example/client/types.d.ts b/_example/client/types.d.ts deleted file mode 100644 index 3b08960..0000000 --- a/_example/client/types.d.ts +++ /dev/null @@ -1,55 +0,0 @@ -// Code generated by tygo. DO NOT EDIT. - -////////// -// source: addtocart.go - -export interface AddToCart { - currency?: string; - value?: number /* float64 */; - items?: (Item | undefined)[]; -} - -////////// -// source: item.go - -export interface Item { - affiliation?: string; - coupon?: string; - discount?: number /* float64 */; - index?: number /* int */; - item_brand?: string; - item_category?: string; - item_category2?: string; - item_category3?: string; - item_category4?: string; - item_category5?: string; - item_id?: string; - item_list_name?: string; - item_name?: string; - item_variant?: string; - item_list_id?: string; - location_id?: string; - price?: string; - quantity?: number /* float64 */; -} - -////////// -// source: login.go - -export interface Login { - method?: string; -} - -////////// -// source: search.go - -export interface Search { - search_term?: string; -} - -////////// -// source: signup.go - -export interface SignUp { - method?: string; -} diff --git a/_example/server/addtocart.go b/_example/server/addtocart.go deleted file mode 100644 index 5d044ed..0000000 --- a/_example/server/addtocart.go +++ /dev/null @@ -1,7 +0,0 @@ -package server - -type AddToCart struct { - Currency string `json:"currency,omitempty"` - Value float64 `json:"value,omitempty"` - Items []*Item `json:"items,omitempty"` -} diff --git a/_example/server/item.go b/_example/server/item.go deleted file mode 100644 index 385b6df..0000000 --- a/_example/server/item.go +++ /dev/null @@ -1,22 +0,0 @@ -package server - -type Item struct { - Affiliation string `json:"affiliation,omitempty"` - Coupon string `json:"coupon,omitempty"` - Discount float64 `json:"discount,omitempty"` - Index int `json:"index,omitempty"` - ItemBrand string `json:"item_brand,omitempty"` - ItemCategory string `json:"item_category,omitempty"` - ItemCategory2 string `json:"item_category2,omitempty"` - ItemCategory3 string `json:"item_category3,omitempty"` - ItemCategory4 string `json:"item_category4,omitempty"` - ItemCategory5 string `json:"item_category5,omitempty"` - ItemID string `json:"item_id,omitempty"` - ItemListName string `json:"item_list_name,omitempty"` - ItemName string `json:"item_name,omitempty"` - ItemVariant string `json:"item_variant,omitempty"` - ItemListID string `json:"item_list_id,omitempty"` - LocationID string `json:"location_id,omitempty"` - Price string `json:"price,omitempty"` - Quantity float64 `json:"quantity,omitempty"` -} diff --git a/_example/server/login.go b/_example/server/login.go deleted file mode 100644 index 876e19b..0000000 --- a/_example/server/login.go +++ /dev/null @@ -1,5 +0,0 @@ -package server - -type Login struct { - Method string `json:"method,omitempty"` -} diff --git a/_example/server/search.go b/_example/server/search.go deleted file mode 100644 index 5d5a01f..0000000 --- a/_example/server/search.go +++ /dev/null @@ -1,5 +0,0 @@ -package server - -type Search struct { - SearchTerm string `json:"search_term,omitempty"` -} diff --git a/_example/server/signup.go b/_example/server/signup.go deleted file mode 100644 index 5407b70..0000000 --- a/_example/server/signup.go +++ /dev/null @@ -1,5 +0,0 @@ -package server - -type SignUp struct { - Method string `json:"method,omitempty"` -} diff --git a/cmd/tagmanagerserver.go b/cmd/tagmanagerserver.go index c7efce3..66ff928 100644 --- a/cmd/tagmanagerserver.go +++ b/cmd/tagmanagerserver.go @@ -78,6 +78,22 @@ var tagmanagerServerCmd = &cobra.Command{ } } + var mpv2Client *tagmanager2.Client + { + name := p.ClientName("Measurement Protocol GA4") + if mpv2Client, err = c.UpsertClient(client.NewMPv2(name)); err != nil { + return err + } + } + + var mpv2ClientTrigger *tagmanager2.Trigger + { + name := p.Triggers.ClientName("Measurement Protocol GA4 Client") + if mpv2ClientTrigger, err = c.UpsertTrigger(trigger.NewClient(name, mpv2Client)); err != nil { + return err + } + } + var ga4Client *tagmanager2.Client { name := p.ClientName("Google Analytics GA4") @@ -96,7 +112,7 @@ var tagmanagerServerCmd = &cobra.Command{ { name := p.Tags.ServerGA4EventName("Google Analytics GA4") - if _, err := c.UpsertTag(client2.NewServerGA4Event(name, ga4MeasurementID, ga4ClientTrigger)); err != nil { + if _, err := c.UpsertTag(client2.NewServerGA4Event(name, ga4MeasurementID, ga4ClientTrigger, mpv2ClientTrigger)); err != nil { return err } } diff --git a/cmd/tagmanagerweb.go b/cmd/tagmanagerweb.go index 0c89284..3a4785c 100644 --- a/cmd/tagmanagerweb.go +++ b/cmd/tagmanagerweb.go @@ -26,7 +26,7 @@ var tagmanagerWebCmd = &cobra.Command{ clientCredentialsOption = option.WithCredentialsJSON([]byte(cfg.Google.CredentialsJSON)) } - eventParameters, err := internal.GetEventParameters(cfg.Tagmanager) + eventParameters, err := internal.GetStructTypeParameters(cmd.Context(), cfg.Tagmanager.Packages) if err != nil { return err } diff --git a/cmd/typescript.go b/cmd/typescript.go index 0a1a2b8..3f15fb2 100644 --- a/cmd/typescript.go +++ b/cmd/typescript.go @@ -1,7 +1,13 @@ package cmd import ( - "github.com/gzuidhof/tygo/tygo" + "os" + "path/filepath" + + "github.com/foomo/sesamy-cli/internal" + "github.com/foomo/sesamy-cli/pkg/typescript" + "github.com/pkg/errors" + "github.com/pterm/pterm" "github.com/spf13/cobra" ) @@ -11,14 +17,32 @@ var typescriptCmd = &cobra.Command{ Short: "Generate typescript events", PersistentPreRunE: preRunReadConfig, RunE: func(cmd *cobra.Command, args []string) error { - gen := tygo.New(&tygo.Config{ - Packages: cfg.Typescript.Packages, - }) - for k, v := range cfg.Typescript.TypeMappings { - gen.SetTypeMapping(k, v) + + eventTypes, err := internal.GetStructTypes(cmd.Context(), cfg.Typescript.Packages) + if err != nil { + return err } - return gen.Generate() + code, err := typescript.Generate(eventTypes) + if err != nil { + return err + } + + outPath, err := filepath.Abs(cfg.Typescript.OutputPath) + if err != nil { + return errors.Wrap(err, "failed to get output path") + } + pterm.Info.Printfln("Generated typescript code to: %s", outPath) + + if err = os.MkdirAll(filepath.Dir(outPath), os.ModePerm); err != nil { + return errors.Wrap(err, "failed to create typescript output directory") + } + + if err = os.WriteFile(outPath, []byte(code), 0600); err != nil { + return errors.Wrap(err, "failed to write typescript code") + } + + return nil }, } diff --git a/go.mod b/go.mod index 599a8bf..762624a 100644 --- a/go.mod +++ b/go.mod @@ -2,9 +2,12 @@ module github.com/foomo/sesamy-cli go 1.22.2 +//replace github.com/foomo/sesamy-go => ../sesamy-go + require ( github.com/fatih/structtag v1.2.0 - github.com/gzuidhof/tygo v0.2.14 + github.com/foomo/go v0.0.3 + github.com/foomo/sesamy-go v0.1.34-0.20240515111745-453e5159a1e7 github.com/mitchellh/mapstructure v1.5.0 github.com/pkg/errors v0.9.1 github.com/pterm/pterm v0.12.79 @@ -12,20 +15,22 @@ require ( github.com/spf13/viper v1.18.2 github.com/stoewer/go-strcase v1.3.0 github.com/stretchr/testify v1.9.0 + golang.org/x/exp v0.0.0-20230905200255-921286631fa9 golang.org/x/tools v0.21.0 - google.golang.org/api v0.178.0 + google.golang.org/api v0.180.0 ) require ( atomicgo.dev/cursor v0.2.0 // indirect atomicgo.dev/keyboard v0.2.9 // indirect atomicgo.dev/schedule v0.1.0 // indirect - cloud.google.com/go/auth v0.3.0 // indirect + cloud.google.com/go/auth v0.4.1 // indirect cloud.google.com/go/auth/oauth2adapt v0.2.2 // indirect cloud.google.com/go/compute/metadata v0.3.0 // indirect github.com/containerd/console v1.0.3 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/foomo/gostandards v0.1.0 // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/go-logr/logr v1.4.1 // indirect github.com/go-logr/stdr v1.2.2 // indirect @@ -57,10 +62,8 @@ require ( go.opentelemetry.io/otel v1.24.0 // indirect go.opentelemetry.io/otel/metric v1.24.0 // indirect go.opentelemetry.io/otel/trace v1.24.0 // indirect - go.uber.org/atomic v1.9.0 // indirect - go.uber.org/multierr v1.9.0 // indirect + go.uber.org/multierr v1.11.0 // indirect golang.org/x/crypto v0.23.0 // indirect - golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect golang.org/x/mod v0.17.0 // indirect golang.org/x/net v0.25.0 // indirect golang.org/x/oauth2 v0.20.0 // indirect diff --git a/go.sum b/go.sum index defdee3..68bbda5 100644 --- a/go.sum +++ b/go.sum @@ -7,8 +7,8 @@ atomicgo.dev/keyboard v0.2.9/go.mod h1:BC4w9g00XkxH/f1HXhW2sXmJFOCWbKn9xrOunSFtE atomicgo.dev/schedule v0.1.0 h1:nTthAbhZS5YZmgYbb2+DH8uQIZcTlIrd4eYr3UQxEjs= atomicgo.dev/schedule v0.1.0/go.mod h1:xeUa3oAkiuHYh8bKiQBRojqAMq3PXXbJujjb0hw8pEU= cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go/auth v0.3.0 h1:PRyzEpGfx/Z9e8+lHsbkoUVXD0gnu4MNmm7Gp8TQNIs= -cloud.google.com/go/auth v0.3.0/go.mod h1:lBv6NKTWp8E3LPzmO1TbiiRKc4drLOfHsgmlH9ogv5w= +cloud.google.com/go/auth v0.4.1 h1:Z7YNIhlWRtrnKlZke7z3GMqzvuYzdc2z98F9D1NV5Hg= +cloud.google.com/go/auth v0.4.1/go.mod h1:QVBuVEKpCn4Zp58hzRGvL0tjRGU0YqdRTdCHM1IHnro= cloud.google.com/go/auth/oauth2adapt v0.2.2 h1:+TTV8aXpjeChS9M+aTtN/TjdQnzJvmzKFt//oWu7HX4= cloud.google.com/go/auth/oauth2adapt v0.2.2/go.mod h1:wcYjgpZI9+Yu7LyYBg4pqSiaRkfEK3GQcpb7C/uyF1Q= cloud.google.com/go/compute/metadata v0.3.0 h1:Tz+eQXMEqDIKRsmY3cHTL6FVaynIjX2QxYC4trgAKZc= @@ -42,6 +42,12 @@ github.com/fatih/structtag v1.2.0 h1:/OdNE99OxoI/PqaW/SuSK9uxxT3f/tcSZgon/ssNSx4 github.com/fatih/structtag v1.2.0/go.mod h1:mBJUNpUnHmRKrKlQQlmCrh5PuhftFbNv8Ys4/aAZl94= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/foomo/go v0.0.3 h1:5pGzcPC78dImuBTT7nsZZnH+GIQUylbCtMkFEH26uZk= +github.com/foomo/go v0.0.3/go.mod h1:x6g64wiQusqaFElnh5rlk9unCgLKmfUWy0YFLejJxio= +github.com/foomo/gostandards v0.1.0 h1:dN6Yoj5un74W8hooC+boYcdbkTzF9jqU39q5kQCkzn4= +github.com/foomo/gostandards v0.1.0/go.mod h1:eyoFzndWb1kuDfupR/qf567mHeHZRi5//m64khreVac= +github.com/foomo/sesamy-go v0.1.34-0.20240515111745-453e5159a1e7 h1:SBK4F4iTYuNNYx7ZJa46xGafifZtYNa4F9ZzhgCiJzw= +github.com/foomo/sesamy-go v0.1.34-0.20240515111745-453e5159a1e7/go.mod h1:zeYfOTHDzH9cQF8UjWmOUrMoPUM6LlvmY7IrliA9roQ= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= @@ -88,8 +94,6 @@ github.com/gookit/color v1.4.2/go.mod h1:fqRyamkC1W8uxl+lxCQxOT09l/vYfZ+QeiX3rKQ github.com/gookit/color v1.5.0/go.mod h1:43aQb+Zerm/BWh2GnrgOQm7ffz7tvQXEKV6BFMl7wAo= github.com/gookit/color v1.5.4 h1:FZmqs7XOyGgCAxmWyPslpiok1k05wmY3SJTytgvYFs0= github.com/gookit/color v1.5.4/go.mod h1:pZJOeOS8DM43rXbp4AZo1n9zCU2qjpcRko0b6/QJi9w= -github.com/gzuidhof/tygo v0.2.14 h1:QRBD6eby2SMyYzv8KzXA+yopPbEO6w2Qzuuqqp9z+vU= -github.com/gzuidhof/tygo v0.2.14/go.mod h1:s3lpnppkDixQQhMWD78yPtAmugMHENsPWpQYziUIpw0= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= @@ -161,7 +165,6 @@ github.com/stoewer/go-strcase v1.3.0/go.mod h1:fAH5hQ5pehh+j3nZfvwdk2RgEgQjAoM8w github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= -github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= @@ -187,10 +190,8 @@ go.opentelemetry.io/otel/metric v1.24.0 h1:6EhoGWWK28x1fbpA4tYTOWBkPefTDQnb8WSGX go.opentelemetry.io/otel/metric v1.24.0/go.mod h1:VYhLe1rFfxuTXLgj4CBiyz+9WYBA8pNGJgDcSFRKBco= go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y1YELI= go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU= -go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= -go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= -go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI= -go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= @@ -269,8 +270,8 @@ golang.org/x/tools v0.21.0 h1:qc0xYgIbsSDt9EyWz05J5wfa7LOVW0YTLOXrqdLAWIw= golang.org/x/tools v0.21.0/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/api v0.178.0 h1:yoW/QMI4bRVCHF+NWOTa4cL8MoWL3Jnuc7FlcFF91Ok= -google.golang.org/api v0.178.0/go.mod h1:84/k2v8DFpDRebpGcooklv/lais3MEfqpaBLA12gl2U= +google.golang.org/api v0.180.0 h1:M2D87Yo0rGBPWpo1orwfCLehUUL6E7/TYe5gvMQWDh4= +google.golang.org/api v0.180.0/go.mod h1:51AiyoEg1MJPSZ9zvklA8VnRILPXxn1iVen9v25XHAE= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= diff --git a/internal/events.go b/internal/events.go deleted file mode 100644 index b8f727a..0000000 --- a/internal/events.go +++ /dev/null @@ -1,82 +0,0 @@ -package internal - -import ( - "fmt" - "go/ast" - "go/token" - "strings" - - "github.com/fatih/structtag" - "github.com/foomo/sesamy-cli/pkg/config" - "github.com/pterm/pterm" - "github.com/stoewer/go-strcase" - "golang.org/x/tools/go/packages" -) - -func GetEventParameters(source config.Tagmanager) (map[string][]string, error) { - ret := map[string][]string{} - - pkgs, err := packages.Load(&packages.Config{ - Mode: packages.NeedSyntax | packages.NeedFiles, - }, source.PackageNames()...) - if err != nil { - return nil, err - } - - for i, pkg := range pkgs { - if len(pkg.Errors) > 0 { - return nil, fmt.Errorf("%+v", pkg.Errors) - } - - if len(pkg.GoFiles) == 0 { - return nil, fmt.Errorf("no input go files for package index %d", i) - } - - conf := source.PackageConfig(pkg.ID) - - for i, file := range pkg.Syntax { - if conf.IsFileIgnored(pkg.GoFiles[i]) { - continue - } - - ast.Inspect(file, func(n ast.Node) bool { - // GenDecl can be an import, type, var, or const expression - if x, ok := n.(*ast.GenDecl); ok { - if x.Tok == token.IMPORT { - return false - } - - for _, spec := range x.Specs { - // e.g. "type Foo struct {}" or "type Bar = string" - if elem, ok := spec.(*ast.TypeSpec); ok && elem.Name.IsExported() { - if strct, ok := elem.Type.(*ast.StructType); ok { - name := strcase.SnakeCase(elem.Name.String()) - var fields []string - for _, field := range strct.Fields.List { - tags, err := structtag.Parse(strings.Trim(field.Tag.Value, "`")) - if err != nil { - pterm.Warning.Println(err.Error(), field.Tag.Value) - return false - } - tag, err := tags.Get("json") - if err != nil { - pterm.Warning.Println(err.Error()) - return false - } - if tag.Value() != "" && tag.Value() != "-" { - fields = append(fields, strings.Split(tag.Value(), ",")[0]) - } - } - ret[name] = fields - } - } - } - return false - } - return true - }) - } - } - - return ret, nil -} diff --git a/internal/reflect.go b/internal/reflect.go new file mode 100644 index 0000000..fcdaf75 --- /dev/null +++ b/internal/reflect.go @@ -0,0 +1,433 @@ +package internal + +import ( + "context" + "fmt" + "go/ast" + "go/token" + "go/types" + "maps" + "slices" + + "github.com/foomo/sesamy-cli/pkg/config" + "github.com/pkg/errors" + "github.com/pterm/pterm" + "github.com/stoewer/go-strcase" + "golang.org/x/tools/go/packages" +) + +func GetStructTypes(ctx context.Context, cfg config.Packages) (map[string]*types.Struct, error) { + ret := map[string]*types.Struct{} + + fset := token.NewFileSet() + pkgs, err := packages.Load(&packages.Config{ + Mode: packages.NeedName | packages.NeedTypesInfo | + packages.NeedFiles | packages.NeedImports | packages.NeedDeps | + packages.NeedModule | packages.NeedTypes | packages.NeedSyntax, + Context: ctx, + Logf: func(format string, args ...any) { + pterm.Debug.Printfln(format, args...) + }, + Fset: fset, + }, cfg.PackageNames()...) + if err != nil { + return nil, err + } + + for _, pkg := range pkgs { + if len(pkg.Errors) > 0 { + return nil, errors.Wrap(pkg.Errors[0], "packages contain errors") + } + packageStructs, err := getPackageStructs(cfg, pkg) + if err != nil { + return nil, err + } + maps.Copy(ret, packageStructs) + } + + return ret, nil +} + +func GetStructTypeParameters(ctx context.Context, cfg config.Packages) (map[string][]string, error) { + typs, err := GetStructTypes(ctx, cfg) + if err != nil { + return nil, err + } + + ret := map[string][]string{} + for name, t := range typs { + var fields []string + for i := range t.NumFields() { + tag, err := ParseTag(t.Tag(i)) + if err != nil { + return nil, err + } + if tag != "" { + fields = append(fields, tag) + } + } + ret[strcase.SnakeCase(name)] = fields + } + return ret, nil +} + +type Struct struct { + Name string `json:"name,omitempty"` + Attributes []Attribute `json:"attributes,omitempty"` +} + +type Attribute struct { + Name string `json:"name,omitempty"` + Tags map[string]string `json:"tags,omitempty"` +} + +type ( + Scanner struct { + cfg *Config + pkgs []*packages.Package + Packages map[string]*ScannerPackage + } + ScannerPackage struct { + pkg *packages.Package + Name string + PkgPath string + Imports map[*types.Package][]string + Scope ScannerScope + Values ScannerValues + } + ScannerScope map[string]types.Type +) + +func (s ScannerScope) LookupUnderlyingTypeName(name string) map[string]types.Type { + ret := map[string]types.Type{} + for i, i2 := range s { + if x, ok := i2.(*types.Named); ok && i != name && x.Obj().Name() == name { + ret[i] = i2 + } + } + return ret +} + +type ( + Config struct { + Packages []*ConfigPackage + } + ConfigPackage struct { + Path string + Names []string + } +) + +func (c *Config) PackageNames(path string) []string { + for _, configPackage := range c.Packages { + if configPackage.Path == path { + return configPackage.Names + } + } + return nil +} + +func (c *Config) PackagePaths() []string { + ret := make([]string, len(c.Packages)) + for i, p := range c.Packages { + ret[i] = p.Path + } + return ret +} + +func NewScanner(cfg *Config) *Scanner { + return &Scanner{ + cfg: cfg, + Packages: map[string]*ScannerPackage{}, + } +} + +func (s *Scanner) Scan(ctx context.Context) error { + var err error + // load packages + s.pkgs, err = packages.Load(&packages.Config{ + Mode: packages.NeedName | packages.NeedTypesInfo | + packages.NeedFiles | packages.NeedImports | packages.NeedDeps | + packages.NeedModule | packages.NeedTypes | packages.NeedSyntax, + Context: ctx, + // Fset: token.NewFileSet(), + }, s.cfg.PackagePaths()...) + if err != nil { + return err + } + + // iterate packages + for _, pkg := range s.pkgs { + // retrieve requested packages names + if names := s.cfg.PackageNames(pkg.PkgPath); len(names) > 0 { + s.pkgPackage(pkg, names...) + } + } + + return nil +} + +// ------------------------------------------------------------------------------------------------ +// ~ Private methods +// ------------------------------------------------------------------------------------------------ + +func (s *Scanner) pkgPackage(pkg *packages.Package, names ...string) { + if _, ok := s.Packages[pkg.PkgPath]; !ok { + s.Packages[pkg.PkgPath] = NewScannerPackage(pkg) + } + // add request scopes + added := s.Packages[pkg.PkgPath].typesScope(names) + + // check underlying added scopes + for _, name := range added { + s.typesType(pkg, s.Packages[pkg.PkgPath].Scope[name].Underlying()) + } +} + +func (s *Scanner) typesType(pkg *packages.Package, v types.Type) { + switch t := v.(type) { + case *types.Struct: + // iterate fields + for i := range t.NumFields() { + s.typesVar(pkg, t.Field(i)) + } + default: + fmt.Println(t) + } +} + +func (s *Scanner) typesVar(pkg *packages.Package, v *types.Var) { + if !v.Exported() { + return + } + switch t := v.Type().(type) { + case *types.Named: + if p, ok := pkg.Imports[v.Pkg().Path()]; ok { + s.pkgPackage(p, t.Obj().Name()) + } + } +} + +type ScannerValues map[string]*ast.ValueSpec + +func (s ScannerValues) Lookup(name string) *ast.ValueSpec { + return s[name] +} + +func NewScannerPackage(pkg *packages.Package) *ScannerPackage { + values := ScannerValues{} + for _, file := range pkg.Syntax { + for _, decl := range file.Decls { + if genDecl, ok := decl.(*ast.GenDecl); ok { + for _, spec := range genDecl.Specs { + if valueSpec, ok := spec.(*ast.ValueSpec); ok && len(valueSpec.Names) > 0 { + values[valueSpec.Names[0].Name] = valueSpec + } + } + } + } + } + + return &ScannerPackage{ + pkg: pkg, + Name: pkg.Name, + PkgPath: pkg.PkgPath, + Imports: map[*types.Package][]string{}, + Scope: ScannerScope{}, + Values: values, + } +} + +func (s *ScannerPackage) typesScope(names []string) []string { + var added []string + scope := s.pkg.Types.Scope() + + // iterate requested names + for _, name := range names { + // check if already within the local scope + if _, ok := s.Scope[name]; !ok { + // lookup scope object + if obj := scope.Lookup(name); obj != nil { + // add type to local scope + s.Scope[name] = obj.Type() + // scan the underlying type + s.typesType(obj.Type().Underlying()) + // append to added scopes + added = append(added, name) + } + + // FIXME find underlying type usages e.g. `const Foo ` + for _, i := range scope.Names() { + child := scope.Lookup(i) + if x, ok := child.Type().(*types.Named); ok && x.Obj().Name() == name { + s.Scope[i] = x + added = append(added, i) + } + } + } + } + return added +} + +func (s *ScannerPackage) typesType(v types.Type) { + switch t := v.(type) { + case *types.Struct: + for i := range t.NumFields() { + s.typesVar(t.Field(i)) + } + } +} + +func (s *ScannerPackage) typesVar(v *types.Var) { + if v.Exported() { + if p, ok := v.Type().(*types.Named); ok { + pkg := p.Obj().Pkg().Path() + if s.pkg.PkgPath != pkg { + if tv, ok := v.Type().(*types.Named); ok { + typeName := tv.Obj().Name() + if !slices.Contains(s.Imports[v.Pkg()], typeName) { + s.Imports[v.Pkg()] = append(s.Imports[v.Pkg()], typeName) + } + } + } + } else { + pterm.Debug.Println("unhandled typeVar") + } + } +} + +// -------------------- + +type Package struct { + pkg *packages.Package + Types []*Type `json:"s,omitempty"` +} + +type Type struct { + Spec *ast.TypeSpec + TypeInfo types.TypeAndValue +} + +func NewPackage(pkg *packages.Package) *Package { + inst := &Package{ + pkg: pkg, + } + for _, file := range pkg.Syntax { + ast.Inspect(file, inst.astNode) + } + return inst +} + +func (s *Package) Name() string { + return s.pkg.Name +} + +func (s *Package) Path() string { + return s.pkg.PkgPath +} + +// ------------------------------------------------------------------------------------------------ +// ~ Private methods +// ------------------------------------------------------------------------------------------------ + +func (s *Package) astNode(n ast.Node) bool { + switch t := n.(type) { + case *ast.File: + for _, d := range t.Decls { + s.astDecl(d) + } + } + return false +} + +func (s *Package) astDecl(v ast.Decl) { + switch t := v.(type) { + case *ast.GenDecl: + s.astGenDecl(t) + } +} + +func (s *Package) astGenDecl(v *ast.GenDecl) { + switch v.Tok { + case token.IMPORT: + return + default: + for _, spec := range v.Specs { + s.astSpec(spec) + } + } +} + +func (s *Package) astSpec(v ast.Spec) { + switch t := v.(type) { + case *ast.TypeSpec: + if t.Name.IsExported() { + s.astTypeSpec(t) + } + } +} + +func (s *Package) astTypeSpec(v *ast.TypeSpec) { + r := &Type{Spec: v} + ast.Inspect(v, s.astNode) + switch t := v.Type.(type) { + case *ast.IndexExpr: + if value, ok := s.typeInfo(t.Index); ok { + r.TypeInfo = value + } + default: + return + } + s.Types = append(s.Types, r) +} + +func (s *Package) typeInfo(n ast.Expr) (types.TypeAndValue, bool) { + v, ok := s.pkg.TypesInfo.Types[n] + return v, ok +} + +func getPackageStructs(cfg config.Packages, pkg *packages.Package) (map[string]*types.Struct, error) { + ret := map[string]*types.Struct{} + if len(pkg.GoFiles) == 0 { + return nil, fmt.Errorf("no input go files for package index") + } + + pkgCfg, err := cfg.PackageConfig(pkg.ID) + if err != nil { + return nil, err + } + + for _, file := range pkg.Syntax { + ast.Inspect(file, func(n ast.Node) bool { + // GenDecl can be an import, type, var, or const expression + if genDecl, ok := n.(*ast.GenDecl); ok { + if genDecl.Tok == token.IMPORT { + return false + } + for _, spec := range genDecl.Specs { + // e.g. "type Foo struct {}" or "type Bar = string" + if typeSpec, ok := spec.(*ast.TypeSpec); ok && typeSpec.Name.IsExported() { + if t, ok := typeSpec.Type.(*ast.IndexExpr); ok { + x := pkg.TypesInfo.Types[t] + fmt.Println(x) + } + if !pkgCfg.ExportEvent(typeSpec.Name.String()) { + continue + } + if indexExpr, ok := typeSpec.Type.(*ast.IndexExpr); ok { + if indexType, ok := pkg.TypesInfo.Types[indexExpr.Index]; ok { + if s, ok := indexType.Type.Underlying().(*types.Struct); ok { + ret[typeSpec.Name.String()] = s + } + } + } + } + } + return false + } + return true + }) + } + + return ret, nil +} diff --git a/internal/reflect_test.go b/internal/reflect_test.go new file mode 100644 index 0000000..1f5b744 --- /dev/null +++ b/internal/reflect_test.go @@ -0,0 +1,57 @@ +package internal_test + +import ( + "context" + "encoding/json" + "testing" + + "github.com/foomo/sesamy-cli/internal" + "github.com/foomo/sesamy-cli/pkg/config" + _ "github.com/foomo/sesamy-go" // force inclusion + _ "github.com/foomo/sesamy-go/event/params" // force inclusion + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestGetEventParameters(t *testing.T) { + params, err := internal.GetStructTypeParameters(context.TODO(), config.Packages{ + { + Path: "github.com/foomo/sesamy-go/event", + Events: []string{ + "PageView", + "SelectItem", + "AddToCart", + }, + }, + }) + require.NoError(t, err) + + actual, err := json.Marshal(params) + require.NoError(t, err) + + expected := `{"add_to_cart":["currency","value","items"],"page_view":["page_title","page_location"],"select_item":["item_list_id","item_list_name","items"]}` + if !assert.JSONEq(t, expected, string(actual)) { + t.Log(string(actual)) + } +} + +func TestScanner(t *testing.T) { + scanner := internal.NewScanner(&internal.Config{ + Packages: []*internal.ConfigPackage{ + { + Path: "github.com/foomo/sesamy-go/event", + Names: []string{"PageView"}, + }, + }, + }) + err := scanner.Scan(context.TODO()) + require.NoError(t, err) + + // actual, err := json.Marshal(scanner) + // require.NoError(t, err) + // + // expected := `{"add_to_cart":["currency","value","items"],"page_view":["page_title","page_location"],"select_item":["item_list_id","item_list_name","items"]}` + // if !assert.JSONEq(t, expected, string(actual)) { + // t.Log(string(actual)) + // } +} diff --git a/internal/utils.go b/internal/utils.go new file mode 100644 index 0000000..9199536 --- /dev/null +++ b/internal/utils.go @@ -0,0 +1,25 @@ +package internal + +import ( + "strings" + + "github.com/fatih/structtag" +) + +func ParseTag(value string) (string, error) { + tags, err := structtag.Parse(strings.Trim(value, "`")) + if err != nil { + return "", err + } + + tag, err := tags.Get("json") + if err != nil { + return "", err + } + + if tag.Value() != "" && tag.Value() != "-" { + return strings.Split(tag.Value(), ",")[0], nil + } + + return "", nil +} diff --git a/pkg/config/package.go b/pkg/config/package.go new file mode 100644 index 0000000..5c838ab --- /dev/null +++ b/pkg/config/package.go @@ -0,0 +1,15 @@ +package config + +type Package struct { + Path string `yaml:"path"` + Events []string `yaml:"events"` +} + +func (c Package) ExportEvent(event string) bool { + for _, name := range c.Events { + if name == event { + return true + } + } + return false +} diff --git a/pkg/config/packages.go b/pkg/config/packages.go new file mode 100644 index 0000000..a0a3272 --- /dev/null +++ b/pkg/config/packages.go @@ -0,0 +1,24 @@ +package config + +import ( + "github.com/pkg/errors" +) + +type Packages []Package + +func (c Packages) PackageNames() []string { + ret := make([]string, len(c)) + for i, value := range c { + ret[i] = value.Path + } + return ret +} + +func (c Packages) PackageConfig(path string) (Package, error) { + for _, value := range c { + if value.Path == path { + return value, nil + } + } + return Package{}, errors.Errorf("package for path '%s' not found", path) +} diff --git a/pkg/config/tagmanager.go b/pkg/config/tagmanager.go index 51f8ae4..466fea2 100644 --- a/pkg/config/tagmanager.go +++ b/pkg/config/tagmanager.go @@ -1,28 +1,6 @@ package config -import ( - "github.com/gzuidhof/tygo/tygo" -) - type Tagmanager struct { - Packages []*tygo.PackageConfig `yaml:"packages"` - TypeMappings map[string]string `yaml:"type_mappings"` - Prefixes TagmanagerPrefixes `yaml:"prefixes"` -} - -func (e Tagmanager) PackageNames() []string { - ret := make([]string, len(e.Packages)) - for i, value := range e.Packages { - ret[i] = value.Path - } - return ret -} - -func (e Tagmanager) PackageConfig(path string) *tygo.PackageConfig { - for _, value := range e.Packages { - if value.Path == path { - return value - } - } - return nil + Packages Packages `yaml:"packages"` + Prefixes TagmanagerPrefixes `yaml:"prefixes"` } diff --git a/pkg/config/typescript.go b/pkg/config/typescript.go index a79b1d4..8c533b0 100644 --- a/pkg/config/typescript.go +++ b/pkg/config/typescript.go @@ -1,27 +1,6 @@ package config -import ( - "github.com/gzuidhof/tygo/tygo" -) - type Typescript struct { - Packages []*tygo.PackageConfig `yaml:"packages"` - TypeMappings map[string]string `yaml:"type_mappings"` -} - -func (e Typescript) PackageNames() []string { - ret := make([]string, len(e.Packages)) - for i, value := range e.Packages { - ret[i] = value.Path - } - return ret -} - -func (e Typescript) PackageConfig(path string) *tygo.PackageConfig { - for _, value := range e.Packages { - if value.Path == path { - return value - } - } - return nil + Packages Packages `yaml:"packages"` + OutputPath string `yaml:"output_path"` } diff --git a/pkg/tagmanager/client/mpv2.go b/pkg/tagmanager/client/mpv2.go new file mode 100644 index 0000000..2fe1646 --- /dev/null +++ b/pkg/tagmanager/client/mpv2.go @@ -0,0 +1,19 @@ +package client + +import ( + "google.golang.org/api/tagmanager/v2" +) + +func NewMPv2(name string) *tagmanager.Client { + return &tagmanager.Client{ + Name: name, + Parameter: []*tagmanager.Parameter{ + { + Type: "template", + Key: "activationPath", + Value: "/mp/collect", + }, + }, + Type: "mpaw_client", + } +} diff --git a/pkg/tagmanager/client_test.go b/pkg/tagmanager/client_test.go index 9dfd640..27f01d1 100644 --- a/pkg/tagmanager/client_test.go +++ b/pkg/tagmanager/client_test.go @@ -8,6 +8,8 @@ import ( "os" "testing" + testingx "github.com/foomo/go/testing" + tagx "github.com/foomo/go/testing/tag" "github.com/foomo/sesamy-cli/pkg/tagmanager" "github.com/foomo/sesamy-cli/pkg/tagmanager/trigger" "github.com/foomo/sesamy-cli/pkg/tagmanager/variable" @@ -18,7 +20,7 @@ import ( ) func TestNewClient_Server(t *testing.T) { - t.Skip() + testingx.Tags(t, tagx.Skip) c, err := tagmanager.NewClient( context.TODO(), @@ -101,7 +103,7 @@ func TestNewClient_Server(t *testing.T) { } func TestNewClient_Web(t *testing.T) { - t.Skip() + testingx.Tags(t, tagx.Skip) c, err := tagmanager.NewClient( context.TODO(), @@ -395,25 +397,6 @@ func TestNewClient_Web(t *testing.T) { // ~ Private methods // ------------------------------------------------------------------------------------------------ -// func eventParameters(event any) []string { -// if event == nil { -// return nil -// } -// var res []string -// v := reflect.TypeOf(event) -// -// if v.Kind() == reflect.Ptr { -// v = v.Elem() -// } -// for i := range v.NumField() { -// tag := v.Field(i).Tag.Get("json") -// if tag != "" && tag != "-" { -// res = append(res, strings.Split(tag, ",")[0]) -// } -// } -// return res -// } - func dump(t *testing.T, i interface{ MarshalJSON() ([]byte, error) }) { t.Helper() var ret bytes.Buffer diff --git a/pkg/tagmanager/tag/serverga4event.go b/pkg/tagmanager/tag/serverga4event.go index 5d15be7..737675a 100644 --- a/pkg/tagmanager/tag/serverga4event.go +++ b/pkg/tagmanager/tag/serverga4event.go @@ -4,9 +4,14 @@ import ( "google.golang.org/api/tagmanager/v2" ) -func NewServerGA4Event(name string, measurementID *tagmanager.Variable, trigger *tagmanager.Trigger) *tagmanager.Tag { +func NewServerGA4Event(name string, measurementID *tagmanager.Variable, triggers ...*tagmanager.Trigger) *tagmanager.Tag { + triggerIDs := make([]string, len(triggers)) + for i, trigger := range triggers { + triggerIDs[i] = trigger.TriggerId + } + return &tagmanager.Tag{ - FiringTriggerId: []string{trigger.TriggerId}, + FiringTriggerId: triggerIDs, Name: name, Parameter: []*tagmanager.Parameter{ { diff --git a/pkg/typescript/builder.go b/pkg/typescript/builder.go new file mode 100644 index 0000000..7f86149 --- /dev/null +++ b/pkg/typescript/builder.go @@ -0,0 +1,245 @@ +package typescript + +import ( + "context" + "fmt" + "go/ast" + "go/types" + "slices" + "sort" + "strings" + + "github.com/foomo/sesamy-cli/internal" + "github.com/stoewer/go-strcase" + "golang.org/x/exp/maps" +) + +type Builder struct { + scanner *internal.Scanner + packageNameReplacer *strings.Replacer +} + +func NewBuilder(scanner *internal.Scanner) *Builder { + return &Builder{ + scanner: scanner, + packageNameReplacer: strings.NewReplacer(".", "_", "/", "_", "-", "_"), + } +} + +// func (b *Builder) AddStruct(name string, s *types.Struct) { +// b.structs[name] = s +// } +// +// func (b *Builder) AddStructs(v map[string]*types.Struct) { +// maps.Copy(b.structs, v) +// } +// +// func (b *Builder) AddImport(pkg string, names ...string) { +// b.imports[pkg] = append(b.imports[pkg], names...) +// } + +type Imports map[string][]string + +func (i Imports) String() string { + var ret string + keys := maps.Keys(i) + sort.Strings(keys) + for _, name := range keys { + vals := i[name] + slices.Sort(vals) + ret += fmt.Sprintf("import { %s } from '%s';\n", strings.Join(vals, ", "), name) + } + return ret +} + +type Package struct { + pkg *internal.ScannerPackage + packageNameReplacer *strings.Replacer + Filename string + headBuilder *SBuilder + importsBuilder *SBuilder + strucstBuilder *SBuilder +} + +type SBuilder struct { + *strings.Builder +} + +func NewSBuilder() *SBuilder { + return &SBuilder{ + Builder: &strings.Builder{}, + } +} + +func (s SBuilder) NL() { + s.WriteString("\n") +} + +func (s SBuilder) Commentf(indent int, format string, args ...interface{}) { + prefix := strings.Repeat("\t", indent) + s.WriteString(fmt.Sprintf(prefix+"// "+format+"\n", args...)) +} + +func (s SBuilder) Codef(indent int, format string, args ...interface{}) { + prefix := strings.Repeat("\t", indent) + s.WriteString(fmt.Sprintf(prefix+format+"\n", args...)) +} + +func NewPackage(pkg *internal.ScannerPackage, packageNameReplacer *strings.Replacer) *Package { + return &Package{ + pkg: pkg, + Filename: packageNameReplacer.Replace(pkg.PkgPath) + ".ts", + packageNameReplacer: packageNameReplacer, + headBuilder: NewSBuilder(), + importsBuilder: NewSBuilder(), + strucstBuilder: NewSBuilder(), + } +} + +func (b *Package) EventPackage(ctx context.Context) (string, error) { + // render header + b.headBuilder.Commentf(0, `Code generated by sesamy. DO NOT EDIT.`) + + // render imports + for name := range b.pkg.Imports { + name = b.packageNameReplacer.Replace(name) + b.importsBuilder.Codef(0, `import * as %s from './%s.ts';`, name, name) + } + b.importsBuilder.Codef(0, `import { collect } from '@foomo/sesamy';`) + + // iterate scope types + names := maps.Keys(b.pkg.Scope) + slices.Sort(names) + for _, name := range names { + scope := b.pkg.Scope[name] + switch t := scope.Underlying().(type) { + case *types.Struct: + b.strucstBuilder.Codef(0, "export interface %s {", name) + for i := range t.NumFields() { + tag, err := internal.ParseTag(t.Tag(i)) + if err != nil { + return "", err + } + if tag != "" { + b.strucstBuilder.Codef(1, "%s: %s;", tag, b.typeType(t.Field(i).Type())) + } + } + b.strucstBuilder.Codef(0, "};") + b.strucstBuilder.NL() + + b.strucstBuilder.Codef(0, "export const %s = (params: %s) => {", strcase.LowerCamelCase(name), name) + b.strucstBuilder.Codef(1, "collect({ name:'%s', params });", strcase.SnakeCase(name)) + b.strucstBuilder.Codef(0, "};") + b.strucstBuilder.NL() + } + } + + s := &strings.Builder{} + s.WriteString(b.headBuilder.String()) + s.WriteString(b.importsBuilder.String()) + s.WriteString("\n") + s.WriteString(b.strucstBuilder.String()) + return s.String(), nil +} + +func (b *Package) DependencyPackage(ctx context.Context) (string, error) { + // render header + b.headBuilder.Commentf(0, `Code generated by sesamy. DO NOT EDIT.`) + + // render imports + for name := range b.pkg.Imports { + name = b.packageNameReplacer.Replace(name) + b.importsBuilder.Codef(0, `import * as %s from './%s.ts';`, name, name) + } + + // iterate scope types + names := maps.Keys(b.pkg.Scope) + slices.Sort(names) + for _, name := range names { + scope := b.pkg.Scope[name] + + switch scope.Underlying().(type) { + case *types.Basic: + // lookup scope type with underlying types e.g. const ScopeTypeCustom ScopeType = "custom" + if u := b.pkg.Scope.LookupUnderlyingTypeName(name); len(u) > 0 { + uNames := maps.Keys(u) + sort.Strings(uNames) + b.strucstBuilder.Codef(0, "export enum %s {", name) + for _, uName := range uNames { + if valueSpec, ok := b.pkg.Values.Lookup(uName).Values[0].(*ast.BasicLit); ok { + b.strucstBuilder.Codef(1, "%s = %s,", strings.TrimPrefix(uName, name), valueSpec.Value) + } + } + b.strucstBuilder.Codef(0, "};") + } + } + } + + s := &strings.Builder{} + s.WriteString(b.headBuilder.String()) + s.WriteString(b.importsBuilder.String()) + s.WriteString("\n") + s.WriteString(b.strucstBuilder.String()) + return s.String(), nil +} + +func (b *Builder) Package(ctx context.Context, paths []string) (map[string]string, error) { + var err error + ret := make(map[string]string) + for name, scannerPackage := range b.scanner.Packages { + var s string + pkg := NewPackage(scannerPackage, b.packageNameReplacer) + if slices.Contains(paths, scannerPackage.PkgPath) { + s, err = pkg.EventPackage(ctx) + } else { + s, err = pkg.DependencyPackage(ctx) + } + if err != nil { + return nil, err + } + ret[name] = s + } + return ret, nil +} + +// func (b *Builder) String(pkgs []*internal.Package) (map[string]string, error) { +// ret := make(map[string]string) +// for _, pkg := range pkgs { +// s, err := b.Package(pkg) +// if err != nil { +// return nil, err +// } +// ret[b.packageNameReplacer.Replace(pkg.Path)] = s +// } +// return ret, nil +// } + +func (b *Package) typeType(value types.Type) string { + switch t := value.(type) { + case *types.Basic: + return b.basicInfoType(t.Info()) + case *types.Named: + return b.namedType(t) + "." + t.Obj().Name() + case *types.Slice: + return "Array<" + b.typeType(t.Elem()) + ">" + default: + return "any" + } +} + +func (b *Package) namedType(t *types.Named) string { + return b.packageNameReplacer.Replace(t.Obj().Pkg().Path()) +} + +func (b *Package) basicInfoType(c types.BasicInfo) string { + switch { + case c&types.IsNumeric != 0: + return "number" + case c&types.IsString != 0: + return "string" + case c&types.IsBoolean != 0: + return "boolean" + default: + return "any" + } +} diff --git a/pkg/typescript/builder_test.go b/pkg/typescript/builder_test.go new file mode 100644 index 0000000..f686ead --- /dev/null +++ b/pkg/typescript/builder_test.go @@ -0,0 +1,130 @@ +package typescript_test + +import ( + "context" + "testing" + + "github.com/foomo/sesamy-cli/internal" + "github.com/foomo/sesamy-cli/pkg/typescript" + _ "github.com/foomo/sesamy-go" // force inclusion + _ "github.com/foomo/sesamy-go/event/params" // force inclusion + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestBuiler(t *testing.T) { + cfg := &internal.Config{ + Packages: []*internal.ConfigPackage{ + { + Path: "github.com/foomo/sesamy-go/event", + Names: []string{"PageView", "AddToCart"}, + }, + }, + } + scanner := internal.NewScanner(cfg) + err := scanner.Scan(context.TODO()) + + // structTypes, err := internal.GetStructTypes(context.TODO(), config.Packages{ + // { + // Path: "github.com/foomo/sesamy-go/event", + // Events: []string{ + // "PageView", + // "SelectItem", + // "AddToCart", + // }, + // }, + // }) + // require.NoError(t, err) + + b := typescript.NewBuilder(scanner) + // b.AddStructs(structTypes) + actual, err := b.Package(context.Background(), cfg.PackagePaths()) + require.NoError(t, err) + + { + expected := ` +// Code generated by sesamy. DO NOT EDIT. +import * as github_com_foomo_sesamy_go from './github_com_foomo_sesamy_go.ts'; +import { collect } from '@foomo/sesamy'; + +export interface AddToCart { + name: github_com_foomo_sesamy_go.EventName; + params: github_com_foomo_sesamy_go_event_params.AddToCart; +}; + +export const addToCart = (params: AddToCart) => { + collect({ name:'add_to_cart', params }); +}; + +export interface PageView { + name: github_com_foomo_sesamy_go.EventName; + params: github_com_foomo_sesamy_go_event_params.PageView; +}; + +export const pageView = (params: PageView) => { + collect({ name:'page_view', params }); +}; + +` + if !assert.Equal(t, expected, "\n"+actual["github.com/foomo/sesamy-go/event"]) { + t.Log("\n" + actual["github.com/foomo/sesamy-go/event"]) + } + } + + { + expected := ` +// Code generated by sesamy. DO NOT EDIT. + +export enum EventName { + AdImpression = "ad_impression", + AddPaymentInfo = "add_payment_info", + AddShippingInfo = "add_shipping_info", + AddToCart = "add_to_cart", + AddToWishlist = "add_to_wishlit", + BeginCheckout = "begin_checkout", + CampaignDetails = "campaign_details", + Click = "click", + EarnVirtualMoney = "earn_virtual_money", + FileDownload = "file_download", + FormStart = "form_start", + FormSubmit = "form_submit", + GenerateLead = "generate_lead", + JoinGroup = "join_group", + LevelEnd = "level_end", + LevelStart = "level_start", + LevelUp = "level_up", + Login = "login", + PageView = "page_view", + PostScore = "post_score", + Purchase = "purchase", + Refund = "refund", + RemoveFromCart = "remove_from_cart", + ScreenView = "screen_view", + Scroll = "scroll", + Search = "search", + SelectContent = "select_content", + SelectItem = "select_item", + SelectPromotion = "select_promotion", + SessionStart = "session_start", + Share = "share", + SignUp = "sign_up", + SpendVirtualCurrency = "spend_virtual_currency", + TutorialBegin = "tutorial_begin", + TutorialComplete = "tutorial_complete", + UnlockArchievement = "unlock_archievement", + UserEngagement = "user_engagement", + VideoComplete = "video_complete", + VideoProgress = "video_progress", + VideoStart = "video_start", + ViewCart = "view_cart", + ViewItem = "view_item", + ViewItemList = "view_item_list", + ViewPromotion = "view_promotion", + ViewSearchResults = "view_search_results", +}; +` + if !assert.Equal(t, expected, "\n"+actual["github.com/foomo/sesamy-go"]) { + t.Log("\n" + actual["github.com/foomo/sesamy-go"]) + } + } +} diff --git a/pkg/typescript/utils.go b/pkg/typescript/utils.go new file mode 100644 index 0000000..81e93b8 --- /dev/null +++ b/pkg/typescript/utils.go @@ -0,0 +1,31 @@ +package typescript + +// func TSType(value types.Type) string { +// switch t := value.(type) { +// case *types.Basic: +// return tsType(t.Info()) +// case *types.Named: +// return TSType(t.Obj().Type().Underlying()) +// case *types.Slice: +// var t string +// if s, ok := t.Elem().(*types.Named); ok { +// s.Obj() +// } +// return "Array<" + TSType(t.Elem().Underlying()) + ">" +// default: +// return "any" +// } +// } +// +// func tsType(c types.BasicInfo) string { +// switch { +// case c&types.IsNumeric != 0: +// return "number" +// case c&types.IsString != 0: +// return "string" +// case c&types.IsBoolean != 0: +// return "boolean" +// default: +// return "any" +// } +// } diff --git a/_example/sesamy.yaml b/sesamy.yaml similarity index 92% rename from _example/sesamy.yaml rename to sesamy.yaml index de741cc..8ea066f 100644 --- a/_example/sesamy.yaml +++ b/sesamy.yaml @@ -30,9 +30,9 @@ typescript: tagmanager: packages: - path: 'github.com/foomo/sesamy-cli/_example/server' - output_path: './_example/client/types.d.ts' - exclude_files: - - item.go + events: + - PageView + - SelectItem prefixes: client: '' folder: ''