├── LICENSE ├── README.md ├── cmd ├── example │ ├── main.go │ └── schema.sql └── generator │ └── main.go ├── domain └── product.go ├── go.mod ├── go.sum └── repository ├── mapping.go ├── mapping_product_gen.go └── repository_product.go /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2020 Christopher Hlubek (networkteam GmbH) 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Metaprogramming with Go - or how to build code generators that parse Go code 2 | 3 | The ideal Go code is optimized for legibility (easy to read, follow and understand). It favors explicitness over cleverness and tricks to save some lines of code. I think that's one of my favorite "features" of the Go language / mindset. It allows me to jump straight into the code of many popular projects. While something like the Kubernetes codebase is not exactly small and can be quite intimidating to get started with, it's nonetheless mostly functions calling other functions. Any editor offering code assistance for Go makes it easy to just follow calls and understand the flow of logic inside a program. 4 | 5 | So while Go certainly has some nice features, it's also pretty minimal (by design). There are no generics (yet), but that will change soon with the 1.18 release. But even with generics all the boilerplate (rather boring code that sets things up or converts between different layers) can be removed. 6 | 7 | ## Example: abstractions for database access 8 | 9 | We want to create an abstraction of `Product` database records as entities in a repository. 10 | 11 | First the domain model as the entity: 12 | 13 | ```go 14 | type Product struct { 15 | ID uuid.UUID 16 | ArticleNumber string 17 | Name string 18 | Description string 19 | Color string 20 | Size string 21 | StockAvailability int 22 | PriceCents int 23 | OnSale bool 24 | } 25 | ``` 26 | 27 | A repository can be just a bunch of functions operating on a database (or transaction) and this type. I like to use [squirrel](https://github.com/Masterminds/squirrel) because it's not an ORM but just helps to write SQL and reduce boilerplate: 28 | 29 | ```go 30 | func InsertProduct(ctx context.Context, runner squirrel.BaseRunner, product domain.Product) error { 31 | _, err := squirrel.Insert("products"). 32 | SetMap(map[string]interface{}{ 33 | "product_id": product.ID, 34 | "article_number": product.ArticleNumber, 35 | "name": product.Name, 36 | "description": product.Description, 37 | "color": product.Color, 38 | "size": product.Size, 39 | "stock_availability": product.StockAvailability, 40 | "price_cents": product.PriceCents, 41 | "on_sale": product.OnSale, 42 | }). 43 | ExecContext(ctx) 44 | return err 45 | } 46 | ``` 47 | 48 | For updating we implement a `ChangeSet` type with optional (nillable) fields to select which columns to set or update: 49 | 50 | ```go 51 | type ProductChangeSet struct { 52 | ArticleNumber *string 53 | Name *string 54 | Description *string 55 | Color *string 56 | Size *string 57 | StockAvailability *int 58 | PriceCents *int 59 | OnSale *bool 60 | } 61 | 62 | func UpdateProduct(ctx context.Context, runner squirrel.BaseRunner, id uuid.UUID, changeSet ProductChangeSet) error { 63 | res, err := squirrel. 64 | Update("gamers"). 65 | Where(squirrel.Eq{"gamer_id": id}). 66 | SetMap(changeSet.toMap()). 67 | RunWith(runner). 68 | ExecContext(ctx) 69 | if err != nil { 70 | return fmt.Errorf("executing update: %w", err) 71 | } 72 | rowsAffected, err := res.RowsAffected() 73 | if err != nil { 74 | return fmt.Errorf("getting affected rows: %w", err) 75 | } 76 | if rowsAffected != 1 { 77 | return fmt.Errorf("update affected %d rows, but expected exactly 1", rowsAffected) 78 | } 79 | return err 80 | } 81 | ``` 82 | 83 | To get the map of fields that actually changed, we implement a `toMap` method on our `ProductChangeSet` type: 84 | 85 | ```go 86 | func (c ProductChangeSet) toMap() map[string]interface{} { 87 | m := make(map[string]interface{}) 88 | if c.ArticleNumber != nil { 89 | m["article_number"] = c.ArticleNumber 90 | } 91 | if c.Name != nil { 92 | m["name"] = c.Name 93 | } 94 | if c.Description != nil { 95 | m["description"] = c.Description 96 | } 97 | if c.Color != nil { 98 | m["color"] = c.Color 99 | } 100 | if c.Size != nil { 101 | m["size"] = c.Size 102 | } 103 | if c.StockAvailability != nil { 104 | m["stock_availability"] = c.StockAvailability 105 | } 106 | if c.PriceCents != nil { 107 | m["price_cents"] = c.PriceCents 108 | } 109 | if c.OnSale != nil { 110 | m["on_sale"] = c.OnSale 111 | } 112 | return m 113 | } 114 | ``` 115 | 116 | So while now everything should be nicely typed for a consumer of the repository we ended up with quite some boilerplate code. Most fields are referenced at 4 different places and `ProductChangeSet` is almost a direct copy of `Product` - just with pointers to the original types for optional values. The column names are referenced at 2 different places, and we haven't even yet implemented sorting or filtering. This makes adding new fields / columns a little too cumbersome and error-prone. 117 | 118 | So can we do better without adding complexity by using an object relational mapper (ORM)? 119 | 120 | ## Using reflection for metaprogramming 121 | 122 | Metaprogramming in its essence is about programming a program (haha, sooo meta isn't it). One of the ways is to add some additional metadata into struct tags, so there's only one source of truth: 123 | 124 | ```go 125 | type Product struct { 126 | ID uuid.UUID `col:"product_id"` 127 | ArticleNumber string `col:"article_number"` 128 | Name string `col:"name"` 129 | Description string `col:"description"` 130 | Color string `col:"color"` 131 | Size string `col:"size"` 132 | StockAvailability int `col:"stock_availability"` 133 | PriceCents int `col:"price_cents"` 134 | OnSale bool `col:"on_sale"` 135 | } 136 | ``` 137 | 138 | We could now leverage reflection, which is built-in into Go, and access those struct tags in at runtime: 139 | 140 | ```go 141 | func InsertProductReflect(ctx context.Context, runner squirrel.BaseRunner, product domain.Product) error { 142 | m := make(map[string]interface{}) 143 | t := reflect.TypeOf(product) 144 | for i:=0; i< t.NumField();i++ { 145 | field := t.Field(i) 146 | col := field.Tag.Get("col") 147 | if col != "" { 148 | m[col] = reflect.ValueOf(product).Field(i).Interface() 149 | } 150 | } 151 | 152 | _, err := squirrel.Insert("products"). 153 | SetMap(m). 154 | RunWith(runner). 155 | ExecContext(ctx) 156 | return err 157 | } 158 | ``` 159 | 160 | Note that this gets rid of some references to the column and field names, but it's now also quite unreadable and hard to follow. Well, we could extract that to a function that can be re-used across repository functions. But we now loose all information about field usage when going through the code. So we only made a trade-off between legibility and reducing repetition in the code. 161 | 162 | And: we cannot get rid of our `ProductChangeSet` definition that copies many fields of `Product` unless we get rid of static typing all fields and just accept a map of `interface{}` values. 163 | 164 | ## Metaprogramming for real: writing code that generates code 165 | 166 | So how can we go beyond pure Go without sacrificing the advantages of explicitness and code which is easy to follow? 167 | 168 | In the Go toolstack there's a built-in command for generating code called `go generate`. It can be used to process special `//go:generate my-command args` comments in .go files and will invoke the specified command to generate code (which is up to the command). This way it's easy and uniform to include generated code into a project and it's quite common to do so in the Go world (maybe a consequence of the lack of generics in a way). 169 | 170 | So before jumping into code again, let's think about the necessary steps to write the boilerplate code with a code generator: 171 | 172 | 1. Add metadata by using struct tags (✅) 173 | 2. Implement a code generator that parses our Go code, extracts type information with struct tags and generates a new type and methods (🤔) 174 | 175 | Well, luckily the Go toolchain includes a tokenizer and parser for Go code. There's not too much information out there, but you can find some nice articles (like https://arslan.io/2017/09/14/the-ultimate-guide-to-writing-a-go-tool/) about using the parser and working with the Go AST. 176 | 177 | When first sketching a generator for this exact problem, I just used the Go AST to extract struct tags, which works quite well after fiddling with the data structure. This is a visualization of the AST using http://goast.yuroyoro.net/: 178 | 179 | ![Go AST visualization](https://dev-to-uploads.s3.amazonaws.com/i/lxpjlv0d2dwsppw8ldaa.png) 180 | 181 | So you can see it's much more low-level than using `reflect` and closely resembles the structure of the Go language. By using `ast.Inspect(node, func(n ast.Node) bool { ... }` you can visit the nodes and check if it matches a top-level declaration and then inspect all the fields with tags etc. 182 | 183 | Just turns out it's pretty hard to get type information for field declarations in a reliable way. This is, because in the end a definition like `Color string` is just two identifiers `*ast.Ident`. For built-ins this is still easy. But what about `uuid.UUID` (which is a `*ast.SelectorExpr`)? How to get the full import path to write it correctly in the generated code and what if we used anonymous imports or are referring to types in the same or some other package? 184 | 185 | Turns out this is a rather hard problem and luckily (again) there's a type checker package that's doing all the hard work called [go/types](https://go.googlesource.com/example/+/HEAD/gotypes). Too bad, it's not working with modules to resolve packages. But we don't have to worry, since there's [golang.org/x/tools/go/packages](https://godoc.org/golang.org/x/tools/go/packages) which does all that. 186 | 187 | ## Code Generator Part 1 188 | 189 | Again, there's not too much information out there on how to get started with this, so here's how a first attempt at the code generator could look like (without actually generating code): 190 | 191 | ```go 192 | package main 193 | 194 | import ( 195 | "fmt" 196 | "go/types" 197 | "os" 198 | "strings" 199 | 200 | "golang.org/x/tools/go/packages" 201 | ) 202 | 203 | func main() { 204 | // 1. Handle arguments to command 205 | if len(os.Args) != 2 { 206 | failErr(fmt.Errorf("expected exactly one argument: ")) 207 | } 208 | sourceType := os.Args[1] 209 | sourceTypePackage, sourceTypeName := splitSourceType(sourceType) 210 | 211 | // 2. Inspect package and use type checker to infer imported types 212 | pkg := loadPackage(sourceTypePackage) 213 | 214 | // 3. Lookup the given source type name in the package declarations 215 | obj := pkg.Types.Scope().Lookup(sourceTypeName) 216 | if obj == nil { 217 | failErr(fmt.Errorf("%s not found in declared types of %s", 218 | sourceTypeName, pkg)) 219 | } 220 | 221 | // 4. We check if it is a declared type 222 | if _, ok := obj.(*types.TypeName); !ok { 223 | failErr(fmt.Errorf("%v is not a named type", obj)) 224 | } 225 | // 5. We expect the underlying type to be a struct 226 | structType, ok := obj.Type().Underlying().(*types.Struct) 227 | if !ok { 228 | failErr(fmt.Errorf("type %v is not a struct", obj)) 229 | } 230 | 231 | // 6. Now we can iterate through fields and access tags 232 | for i := 0; i < structType.NumFields(); i++ { 233 | field := structType.Field(i) 234 | tagValue := structType.Tag(i) 235 | fmt.Println(field.Name(), tagValue, field.Type()) 236 | } 237 | } 238 | 239 | func loadPackage(path string) *packages.Package { 240 | cfg := &packages.Config{Mode: packages.NeedTypes | packages.NeedImports} 241 | pkgs, err := packages.Load(cfg, path) 242 | if err != nil { 243 | failErr(fmt.Errorf("loading packages for inspection: %v", err)) 244 | } 245 | if packages.PrintErrors(pkgs) > 0 { 246 | os.Exit(1) 247 | } 248 | 249 | return pkgs[0] 250 | } 251 | 252 | func splitSourceType(sourceType string) (string, string) { 253 | idx := strings.LastIndexByte(sourceType, '.') 254 | if idx == -1 { 255 | failErr(fmt.Errorf(`expected qualified type as "pkg/path.MyType"`)) 256 | } 257 | sourceTypePackage := sourceType[0:idx] 258 | sourceTypeName := sourceType[idx+1:] 259 | return sourceTypePackage, sourceTypeName 260 | } 261 | 262 | func failErr(err error) { 263 | if err != nil { 264 | fmt.Fprintf(os.Stderr, "Error: %v\n", err) 265 | os.Exit(1) 266 | } 267 | } 268 | ``` 269 | 270 | So we now end up with some more code as in the version using `reflect`. What this program does is basically these steps: 271 | 272 | 1. Handle arguments 273 | 2. Load type information via `packages` 274 | 3. Make a lookup into the types of the package scope 275 | 4. Check if the type exists... 276 | 5. ...and is a struct 277 | 6. Iterate through struct fields and tags with declared type 278 | 279 | How to run this generator? 280 | 281 | Let's add a mostly empty `mapping.go` file in the `repository` package: 282 | 283 | ```go 284 | //go:generate go run github.com/hlubek/metaprogramming-go/cmd/generator github.com/hlubek/metaprogramming-go/domain.Product 285 | package repository 286 | ``` 287 | 288 | This is a stub for the above-mentioned `go generate` command. In a project set up with Go modules, you can now run `go generate ./...` in the module root and the generator will be called: 289 | 290 | ``` 291 | $ go generate ./... 292 | ID col:"product_id" github.com/gofrs/uuid.UUID 293 | ArticleNumber col:"article_number" string 294 | Name col:"name" string 295 | Description col:"description" string 296 | Color col:"color" string 297 | Size col:"size" string 298 | StockAvailability col:"stock_availability" int 299 | PriceCents col:"price_cents" int 300 | OnSale col:"on_sale" bool 301 | ``` 302 | 303 | Note that tags are not yet parsed, so we have to deal with this ourselves. But types are now fully qualified, so we can use that information to generate a `ProductChangeSet` struct type. 304 | 305 | ## How to generate code? 306 | 307 | So how can we now generate the needed Go code? Of course, you could use string manipulation or use the Go `text/template` package (many tools actually do this). 308 | 309 | I recently saw [jennifer](https://github.com/dave/jennifer), a nice tool that has a Go API to generate Go code and includes proper handling of qualified types, formatting and some more. 310 | 311 | Turns out it's quite easy to use the information we now have at our hands and use `jennifer` to generate the code we previously coded by hand. 312 | 313 | ## Code Generator Part 2 314 | 315 | ```go 316 | package main 317 | 318 | import ( 319 | "fmt" 320 | "go/types" 321 | "os" 322 | "path/filepath" 323 | "strings" 324 | 325 | . "github.com/dave/jennifer/jen" 326 | "golang.org/x/tools/go/packages" 327 | ) 328 | 329 | func main() { 330 | // ... 331 | 332 | // Generate code using jennifer 333 | err := generate(sourceTypeName, structType) 334 | if err != nil { 335 | failErr(err) 336 | } 337 | } 338 | 339 | func generate(sourceTypeName string, structType *types.Struct) error { 340 | 341 | // 1. Get the package of the file with go:generate comment 342 | goPackage := os.Getenv("GOPACKAGE") 343 | 344 | // 2. Start a new file in this package 345 | f := NewFile(goPackage) 346 | 347 | // 3. Add a package comment, so IDEs detect files as generated 348 | f.PackageComment("Code generated by generator, DO NOT EDIT.") 349 | 350 | var ( 351 | changeSetFields []Code 352 | ) 353 | 354 | // 4. Iterate over struct fields 355 | for i := 0; i < structType.NumFields(); i++ { 356 | field := structType.Field(i) 357 | 358 | // Generate code for each changeset field 359 | code := Id(field.Name()) 360 | switch v := field.Type().(type) { 361 | case *types.Basic: 362 | code.Op("*").Id(v.String()) 363 | case *types.Named: 364 | typeName := v.Obj() 365 | // Qual automatically imports packages 366 | code.Op("*").Qual( 367 | typeName.Pkg().Path(), 368 | typeName.Name(), 369 | ) 370 | default: 371 | return fmt.Errorf("struct field type not hanled: %T", v) 372 | } 373 | changeSetFields = append(changeSetFields, code) 374 | } 375 | 376 | // 5. Generate changeset type 377 | changeSetName := sourceTypeName + "ChangeSet" 378 | f.Type().Id(changeSetName).Struct(changeSetFields...) 379 | 380 | // 6. Build the target file name 381 | goFile := os.Getenv("GOFILE") 382 | ext := filepath.Ext(goFile) 383 | baseFilename := goFile[0 : len(goFile)-len(ext)] 384 | targetFilename := baseFilename + "_" + strings.ToLower(sourceTypeName) + "_gen.go" 385 | 386 | // 7. Write generated file 387 | return f.Save(targetFilename) 388 | } 389 | 390 | // ... 391 | 392 | ``` 393 | 394 | That's actually not too much new code here! All code is generated by using functions from the `jen` package and finally writing to a filename we derive from the filename where the `go:generate` comment was declared. 395 | 396 | So, what's the output if we run the command again? 397 | 398 | ``` 399 | go generate ./... 400 | cat repository/mapping_product_gen.go 401 | ``` 402 | 403 | ```go 404 | // Code generated by generator, DO NOT EDIT. 405 | package repository 406 | 407 | import uuid "github.com/gofrs/uuid" 408 | 409 | type ProductChangeSet struct { 410 | ID *uuid.UUID 411 | ArticleNumber *string 412 | Name *string 413 | Description *string 414 | Color *string 415 | Size *string 416 | StockAvailability *int 417 | PriceCents *int 418 | OnSale *bool 419 | } 420 | ``` 421 | 422 | Looks pretty nice so far. Now let's generate the `toMap()` method that returns a map for all the changes: 423 | 424 | 425 | ```go 426 | import ( 427 | // ... 428 | "regexp" 429 | // ... 430 | ) 431 | 432 | // ... 433 | 434 | 435 | // Use a simple regexp pattern to match tag values 436 | var structColPattern = regexp.MustCompile(`col:"([^"]+)"`) 437 | 438 | func generate(sourceTypeName string, structType *types.Struct) error { 439 | // ... 440 | 441 | // 1. Collect code in toMap() block 442 | var toMapBlock []Code 443 | 444 | // 2. Build "m := make(map[string]interface{})" 445 | toMapBlock = append(toMapBlock, Id("m").Op(":=").Make(Map(String()).Interface())) 446 | 447 | for i := 0; i < structType.NumFields(); i++ { 448 | field := structType.Field(i) 449 | tagValue := structType.Tag(i) 450 | 451 | matches := structColPattern.FindStringSubmatch(tagValue) 452 | if matches == nil { 453 | continue 454 | } 455 | col := matches[1] 456 | 457 | // 3. Build "if c.Field != nil { m["col"] = *c.Field }" 458 | code := If(Id("c").Dot(field.Name()).Op("!=").Nil()).Block( 459 | Id("m").Index(Lit(col)).Op("=").Op("*").Id("c").Dot(field.Name()), 460 | ) 461 | toMapBlock = append(toMapBlock, code) 462 | } 463 | 464 | // 4. Build return statement 465 | toMapBlock = append(toMapBlock, Return(Id("m"))) 466 | 467 | // 5. Build toMap method 468 | f.Func().Params( 469 | Id("c").Id(changeSetName), 470 | ).Id("toMap").Params().Map(String()).Interface().Block( 471 | toMapBlock..., 472 | ) 473 | 474 | // ... 475 | } 476 | 477 | // ... 478 | 479 | ``` 480 | 481 | And run the generator again: 482 | 483 | 484 | ``` 485 | go generate ./... 486 | cat repository/mapping_product_gen.go 487 | ``` 488 | 489 | ```go 490 | // Code generated by generator, DO NOT EDIT. 491 | package repository 492 | 493 | import uuid "github.com/gofrs/uuid" 494 | 495 | type ProductChangeSet struct { 496 | ID *uuid.UUID 497 | ArticleNumber *string 498 | Name *string 499 | Description *string 500 | Color *string 501 | Size *string 502 | StockAvailability *int 503 | PriceCents *int 504 | OnSale *bool 505 | } 506 | 507 | func (c ProductChangeSet) toMap() map[string]interface{} { 508 | m := make(map[string]interface{}) 509 | if c.ID != nil { 510 | m["product_id"] = *c.ID 511 | } 512 | if c.ArticleNumber != nil { 513 | m["article_number"] = *c.ArticleNumber 514 | } 515 | if c.Name != nil { 516 | m["name"] = *c.Name 517 | } 518 | if c.Description != nil { 519 | m["description"] = *c.Description 520 | } 521 | if c.Color != nil { 522 | m["color"] = *c.Color 523 | } 524 | if c.Size != nil { 525 | m["size"] = *c.Size 526 | } 527 | if c.StockAvailability != nil { 528 | m["stock_availability"] = *c.StockAvailability 529 | } 530 | if c.PriceCents != nil { 531 | m["price_cents"] = *c.PriceCents 532 | } 533 | if c.OnSale != nil { 534 | m["on_sale"] = *c.OnSale 535 | } 536 | return m 537 | } 538 | ``` 539 | 540 | This looks exactly like the code we previously wrote by hand! And the best thing: it still is readable and very explicit when inspecting what the code does. Only the generator itself is a little harder to follow and understand. But stepping through the generated code should be a breeze and not different from hand rolled code by any means. 541 | 542 | ## Conclusion 543 | 544 | Writing code generators that uses existing Go code for metadata is not as hard as it might sound first. It's a viable solution to reduce boilerplate in many situations. When choosing the right tools the hard work of analyzing code and types as well as generating readable Go code is already done. 545 | 546 | The code of this article can be found at https://github.com/hlubek/metaprogramming-go and is free to use under a MIT license. 547 | 548 | Check out the example program that can be run with `go run ./cmd/example` and sets up a fully working example application accessing a PostgreSQL database based on the generated code (it uses an embedded database, so you don't need to have Postgres running already). 549 | 550 | ## Additional Resources 551 | 552 | - [github.com/networkteam/construct](https://github.com/networkteam/construct/) - An opinionated set of generated structs and functions for building low abstraction persistence (e.g. SQL) - built on top of the ideas presented in this article. 553 | -------------------------------------------------------------------------------- /cmd/example/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "database/sql" 7 | _ "embed" 8 | "fmt" 9 | "log" 10 | 11 | embeddedpostgres "github.com/fergusstrange/embedded-postgres" 12 | "github.com/gofrs/uuid" 13 | _ "github.com/lib/pq" 14 | 15 | "github.com/hlubek/metaprogramming-go/domain" 16 | "github.com/hlubek/metaprogramming-go/repository" 17 | ) 18 | 19 | func main() { 20 | ctx := context.Background() 21 | 22 | err := run(ctx) 23 | if err != nil { 24 | log.Fatalf("Error: %v", err) 25 | } 26 | } 27 | 28 | //go:embed schema.sql 29 | var schema []byte 30 | 31 | func run(ctx context.Context) error { 32 | // Start an embedded PostgreSQL server 33 | 34 | var postgresLog bytes.Buffer 35 | 36 | postgres := embeddedpostgres.NewDatabase( 37 | embeddedpostgres. 38 | DefaultConfig(). 39 | Version(embeddedpostgres.V13). 40 | Port(54329). 41 | Database("example"). 42 | Logger(&postgresLog), 43 | ) 44 | log.Printf("Starting embedded PostgreSQL server...") 45 | err := postgres.Start() 46 | if err != nil { 47 | return fmt.Errorf("starting embedded PostgreSQL server: %w", err) 48 | } 49 | 50 | defer func() { 51 | log.Printf("Stopping embedded PostgreSQL server") 52 | err = postgres.Stop() 53 | if err != nil { 54 | log.Printf("Failed to stop embedded PostgreSQL server: %v", err) 55 | } 56 | }() 57 | 58 | // Connect to the database 59 | 60 | connStr := "user=postgres password=postgres dbname=example host=localhost port=54329 sslmode=disable" 61 | db, err := sql.Open("postgres", connStr) 62 | if err != nil { 63 | return fmt.Errorf("opening database connection: %w", err) 64 | } 65 | 66 | _, err = db.Exec(string(schema)) 67 | if err != nil { 68 | return fmt.Errorf("executing schema: %w", err) 69 | } 70 | 71 | // Run some example repository operations: 72 | 73 | // Insert a product: 74 | 75 | productID := uuid.Must(uuid.FromString("b34081c7-9f33-4b04-ba33-3a112199f8c2")) 76 | 77 | err = repository.InsertProduct(ctx, db, domain.Product{ 78 | ID: productID, 79 | }) 80 | if err != nil { 81 | return fmt.Errorf("inserting product: %w", err) 82 | } 83 | log.Printf("Inserted product %s", productID) 84 | 85 | // Update an existing product with the generated change set: 86 | 87 | err = repository.UpdateProduct(ctx, db, productID, repository.ProductChangeSet{ 88 | ArticleNumber: stringPtr("12345678"), 89 | Name: stringPtr("Cheddar cheese"), 90 | }) 91 | if err != nil { 92 | return fmt.Errorf("updating product: %w", err) 93 | } 94 | log.Printf("Updated product %s", productID) 95 | 96 | return nil 97 | } 98 | 99 | func stringPtr(s string) *string { 100 | return &s 101 | } 102 | -------------------------------------------------------------------------------- /cmd/example/schema.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE IF NOT EXISTS products 2 | ( 3 | product_id uuid PRIMARY KEY, 4 | article_number text, 5 | name text, 6 | description text, 7 | color text, 8 | size text, 9 | stock_availability int, 10 | price_cents int, 11 | on_sale bool 12 | ); 13 | -------------------------------------------------------------------------------- /cmd/generator/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "go/types" 6 | "os" 7 | "path/filepath" 8 | "regexp" 9 | "strings" 10 | 11 | . "github.com/dave/jennifer/jen" 12 | "golang.org/x/tools/go/packages" 13 | ) 14 | 15 | func main() { 16 | // Handle arguments to command 17 | if len(os.Args) != 2 { 18 | failErr(fmt.Errorf("expected exactly one argument: ")) 19 | } 20 | sourceType := os.Args[1] 21 | sourceTypePackage, sourceTypeName := splitSourceType(sourceType) 22 | 23 | // Inspect package and use type checker to infer imported types 24 | pkg := loadPackage(sourceTypePackage) 25 | 26 | // Lookup the given source type name in the package declarations 27 | obj := pkg.Types.Scope().Lookup(sourceTypeName) 28 | if obj == nil { 29 | failErr(fmt.Errorf("%s not found in declared types of %s", 30 | sourceTypeName, pkg)) 31 | } 32 | 33 | // We check if it is a declared type 34 | if _, ok := obj.(*types.TypeName); !ok { 35 | failErr(fmt.Errorf("%v is not a named type", obj)) 36 | } 37 | // We expect the underlying type to be a struct 38 | structType, ok := obj.Type().Underlying().(*types.Struct) 39 | if !ok { 40 | failErr(fmt.Errorf("type %v is not a struct", obj)) 41 | } 42 | 43 | // Generate code using jennifer 44 | err := generate(sourceTypeName, structType) 45 | if err != nil { 46 | failErr(err) 47 | } 48 | } 49 | 50 | // Use a simple regexp pattern to match tag values 51 | var structColPattern = regexp.MustCompile(`col:"([^"]+)"`) 52 | 53 | func generate(sourceTypeName string, structType *types.Struct) error { 54 | 55 | // Get the package of the file with go:generate comment 56 | goPackage := os.Getenv("GOPACKAGE") 57 | 58 | // Start a new file in this package 59 | f := NewFile(goPackage) 60 | 61 | // Add a package comment, so IDEs detect files as generated 62 | f.PackageComment("Code generated by generator, DO NOT EDIT.") 63 | 64 | var ( 65 | changeSetFields []Code 66 | ) 67 | 68 | // Iterate over struct fields 69 | for i := 0; i < structType.NumFields(); i++ { 70 | field := structType.Field(i) 71 | 72 | // Generate code for each changeset field 73 | code := Id(field.Name()) 74 | switch v := field.Type().(type) { 75 | case *types.Basic: 76 | code.Op("*").Id(v.String()) 77 | case *types.Named: 78 | typeName := v.Obj() 79 | // Qual automatically imports packages 80 | code.Op("*").Qual( 81 | typeName.Pkg().Path(), 82 | typeName.Name(), 83 | ) 84 | default: 85 | return fmt.Errorf("struct field type not hanled: %T", v) 86 | } 87 | changeSetFields = append(changeSetFields, code) 88 | } 89 | 90 | // Generate change set type 91 | changeSetName := sourceTypeName + "ChangeSet" 92 | f.Type().Id(changeSetName).Struct(changeSetFields...) 93 | 94 | // 1. Collect code in toMap() block 95 | var toMapBlock []Code 96 | 97 | // 2. Build "m := make(map[string]interface{})" 98 | toMapBlock = append(toMapBlock, Id("m").Op(":=").Make(Map(String()).Interface())) 99 | 100 | for i := 0; i < structType.NumFields(); i++ { 101 | field := structType.Field(i) 102 | tagValue := structType.Tag(i) 103 | 104 | matches := structColPattern.FindStringSubmatch(tagValue) 105 | if matches == nil { 106 | continue 107 | } 108 | col := matches[1] 109 | 110 | // 3. Build "if c.Field != nil { m["col"] = *c.Field }" 111 | code := If(Id("c").Dot(field.Name()).Op("!=").Nil()).Block( 112 | Id("m").Index(Lit(col)).Op("=").Op("*").Id("c").Dot(field.Name()), 113 | ) 114 | toMapBlock = append(toMapBlock, code) 115 | } 116 | 117 | // 4. Build return statement 118 | toMapBlock = append(toMapBlock, Return(Id("m"))) 119 | 120 | // 5. Build toMap method 121 | f.Func().Params( 122 | Id("c").Id(changeSetName), 123 | ).Id("toMap").Params().Map(String()).Interface().Block( 124 | toMapBlock..., 125 | ) 126 | 127 | // Build the target file name 128 | goFile := os.Getenv("GOFILE") 129 | ext := filepath.Ext(goFile) 130 | baseFilename := goFile[0 : len(goFile)-len(ext)] 131 | targetFilename := baseFilename + "_" + strings.ToLower(sourceTypeName) + "_gen.go" 132 | 133 | // Write generated file 134 | return f.Save(targetFilename) 135 | } 136 | 137 | func loadPackage(path string) *packages.Package { 138 | cfg := &packages.Config{Mode: packages.NeedTypes | packages.NeedImports} 139 | pkgs, err := packages.Load(cfg, path) 140 | if err != nil { 141 | failErr(fmt.Errorf("loading packages for inspection: %v", err)) 142 | } 143 | if packages.PrintErrors(pkgs) > 0 { 144 | os.Exit(1) 145 | } 146 | 147 | return pkgs[0] 148 | } 149 | 150 | func splitSourceType(sourceType string) (string, string) { 151 | idx := strings.LastIndexByte(sourceType, '.') 152 | if idx == -1 { 153 | failErr(fmt.Errorf(`expected qualified type as "pkg/path.MyType"`)) 154 | } 155 | sourceTypePackage := sourceType[0:idx] 156 | sourceTypeName := sourceType[idx+1:] 157 | return sourceTypePackage, sourceTypeName 158 | } 159 | 160 | func failErr(err error) { 161 | if err != nil { 162 | fmt.Fprintf(os.Stderr, "Error: %v\n", err) 163 | os.Exit(1) 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /domain/product.go: -------------------------------------------------------------------------------- 1 | package domain 2 | 3 | import "github.com/gofrs/uuid" 4 | 5 | type Product struct { 6 | ID uuid.UUID `col:"product_id"` 7 | ArticleNumber string `col:"article_number"` 8 | Name string `col:"name"` 9 | Description string `col:"description"` 10 | Color string `col:"color"` 11 | Size string `col:"size"` 12 | StockAvailability int `col:"stock_availability"` 13 | PriceCents int `col:"price_cents"` 14 | OnSale bool `col:"on_sale"` 15 | } 16 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/hlubek/metaprogramming-go 2 | 3 | go 1.17 4 | 5 | require ( 6 | github.com/Masterminds/squirrel v1.5.2 7 | github.com/dave/jennifer v1.5.0 8 | github.com/fergusstrange/embedded-postgres v1.15.0 9 | github.com/gofrs/uuid v4.2.0+incompatible 10 | github.com/lib/pq v1.10.4 11 | golang.org/x/tools v0.1.9 12 | ) 13 | 14 | require ( 15 | github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect 16 | github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect 17 | github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect 18 | golang.org/x/mod v0.5.1 // indirect 19 | golang.org/x/sys v0.0.0-20220307203707-22a9840ba4d7 // indirect 20 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect 21 | ) 22 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/Masterminds/squirrel v1.5.2 h1:UiOEi2ZX4RCSkpiNDQN5kro/XIBpSRk9iTqdIRPzUXE= 2 | github.com/Masterminds/squirrel v1.5.2/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA400rg+riTZj10= 3 | github.com/dave/astrid v0.0.0-20170323122508-8c2895878b14/go.mod h1:Sth2QfxfATb/nW4EsrSi2KyJmbcniZ8TgTaji17D6ms= 4 | github.com/dave/brenda v1.1.0/go.mod h1:4wCUr6gSlu5/1Tk7akE5X7UorwiQ8Rij0SKH3/BGMOM= 5 | github.com/dave/courtney v0.3.0/go.mod h1:BAv3hA06AYfNUjfjQr+5gc6vxeBVOupLqrColj+QSD8= 6 | github.com/dave/gopackages v0.0.0-20170318123100-46e7023ec56e/go.mod h1:i00+b/gKdIDIxuLDFob7ustLAVqhsZRk2qVZrArELGQ= 7 | github.com/dave/jennifer v1.5.0 h1:HmgPN93bVDpkQyYbqhCHj5QlgvUkvEOzMyEvKLgCRrg= 8 | github.com/dave/jennifer v1.5.0/go.mod h1:4MnyiFIlZS3l5tSDn8VnzE6ffAhYBMB2SZntBsZGUok= 9 | github.com/dave/kerr v0.0.0-20170318121727-bc25dd6abe8e/go.mod h1:qZqlPyPvfsDJt+3wHJ1EvSXDuVjFTK0j2p/ca+gtsb8= 10 | github.com/dave/patsy v0.0.0-20210517141501-957256f50cba/go.mod h1:qfR88CgEGLoiqDaE+xxDCi5QA5v4vUoW0UCX2Nd5Tlc= 11 | github.com/dave/rebecca v0.9.1/go.mod h1:N6XYdMD/OKw3lkF3ywh8Z6wPGuwNFDNtWYEMFWEmXBA= 12 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 13 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 14 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 15 | github.com/fergusstrange/embedded-postgres v1.15.0 h1:9H5c8Xzp+8nXuEpXKkUPpSyhj7PNZ2QmO/FIus8VYp0= 16 | github.com/fergusstrange/embedded-postgres v1.15.0/go.mod h1:VqymgzpNsdJspJeISq4+jpqWL1FOrqIzt9o78W5ekkw= 17 | github.com/gofrs/uuid v4.2.0+incompatible h1:yyYWMnhkhrKwwr8gAOcOCYxOOscHgDS9yZgBrnJfGa0= 18 | github.com/gofrs/uuid v4.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= 19 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 20 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 21 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 22 | github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 h1:SOEGU9fKiNWd/HOJuq6+3iTQz8KNCLtVX6idSoTLdUw= 23 | github.com/lann/builder v0.0.0-20180802200727-47ae307949d0/go.mod h1:dXGbAdH5GtBTC4WfIxhKZfyBF/HBFgRZSWwZ9g/He9o= 24 | github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 h1:P6pPBnrTSX3DEVR4fDembhRWSsG5rVo6hYhAB/ADZrk= 25 | github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0/go.mod h1:vmVJ0l/dxyfGW6FmdpVm2joNMFikkuWg0EoCKLGUMNw= 26 | github.com/lib/pq v1.8.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= 27 | github.com/lib/pq v1.10.4 h1:SO9z7FRPzA03QhHKJrH5BXA6HU1rS4V2nIVrrNC1iYk= 28 | github.com/lib/pq v1.10.4/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= 29 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 30 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 31 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 32 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 33 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 34 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= 35 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 36 | github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 h1:nIPpBwaJSVYIxUFsDv3M8ofmx9yWTog9BfvIu0q41lo= 37 | github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMxjDjgmT5uz5wzYJKVo23qUhYTos= 38 | github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= 39 | github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= 40 | go.uber.org/goleak v1.1.12 h1:gZAh5/EyT/HQwlpkCy6wTpqfH9H8Lz8zbm3dZh+OyzA= 41 | go.uber.org/goleak v1.1.12/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= 42 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 43 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 44 | golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 45 | golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 46 | golang.org/x/mod v0.5.1 h1:OJxoQ/rynoF0dcCdI7cLPktw/hR2cueqYfjm43oqK38= 47 | golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= 48 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 49 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 50 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 51 | golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= 52 | golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 53 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 54 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 55 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 56 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 57 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 58 | golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 59 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 60 | golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 61 | golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 62 | golang.org/x/sys v0.0.0-20220307203707-22a9840ba4d7 h1:8IVLkfbr2cLhv0a/vKq4UFUcJym8RmDoDboxCFWEjYE= 63 | golang.org/x/sys v0.0.0-20220307203707-22a9840ba4d7/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 64 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 65 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 66 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 67 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 68 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 69 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 70 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 71 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 72 | golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= 73 | golang.org/x/tools v0.1.8/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU= 74 | golang.org/x/tools v0.1.9 h1:j9KsMiaP1c3B0OTQGth0/k+miLGTgLsAFUCrF2vLcF8= 75 | golang.org/x/tools v0.1.9/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU= 76 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 77 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 78 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= 79 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 80 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 81 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 82 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= 83 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 84 | -------------------------------------------------------------------------------- /repository/mapping.go: -------------------------------------------------------------------------------- 1 | //go:generate go run github.com/hlubek/metaprogramming-go/cmd/generator github.com/hlubek/metaprogramming-go/domain.Product 2 | package repository 3 | -------------------------------------------------------------------------------- /repository/mapping_product_gen.go: -------------------------------------------------------------------------------- 1 | // Code generated by generator, DO NOT EDIT. 2 | package repository 3 | 4 | import uuid "github.com/gofrs/uuid" 5 | 6 | type ProductChangeSet struct { 7 | ID *uuid.UUID 8 | ArticleNumber *string 9 | Name *string 10 | Description *string 11 | Color *string 12 | Size *string 13 | StockAvailability *int 14 | PriceCents *int 15 | OnSale *bool 16 | } 17 | 18 | func (c ProductChangeSet) toMap() map[string]interface{} { 19 | m := make(map[string]interface{}) 20 | if c.ID != nil { 21 | m["product_id"] = *c.ID 22 | } 23 | if c.ArticleNumber != nil { 24 | m["article_number"] = *c.ArticleNumber 25 | } 26 | if c.Name != nil { 27 | m["name"] = *c.Name 28 | } 29 | if c.Description != nil { 30 | m["description"] = *c.Description 31 | } 32 | if c.Color != nil { 33 | m["color"] = *c.Color 34 | } 35 | if c.Size != nil { 36 | m["size"] = *c.Size 37 | } 38 | if c.StockAvailability != nil { 39 | m["stock_availability"] = *c.StockAvailability 40 | } 41 | if c.PriceCents != nil { 42 | m["price_cents"] = *c.PriceCents 43 | } 44 | if c.OnSale != nil { 45 | m["on_sale"] = *c.OnSale 46 | } 47 | return m 48 | } 49 | -------------------------------------------------------------------------------- /repository/repository_product.go: -------------------------------------------------------------------------------- 1 | package repository 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/Masterminds/squirrel" 8 | "github.com/gofrs/uuid" 9 | 10 | "github.com/hlubek/metaprogramming-go/domain" 11 | ) 12 | 13 | func InsertProduct(ctx context.Context, runner squirrel.BaseRunner, product domain.Product) error { 14 | _, err := squirrel.Insert("products"). 15 | SetMap(map[string]interface{}{ 16 | "product_id": product.ID, 17 | "article_number": product.ArticleNumber, 18 | "name": product.Name, 19 | "description": product.Description, 20 | "color": product.Color, 21 | "size": product.Size, 22 | "stock_availability": product.StockAvailability, 23 | "price_cents": product.PriceCents, 24 | "on_sale": product.OnSale, 25 | }). 26 | RunWith(runner). 27 | PlaceholderFormat(squirrel.Dollar). 28 | ExecContext(ctx) 29 | return err 30 | } 31 | 32 | func UpdateProduct(ctx context.Context, runner squirrel.BaseRunner, id uuid.UUID, changeSet ProductChangeSet) error { 33 | res, err := squirrel. 34 | Update("products"). 35 | Where(squirrel.Eq{"product_id": id}). 36 | SetMap(changeSet.toMap()). 37 | RunWith(runner). 38 | PlaceholderFormat(squirrel.Dollar). 39 | ExecContext(ctx) 40 | if err != nil { 41 | return fmt.Errorf("executing update: %w", err) 42 | } 43 | rowsAffected, err := res.RowsAffected() 44 | if err != nil { 45 | return fmt.Errorf("getting affected rows: %w", err) 46 | } 47 | if rowsAffected != 1 { 48 | return fmt.Errorf("update affected %d rows, but expected exactly 1", rowsAffected) 49 | } 50 | return err 51 | } 52 | --------------------------------------------------------------------------------