mirror of
https://github.com/foomo/sesamy-cli.git
synced 2025-10-16 12:35:36 +00:00
wip: need added
This commit is contained in:
parent
d2c61b5a25
commit
ab48d6326a
@ -77,6 +77,7 @@ linters-settings:
|
||||
- name: unhandled-error
|
||||
arguments:
|
||||
- "fmt.Println"
|
||||
- "strings.Builder.WriteString"
|
||||
# TODO remove
|
||||
- name: deep-exit
|
||||
disabled: true
|
||||
|
||||
96
README.md
96
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
|
||||
|
||||
55
_example/client/types.d.ts
vendored
55
_example/client/types.d.ts
vendored
@ -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;
|
||||
}
|
||||
@ -1,7 +0,0 @@
|
||||
package server
|
||||
|
||||
type AddToCart struct {
|
||||
Currency string `json:"currency,omitempty"`
|
||||
Value float64 `json:"value,omitempty"`
|
||||
Items []*Item `json:"items,omitempty"`
|
||||
}
|
||||
@ -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"`
|
||||
}
|
||||
@ -1,5 +0,0 @@
|
||||
package server
|
||||
|
||||
type Login struct {
|
||||
Method string `json:"method,omitempty"`
|
||||
}
|
||||
@ -1,5 +0,0 @@
|
||||
package server
|
||||
|
||||
type Search struct {
|
||||
SearchTerm string `json:"search_term,omitempty"`
|
||||
}
|
||||
@ -1,5 +0,0 @@
|
||||
package server
|
||||
|
||||
type SignUp struct {
|
||||
Method string `json:"method,omitempty"`
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
15
go.mod
15
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
|
||||
|
||||
23
go.sum
23
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=
|
||||
|
||||
@ -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
|
||||
}
|
||||
433
internal/reflect.go
Normal file
433
internal/reflect.go
Normal file
@ -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 <name>`
|
||||
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
|
||||
}
|
||||
57
internal/reflect_test.go
Normal file
57
internal/reflect_test.go
Normal file
@ -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))
|
||||
// }
|
||||
}
|
||||
25
internal/utils.go
Normal file
25
internal/utils.go
Normal file
@ -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
|
||||
}
|
||||
15
pkg/config/package.go
Normal file
15
pkg/config/package.go
Normal file
@ -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
|
||||
}
|
||||
24
pkg/config/packages.go
Normal file
24
pkg/config/packages.go
Normal file
@ -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)
|
||||
}
|
||||
@ -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"`
|
||||
}
|
||||
|
||||
@ -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"`
|
||||
}
|
||||
|
||||
19
pkg/tagmanager/client/mpv2.go
Normal file
19
pkg/tagmanager/client/mpv2.go
Normal file
@ -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",
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
|
||||
@ -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{
|
||||
{
|
||||
|
||||
245
pkg/typescript/builder.go
Normal file
245
pkg/typescript/builder.go
Normal file
@ -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"
|
||||
}
|
||||
}
|
||||
130
pkg/typescript/builder_test.go
Normal file
130
pkg/typescript/builder_test.go
Normal file
@ -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"])
|
||||
}
|
||||
}
|
||||
}
|
||||
31
pkg/typescript/utils.go
Normal file
31
pkg/typescript/utils.go
Normal file
@ -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"
|
||||
// }
|
||||
// }
|
||||
@ -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: ''
|
||||
Loading…
Reference in New Issue
Block a user