github.com/stripe/stripe-go/v76@v76.25.0/scripts/check_api_clients/main.go (about) 1 // A script that tries to make sure that all API clients (structs called 2 // `Client`) defined throughout all subpackages are included in the master list 3 // as a field on the `client.API` type. Adding a new client to `client.API` has 4 // historically been something that's easily forgotten. 5 package main 6 7 import ( 8 "fmt" 9 "go/ast" 10 "go/parser" 11 "go/token" 12 "io/ioutil" 13 "os" 14 "path/filepath" 15 "regexp" 16 "sort" 17 "strings" 18 ) 19 20 func main() { 21 // 22 // DEBUGGING 23 // 24 // As you can see, working with Go ASTs is quite verbose, and made not 25 // great by all the type casting that's going on. The official docs for the 26 // packages provide only the most basic information about how to use them. 27 // 28 // BY FAR the easiest way to debug something is to just print the AST node 29 // you're interested in (it takes in an `interface{}`). The output is 30 // verbose, detailed, and extremely informative. For example: 31 // 32 // ast.Print(fset, f) 33 // 34 35 fset := token.NewFileSet() 36 37 // 38 // Step 1: See what clients are in `client.API` 39 // 40 41 // Returned as a map to facilitate fast set checking. 42 packagesInClientAPI, err := getClientAPIPackages(fset) 43 if err != nil { 44 exitWithError(err) 45 } 46 47 // 48 // Step 2: See what clients are in `client.API` 49 // 50 51 var packagesWithClient []string 52 err = filepath.Walk(".", func(path string, f os.FileInfo, _ error) error { 53 if filepath.Ext(path) != ".go" { 54 return nil 55 } 56 57 if strings.HasSuffix(filepath.Base(path), "_test.go") { 58 return nil 59 } 60 61 packageName, err := findClientType(fset, path) 62 if err != nil { 63 exitWithError(err) 64 } 65 66 if packageName == nil { 67 return nil 68 } 69 70 // Prepend a directory so that we know where nested packages are 71 // nested. 72 // 73 // For example, `session` will become `checkout/session`. 74 relativeDir := filepath.Dir(path) 75 76 // We're not interested in the immediate parent directory because that 77 // has the same name as the package itself, so strip that off the end. 78 relativeDir = strings.TrimSuffix(relativeDir, *packageName) 79 80 if relativeDir != "" { 81 relativePackageName := relativeDir + *packageName 82 packageName = &relativePackageName 83 } 84 85 packagesWithClient = append(packagesWithClient, *packageName) 86 return nil 87 }) 88 if err != nil { 89 exitWithError(err) 90 } 91 92 if len(packagesWithClient) < 1 { 93 panic("Found no packages with clients; something went wrong " + 94 "(maybe check the working directory?)") 95 } 96 97 sort.Strings(packagesWithClient) 98 99 var anyMissing bool 100 for _, packageName := range packagesWithClient { 101 _, ok := packagesInClientAPI[packageName] 102 if !ok { 103 if !anyMissing { 104 fmt.Fprintf(os.Stderr, "!!! the following clients are missing from client.%s in %s\n", 105 typeNameAPI, clientAPIPath) 106 anyMissing = true 107 } 108 109 fmt.Fprintf(os.Stderr, "%s.Client\n", packageName) 110 } 111 } 112 113 if anyMissing { 114 os.Exit(1) 115 } 116 } 117 118 // 119 // Private 120 // 121 122 const ( 123 // Names of some of the Go types in stripe-go that we're interested in. 124 typeNameAPI = "API" 125 typeNameClient = "Client" 126 127 // Path to file containing the `API` type. 128 clientAPIPath = "./client/api.go" 129 ) 130 131 func exitWithError(err error) { 132 fmt.Fprintf(os.Stderr, "%v\n", err) 133 os.Exit(1) 134 } 135 136 // findType looks for a Go type defined in the given AST and returns its 137 // specification. 138 func findType(f *ast.File, typeName string) *ast.TypeSpec { 139 for _, decl := range f.Decls { 140 genDecl, ok := decl.(*ast.GenDecl) 141 142 // We're only looking for `Client` structs which are always `GenDecl` 143 // of type `TYPE`. 144 if !ok || genDecl.Tok != token.TYPE { 145 continue 146 } 147 148 if len(genDecl.Specs) > 1 { 149 panic("Expected only a single ast.Spec for GenDecl with Tok of Type") 150 } 151 152 typeSpec, ok := genDecl.Specs[0].(*ast.TypeSpec) 153 if !ok { 154 panic("Expected ast.TypeSpec in GenDecl with Tok of Type") 155 } 156 157 if typeSpec.Name.Name != typeName { 158 continue 159 } 160 161 // Type names are unique with packages, so return early. 162 return typeSpec 163 } 164 165 return nil 166 } 167 168 // findClientType looks for a type called `Client` in the `.go` file at the 169 // target path. If found, it returns the name of the file's defined package. 170 func findClientType(fset *token.FileSet, path string) (*string, error) { 171 source, err := ioutil.ReadFile(path) 172 if err != nil { 173 return nil, err 174 } 175 176 f, err := parser.ParseFile(fset, "", source, 0) 177 if err != nil { 178 return nil, err 179 } 180 181 packageName := f.Name.Name 182 183 typeSpec := findType(f, typeNameClient) 184 if typeSpec == nil { 185 return nil, nil 186 } 187 188 return &packageName, nil 189 190 } 191 192 // getClientAPIPackages parses the master `API` type found in `client/api.go`, 193 // looks for all fields that have a type named `Client`, and returns a map 194 // containing the packages of those clients. 195 func getClientAPIPackages(fset *token.FileSet) (map[string]struct{}, error) { 196 packagesInClientAPI := make(map[string]struct{}) 197 198 source, err := ioutil.ReadFile(clientAPIPath) 199 if err != nil { 200 return nil, err 201 } 202 203 f, err := parser.ParseFile(fset, "", source, 0) 204 if err != nil { 205 return nil, err 206 } 207 208 // Regular expression that targets just the last segment(s) of the package 209 // path like `coupon` or `issuing/card`. Note that double quotes on either 210 // side are also stripped. 211 packagePathRE := regexp.MustCompile(`"github.com/stripe/stripe-go/v[0-9]+/(.*)"`) 212 213 // First we need to make a map of any packages that are being imported 214 // in ways that don't map perfectly well with the package paths that we'll 215 // extract by looking for clients in `.go` files. 216 // 217 // The first case are packages imported under aliases. We often do this for 218 // namespaced packages that have a high probability of collision with 219 // something else. For example, `issuing/card` gets imported as 220 // `issuingcard`. 221 // 222 // The second case are nested packages that are referenced are still 223 // referenced by their local package name. For example, 224 // `reporting/reportrun` might have been aliased as `reportingreportrun` if 225 // things were perfectly consisted, but its package name is already unique 226 // enough that no one bothered to do so. 227 importAliases := make(map[string]string) 228 for _, importSpec := range f.Imports { 229 path := importSpec.Path.Value 230 231 // Trim to just the last segment(s) of the package path like `coupon` 232 // or `issuing/card`. 233 path = packagePathRE.ReplaceAllString(path, "$1") 234 235 // A non-nil `Name` is an alias. Save the alias to our map with the 236 // relative package path. 237 if importSpec.Name != nil { 238 importAliases[importSpec.Name.Name] = path 239 continue 240 } 241 242 parts := strings.Split(path, "/") 243 244 // A top-level packaage. No need to keep an alias around for it. 245 if len(parts) == 1 { 246 continue 247 } 248 249 // Otherwise, store the alias as the last component of the path keyed 250 // to the entire relative path. 251 importAliases[parts[len(parts)-1]] = path 252 } 253 254 typeSpec := findType(f, typeNameAPI) 255 if typeSpec == nil { 256 return nil, fmt.Errorf("No 'API' type found in '%s'", clientAPIPath) 257 } 258 259 structType, ok := typeSpec.Type.(*ast.StructType) 260 if !ok { 261 panic(fmt.Sprintf("Expected %s type to be a struct", typeNameAPI)) 262 } 263 264 for _, field := range structType.Fields.List { 265 // A "StarExpr" is a pointer like `*charge.Client`. The type 266 // `charge.Client` is wrapped within it. 267 // 268 // We expect all clients to be pointers, so skip any defined fields 269 // that aren't one. 270 starExpr, ok := field.Type.(*ast.StarExpr) 271 if !ok { 272 continue 273 } 274 275 selectorExpr, ok := starExpr.X.(*ast.SelectorExpr) 276 if !ok { 277 continue 278 } 279 280 // Only look for fields with types named 'Client' 281 if selectorExpr.Sel.Name != typeNameClient { 282 continue 283 } 284 285 ident, ok := selectorExpr.X.(*ast.Ident) 286 if !ok { 287 return nil, fmt.Errorf("Expected client field with type '%s' in '%s' "+ 288 "to be proceeded by an *ast.Ident", typeNameClient, typeNameAPI) 289 } 290 291 packageName := ident.Name 292 packagesInClientAPI[packageName] = struct{}{} 293 } 294 295 for alias, packageName := range importAliases { 296 _, ok := packagesInClientAPI[alias] 297 if !ok { 298 continue 299 } 300 301 delete(packagesInClientAPI, alias) 302 packagesInClientAPI[packageName] = struct{}{} 303 } 304 305 return packagesInClientAPI, nil 306 }