From fbf8d5cf258a828f23ec1980efd5ca065112c7e5 Mon Sep 17 00:00:00 2001 From: Evgeny Khabarov Date: Wed, 4 Mar 2020 18:56:26 -0600 Subject: [PATCH] Initial commit. --- .gitignore | 1 + .golangci.yml | 138 ++++++ .travis.yml | 31 ++ README.md | 184 +++++++ cmd/sts/main.go | 51 ++ doc.go | 1 + examples/nulls/nulls.go | 19 + examples/output/helpers.go | 47 ++ examples/output/source_to_dest.go | 25 + examples/source.go | 38 ++ examples/subsrc_to_anysub.go | 15 + field.go | 181 +++++++ field_test.go | 394 +++++++++++++++ go.mod | 9 + go.sum | 40 ++ main_suite_test.go | 46 ++ parser.go | 229 +++++++++ parser_test.go | 452 ++++++++++++++++++ run.go | 130 +++++ run_test.go | 170 +++++++ template.go | 98 ++++ template_test.go | 116 +++++ testdata/field/left.go | 8 + testdata/field/right.go | 8 + testdata/parser-input/empty.go | 1 + testdata/parser-input/non-struct-types.go | 4 + .../one-struct-field-is-of-types-time-time.go | 15 + .../one-struct-fields-are-of-slice-type.go | 8 + ...-struct-fields-are-of-struct-slice-type.go | 10 + ...-struct-fields-are-of-type-SelectorExpr.go | 11 + ...ct-fields-are-of-unsupported-slice-type.go | 7 + .../one-struct-with-unsupported-type.go | 9 + testdata/parser-input/one-struct.go | 10 + .../parser-input/two-independent-structs.go | 13 + ...wo-structs-one-is-embedded-into-another.go | 13 + testdata/run/a001_to_b001.go.golden | 11 + testdata/run/a002_to_b002.go.golden | 17 + testdata/run/a002_to_bar.go.golden | 19 + testdata/run/a002_to_foo.go.golden | 19 + testdata/run/a003_to_b003.go.golden | 19 + testdata/run/input/001_a.go | 5 + testdata/run/input/001_b.go | 5 + testdata/run/input/002_a.go | 7 + testdata/run/input/002_b.go | 7 + testdata/run/input/bar.go | 7 + testdata/run/input/foo.go | 7 + type_matcher_test.go | 38 ++ 47 files changed, 2693 insertions(+) create mode 100644 .gitignore create mode 100644 .golangci.yml create mode 100644 .travis.yml create mode 100644 README.md create mode 100644 cmd/sts/main.go create mode 100644 doc.go create mode 100644 examples/nulls/nulls.go create mode 100644 examples/output/helpers.go create mode 100644 examples/output/source_to_dest.go create mode 100644 examples/source.go create mode 100644 examples/subsrc_to_anysub.go create mode 100644 field.go create mode 100644 field_test.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 main_suite_test.go create mode 100644 parser.go create mode 100644 parser_test.go create mode 100644 run.go create mode 100644 run_test.go create mode 100644 template.go create mode 100644 template_test.go create mode 100644 testdata/field/left.go create mode 100644 testdata/field/right.go create mode 100644 testdata/parser-input/empty.go create mode 100644 testdata/parser-input/non-struct-types.go create mode 100644 testdata/parser-input/one-struct-field-is-of-types-time-time.go create mode 100644 testdata/parser-input/one-struct-fields-are-of-slice-type.go create mode 100644 testdata/parser-input/one-struct-fields-are-of-struct-slice-type.go create mode 100644 testdata/parser-input/one-struct-fields-are-of-type-SelectorExpr.go create mode 100644 testdata/parser-input/one-struct-fields-are-of-unsupported-slice-type.go create mode 100644 testdata/parser-input/one-struct-with-unsupported-type.go create mode 100644 testdata/parser-input/one-struct.go create mode 100644 testdata/parser-input/two-independent-structs.go create mode 100644 testdata/parser-input/two-structs-one-is-embedded-into-another.go create mode 100644 testdata/run/a001_to_b001.go.golden create mode 100644 testdata/run/a002_to_b002.go.golden create mode 100644 testdata/run/a002_to_bar.go.golden create mode 100644 testdata/run/a002_to_foo.go.golden create mode 100644 testdata/run/a003_to_b003.go.golden create mode 100644 testdata/run/input/001_a.go create mode 100644 testdata/run/input/001_b.go create mode 100644 testdata/run/input/002_a.go create mode 100644 testdata/run/input/002_b.go create mode 100644 testdata/run/input/bar.go create mode 100644 testdata/run/input/foo.go create mode 100644 type_matcher_test.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7823778 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +*.coverprofile diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 0000000..5374cce --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,138 @@ +run: + skip-dirs: + + # default is true. Enables skipping of directories: + # vendor$, third_party$, testdata$, examples$, Godeps$, builtin$ + skip-dirs-use-default: true + + +# all available settings of specific linters +linters-settings: + dogsled: + # checks assignments with too many blank identifiers; default is 2 + max-blank-identifiers: 2 + dupl: + # tokens count to trigger issue, 150 by default + threshold: 150 + errcheck: + # report about not checking of errors in type assertions: `a := b.(MyStruct)`; + # default is false: such cases aren't reported by default. + check-type-assertions: true + + # report about assignment of errors to blank identifier: `num, _ := strconv.Atoi(numStr)`; + # default is false: such cases aren't reported by default. + check-blank: true + + funlen: + lines: 100 + statements: 45 + gocognit: + # minimal code complexity to report, 30 by default (but we recommend 10-20) + min-complexity: 10 + goconst: + # minimal length of string constant, 3 by default + min-len: 3 + # minimal occurrences count to trigger, 3 by default + min-occurrences: 3 + gocyclo: + # minimal code complexity to report, 30 by default (but we recommend 10-20) + min-complexity: 18 + gofmt: + # simplify code: gofmt with `-s` option, true by default + simplify: true + goimports: + # put imports beginning with prefix after 3rd-party packages; + # it's a comma-separated list of prefixes + local-prefixes: github.com/ekhabarov/sts + golint: + # minimal confidence for issues, default is 0.8 + min-confidence: 0.8 + gomnd: + settings: + mnd: + # the list of enabled checks, see https://github.com/tommy-muehle/go-mnd/#checks for description. + checks: argument,case,condition,operation,return,assign + govet: + # report about shadowed variables + check-shadowing: true + + # settings per analyzer + settings: + printf: # analyzer name, run `go tool vet help` to see all analyzers + funcs: # run `go tool vet help printf` to see available settings for `printf` analyzer + - (github.com/golangci/golangci-lint/pkg/logutils.Log).Infof + - (github.com/golangci/golangci-lint/pkg/logutils.Log).Warnf + - (github.com/golangci/golangci-lint/pkg/logutils.Log).Errorf + - (github.com/golangci/golangci-lint/pkg/logutils.Log).Fatalf + + # enable or disable analyzers by name + enable: + - atomicalign + enable-all: false + disable: + - shadow + disable-all: false + lll: + # max line length, lines longer will be reported. Default is 120. + # '\t' is counted as 1 character by default, and can be changed with the tab-width option + line-length: 80 + # tab width in spaces. Default to 1. + tab-width: 1 + maligned: + # print struct with more effective memory layout or not, false by default + suggest-new: true + misspell: + # Correct spellings using locale preferences for US or UK. + # Default is to use a neutral variety of English. + # Setting locale to US will correct the British spelling of 'colour' to 'color'. + locale: US + ignore-words: + - someword + nakedret: + # make an issue if func has more lines of code than this setting and it has naked returns; default is 30 + max-func-lines: 30 + +linters: + disable-all: true + enable: + - bodyclose + - deadcode + - depguard + - dogsled + - dupl + - errcheck + - funlen + - gochecknoinits + - goconst + - gocritic + - gocyclo + - gofmt + - goimports + - golint + # - gomnd + - goprintffuncname + - gosec + - gosimple + - govet + - ineffassign + - interfacer + - lll + - misspell + - nakedret + - rowserrcheck + - scopelint + - staticcheck + - structcheck + - stylecheck + - typecheck + - unconvert + - unparam + - unused + - varcheck + - whitespace +issues: + # Excluding configuration per-path, per-linter, per-text and per-source + exclude-rules: + - path: _test\.go + linters: + - gomnd diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..2907d4f --- /dev/null +++ b/.travis.yml @@ -0,0 +1,31 @@ +language: go + +cache: + directories: + - $GOPATH/pkg/mod + +env: + - GO111MODULE=on + +go: + - 1.12.x + - 1.13.x + - 1.14.x + - tip + +before_install: + - GO111MODULE=off go get github.com/onsi/gomega + - GO111MODULE=off go get github.com/onsi/ginkgo/ginkgo + - go mod download + - go mod verify + +script: + - ginkgo -r -race -coverprofile=coverage.txt -covermode=atomic + +matrix: + fast_finish: true + allow_failures: + - go: tip + +after_success: + - bash <(curl -s https://codecov.io/bash) diff --git a/README.md b/README.md new file mode 100644 index 0000000..c5b3970 --- /dev/null +++ b/README.md @@ -0,0 +1,184 @@ +# sts: struct to struct: generator of transformation functions + +[![codecov](https://codecov.io/gh/ekhabarov/sts/branch/master/graph/badge.svg)](https://codecov.io/gh/ekhabarov/sts) +[![GitHub release (latest SemVer)](https://img.shields.io/github/v/release/ekhabarov/sts)](https://github.com/ekhabarov/sts/releases) +[![Travis (.org)](https://img.shields.io/travis/ekhabarov/sts)](https://travis-ci.org/ekhabarov/sts) +[![GoDoc](https://godoc.org/https://godoc.org/github.com/ekhabarov/sts?status.svg)](https://godoc.org/github.com/ekhabarov/sts) + + + +* [Install](#install) +* [Motivation](#motivation) +* [Idea](#idea) + * [Other implementations.](#other-implementations) +* [How](#how) + * [Step 1](#step-1) + * [Step 2](#step-2) + * [Example matcher](#example-matcher) + * [Int2Bool, NullsTime2TimeTimePtr wait, what?](#int2bool-nullstime2timetimeptr-wait-what) +* [go generate](#go-generate) +* [License](#license) + + + +## Install + +```shell +go get -u github.com/ekhabarov/sts +``` + +## Motivation +Working on integration between one app and different APIs (most of them, +fortunately, have Go clients) includes pretty much code which transforms one +structure into another, because for Go two structures with identical field set +and identical types are different types. Identical types could be converted one +into another with simple conversion: `targetType(destType)`, but having +[identical](https://golang.org/ref/spec#Type_identity) type is too rare case. + +That means it's necessary to write such transformations manually, which is, from +one hand is tediously from another one is straightforward. + +## Idea +The idea is as simple as possible: produce set of functions which allow convert +one type into another. + +It can be done within three steps: + +1. Source code analyze. +1. Field type matching. +1. Generations pair of functions: forward `SourceType2DestType` and reverse `DestType2SourceType`. + +### Other implementations. +There is a [plugin](http://github.com/bold-commerce/protoc-gen-struct-transformer) for Protobuf with the same idea. + +## How + +### Step 1 +On first step `sts` have to obtain information about structures which will be +involved into transformation process by analyzing source code files contained +these structures. To achieve this, packages [go/ast](https://golang.org/pkg/go/ast), [go/types](https://golang.org/pkg/go/types), etc., from +standard library can be used. + +Using these packages `sts` builds a map with data types information. For details +see [parser.go](./parser.go). + +### Step 2 +Information from previous step is passes to matcher. Matcher lookups two +structures by name (structures names are passed via CLI params, see examples +below), source (left) and destination (right). Then it builds field pairs using +next rules: + +* field on the left structure with `sts` tag will be matched with field on right side by right-side field name equals to `sts` tag value. +* if right-side field not found by name, then `sts` tag value will be compared with value of provided tag list. +* any fields without `sts` or other source tags will be skipped. + +#### Example matcher +Let's say we have two structures + +```go +type Source struct { + I int + S string + I1 int `sts:"I64"` + I2 int `sts:"B"` + PT *time.Time `sts:"Nt"` + JJ string `sts:"json_field"` + D int32 `sts:"db_field"` +} +``` + +and + +```go +type Dest struct { + I int + S string + I64 int64 + B bool + Nt nulls.Time + JsonField string `json:"json_field"` + DB int64 `db:"db_field"` +} +``` + +after run a command + +```shell +sts -src /path/to/src.go:Source -dst /path/to/dst.go:Dest -o ./output -dt json,db +``` + +matcher consider next combinations + + +Source | Destination | Conversion | Note +-------|-------------|-------------------------|------ + `I` | `--` | `--` | source field has not tag + `S` | `--` | `--` | source field has not tag + `I1` | `I64` | direct | matched `sts` tag value and field name + `I2` | `B` | `Int2Bool` | matched `sts` tag value and field name + `PT` | `Nt` | `NullsTime2TimeTimePtr` | matched `sts` tag value and field name +`JJ` | `JsonField` | none | matched `sts` tag value and `json` tag value. `json` tag passed via `-dt` CLI parameter. +`DB` | `D` | direct | matched `sts` tag value and `db` tag value. `db` tag passed via `-dt` CLI parameter. + + +##### Int2Bool, NullsTime2TimeTimePtr wait, what? +Matcher uses type info provided by `go/types` package. When it compares field it +also checks paired field for [assignability](https://golang.org/pkg/go/types/#AssignableTo) and [convertibility](https://golang.org/pkg/go/types/#ConvertibleTo). +* Assignability shows can one field be assigned to another without any conversion. +* Convertibility shows can one field be directly converted to another one. + +But in cases when fields in pair are not `assignable` and are not `convertable`, +the tool just generate conversion function with name of format + +```go +2 +// and +2 +``` + +that means it's necessary to write these helper functions manually. Fortunately, +quantity of such function should be low. Number of examples can be found in +[examples](./examples/output/helpers.go) package. + + +## go generate +Go has a command `go generate` ([blog](https://blog.golang.org/generate)|[proposal](https://docs.google.com/document/d/1V03LUfjSADDooDMhe-_K59EgpTEm3V8uvQRuNMAEnjg/edit)). +This command allows to run tools mentioned in special comments in Go code, like +this: + +```go +//go:generate sts -src $GOFILE:Source -dst $GOFILE:Dest -o ./output -dt json,db +type Source struct { + I int +... +``` + +after `go generate ./...` will be run, it in turn, will run `sts` tool with +given parameters. `$GOFILE` variable will be replaced with a path to current +`.go` file by `go generate` tool. + + +## License + +MIT License + +Copyright (c) 2020 Evgeny Khabarov + +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: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +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. + diff --git a/cmd/sts/main.go b/cmd/sts/main.go new file mode 100644 index 0000000..ae68b60 --- /dev/null +++ b/cmd/sts/main.go @@ -0,0 +1,51 @@ +package main + +import ( + "flag" + "log" + "os" + + "github.com/ekhabarov/sts" +) + +var ( + src = flag.String("src", "", "Source: :") + dst = flag.String("dst", "", "Dest: :") + out = flag.String("o", "./transform", `Path to output directory. +Last part of this path will be used as output package name. +`) + st = flag.String("st", "sts", "Field tag in source structure.") + dt = flag.String("dt", "", + "List of comma-separated tag on destination structure.", + ) + helperpkg = flag.String("hp", "", "Package with helper functions") + debug = flag.Bool("debug", false, "Debug") + + version = "0.0.1-alpha-dev" +) + +func main() { + flag.Parse() + + name, content, err := sts.Run( + *src, *dst, + *st, *dt, + *out, *helperpkg, version, *debug) + must(err) + + file, err := os.Create(name) + must(err) + + _, err = file.Write(content) + must(err) +} + +func must(err error) { + if err != nil { + if *debug { + log.Fatalf("%+v", err) + } else { + log.Fatalf("%v", err) + } + } +} diff --git a/doc.go b/doc.go new file mode 100644 index 0000000..e6a16e8 --- /dev/null +++ b/doc.go @@ -0,0 +1 @@ +package sts diff --git a/examples/nulls/nulls.go b/examples/nulls/nulls.go new file mode 100644 index 0000000..50f0f5b --- /dev/null +++ b/examples/nulls/nulls.go @@ -0,0 +1,19 @@ +// Package nulls contains wrappers for representing dummy null types. +package nulls + +import "time" + +type Time struct { + Time time.Time + Valid bool +} + +type String struct { + String string + Valid bool +} + +type Int struct { + Int int + Valid bool +} diff --git a/examples/output/helpers.go b/examples/output/helpers.go new file mode 100644 index 0000000..7a6e8b3 --- /dev/null +++ b/examples/output/helpers.go @@ -0,0 +1,47 @@ +package output + +import ( + "time" + + "github.com/ekhabarov/sts/examples/nulls" +) + +func Bool2Int(b bool) int { + if b { + return 1 + } + + return 0 +} + +func Int2Bool(i int) bool { + return i > 0 +} + +func TimeTime2NullsTime(t time.Time) nulls.Time { + return nulls.Time{Time: t} +} + +func TimeTimePtr2NullsTime(t *time.Time) nulls.Time { + if t == nil { + return nulls.Time{} + } + return nulls.Time{ + Time: *t, + Valid: true, + } +} + +func NullsTime2TimeTime(nt nulls.Time) time.Time { + if nt.Valid { + return nt.Time + } + return time.Time{} +} + +func NullsTime2TimeTimePtr(nt nulls.Time) *time.Time { + if nt.Valid { + return &nt.Time + } + return &time.Time{} +} diff --git a/examples/output/source_to_dest.go b/examples/output/source_to_dest.go new file mode 100644 index 0000000..87a1d18 --- /dev/null +++ b/examples/output/source_to_dest.go @@ -0,0 +1,25 @@ +// Auto-generated code. DO NOT EDIT!!! +// Generated by sts v0.0.1-alpha-dev. + +package output + +import "github.com/ekhabarov/sts/examples" + +func Source2Dest(src examples.Source) examples.Dest { + return examples.Dest{ + I64: int64(src.I1), + B: Int2Bool(src.I2), + Nt: TimeTimePtr2NullsTime(src.PT), + JsonField: src.JJ, + DB: int64(src.D), + } +} +func Dest2Source(src examples.Dest) examples.Source { + return examples.Source{ + I1: int(src.I64), + I2: Bool2Int(src.B), + PT: NullsTime2TimeTimePtr(src.Nt), + JJ: src.JsonField, + D: int32(src.DB), + } +} diff --git a/examples/source.go b/examples/source.go new file mode 100644 index 0000000..0c6af39 --- /dev/null +++ b/examples/source.go @@ -0,0 +1,38 @@ +package examples + +import ( + "time" + + "github.com/ekhabarov/sts/examples/nulls" +) + +//go:generate sts -src $GOFILE:Source -dst $GOFILE:Dest -o ./output -dt json,db +type Source struct { + I int + S string + I1 int `sts:"I64"` + I2 int `sts:"B"` // types.Basic + PT *time.Time `sts:"Nt"` // types.Named + JJ string `sts:"json_field"` + D int32 `sts:"db_field"` +} + +// dest, techincally it's some isolated structure we cannot change. +type Dest struct { + I int + S string + I64 int64 + B bool + Nt nulls.Time + JsonField string `json:"json_field"` + DB int64 `db:"db_field"` +} + +//go:generate sts -src $GOFILE:subsrc -dst $GOFILE:anysub -o . +type subsrc struct { + Field1 string `sts:"Test"` +} + +type anysub struct { + Test string +} diff --git a/examples/subsrc_to_anysub.go b/examples/subsrc_to_anysub.go new file mode 100644 index 0000000..971d2d7 --- /dev/null +++ b/examples/subsrc_to_anysub.go @@ -0,0 +1,15 @@ +// Auto-generated code. DO NOT EDIT!!! +// Generated by sts v0.0.1-alpha-dev. + +package examples + +func subsrc2anysub(src subsrc) anysub { + return anysub{ + Test: src.Field1, + } +} +func anysub2subsrc(src anysub) subsrc { + return subsrc{ + Field1: src.Test, + } +} diff --git a/field.go b/field.go new file mode 100644 index 0000000..2cc86f1 --- /dev/null +++ b/field.go @@ -0,0 +1,181 @@ +package sts + +import ( + "errors" + "fmt" + "go/types" + "strings" +) + +var ( + ErrEmptyLeftType = errors.New("left type is empty") + ErrEmptyRightType = errors.New("right type is empty") + ErrEmptyLeftField = errors.New("left field is empty") + ErrEmptRightField = errors.New("right field is empty") + ErrEmptyTag = errors.New("empty tag") +) + +// fpair represents pair of matched fields. +type fpair struct { + lt, rt string // types + lf, rf string // field + lp, rp bool // pointers + // fields in the pair can be assigned one onto another. + assignable bool + // fields in th pair require conversion. + convertable bool + // Order in structure. + ord uint8 +} + +type fpairlist []fpair + +// Len is the number of elements in the collection. +func (pl fpairlist) Len() int { + return len(pl) +} + +// Less reports whether the element with +// index i should sort before the element with index j. +func (pl fpairlist) Less(i int, j int) bool { + return pl[i].ord < pl[j].ord +} + +// Swap swaps the elements with indexes i and j. +func (pl fpairlist) Swap(i int, j int) { + pl[i], pl[j] = pl[j], pl[i] +} + +// String prints fields map with package names. +func (p fpair) Print(swap, debug bool, helperpkg string) (string, error) { + lf, rf, lt, rt, lp, rp := p.lf, p.rf, p.lt, p.rt, p.lp, p.rp + + switch { + case lf == "": + return "", ErrEmptyLeftField + case rf == "": + return "", ErrEmptRightField + case !p.assignable && lt == "": + return "", ErrEmptyLeftType + case !p.assignable && rt == "": + return "", ErrEmptyRightType + } + + if swap { + lt, rt = rt, lt // types + lf, rf = rf, lf // field names + lp, rp = rp, lp // pointers + } + + switch { + // assignable has precedence over convertable + case p.assignable: + lf = "src." + lf + case p.convertable: + + lf = fmt.Sprintf("%s(src.%s)", rt, lf) + default: + lt = strings.Replace(strings.Title(lt), ".", "", -1) + rt = strings.Replace(strings.Title(rt), ".", "", -1) + + if lp { + lt += "Ptr" + } + + if rp { + rt += "Ptr" + } + + lf = fmt.Sprintf("%s2%s(src.%s)", lt, rt, lf) + + if helperpkg != "" { + lf = helperpkg + "." + lf + } + } + + tpl := "%[1]s: %[2]s," + args := []interface{}{rf, lf} + + if debug { + tpl += " \t// %[3]s: %[4]s\t\tassignable: %t, \t\tconvertable: %t" + args = append(args, rt, lt, p.assignable, p.convertable) + } + + return fmt.Sprintf(tpl, args...), nil +} + +// match returns field name and type for right (destination) side found by +// next rules: +// +// * Try to find destination field comparing tag from left side with field name +// on the right. +// * If field not found, try to compare tag from the left with vtags on the +// right. +// +// Case when we have one structure which should be mapped with 3 different +// structures. In this case it's possible to define 3 different tags on source +// (left) structure which will be mapped to field name or valid tag name on +// destination (right) structure. +func match(tag string, right Fields, vtags []string) (string, string, error) { + if tag == "" { + return "", "", ErrEmptyTag + } + + // search by field name + if f, ok := right[tag]; ok { + if f.Type == nil { + return "", "", fmt.Errorf("type for field %q is not found", f) + } + return tag, typName(f.Type.String()), nil + } + + // search among valid tags on the right side. + for n, f := range right { + for t, v := range f.Tags { + for _, vt := range vtags { + if vt == t && tag == v { + return n, typName(f.Type.String()), nil + } + } + } + } + + return "", "", nil +} + +// link pairs field from different sides by field name or by tag on destination +// (right) structure. +func link( + left, right Fields, + sourceTag string, + vtags []string, +) (fpairlist, error) { + fp := fpairlist{} + + // go through all of the source fields and try + for name, f := range left { + rf, rt, err := match(f.Tags[sourceTag], right, vtags) + if err != nil && err != ErrEmptyTag { // skip fields without tags. + return nil, err + } + + if rf == "" { + continue + } + + ff := right[rf] + fp = append(fp, fpair{ + lf: name, + rf: rf, + lt: typName(f.Type.String()), + rt: rt, + lp: f.IsPointer, + rp: left[rf].IsPointer, + assignable: types.AssignableTo(f.Type, ff.Type), + convertable: types.ConvertibleTo(f.Type, ff.Type), + ord: f.Ord, + }) + } + + return fp, nil +} diff --git a/field_test.go b/field_test.go new file mode 100644 index 0000000..41b0418 --- /dev/null +++ b/field_test.go @@ -0,0 +1,394 @@ +package sts + +import ( + "bytes" + "io/ioutil" + "sort" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/ginkgo/extensions/table" + . "github.com/onsi/gomega" +) + +var _ = Describe("Field", func() { + + Describe("Pairlist", func() { + + Context("when call Len method", func() { + + It("returns length of the list", func() { + fl := fpairlist{fpair{}, fpair{}, fpair{}} + Expect(fl.Len()).To(Equal(3)) + }) + }) + + Context("when call Less method", func() { + + It("compares slice elements", func() { + fl := fpairlist{fpair{ord: 1}, fpair{ord: 2}} + Expect(fl.Less(0, 1)).To(BeTrue()) + Expect(fl.Less(1, 0)).To(BeFalse()) + }) + }) + + Context("when call Swap method", func() { + + It("swaps elements", func() { + fl := fpairlist{fpair{ord: 1}, fpair{ord: 2}} + Expect(fl[0].ord).To(Equal(uint8(1))) + Expect(fl[1].ord).To(Equal(uint8(2))) + fl.Swap(0, 1) + Expect(fl[0].ord).To(Equal(uint8(2))) + Expect(fl[1].ord).To(Equal(uint8(1))) + }) + }) + }) + + Describe("Field printer", func() { + + var ( + ar = "right: src.left," + ars = "left: src.right," + + cr = "right: Rtype(src.left)," + crs = "left: Ltype(src.right)," + + lf, rf = "left", "right" + lt, rt = "Ltype", "Rtype" + ) + + DescribeTable("Print", + func(p fpair, hp, exp, expSwapped string, experr error) { + got, err := p.Print(false, false, hp) + if experr == nil { + Expect(err).NotTo(HaveOccurred()) + } else { + Expect(err).To(MatchError(experr.Error())) + } + + gotSwapped, err := p.Print(true, false, hp) + if experr == nil { + Expect(err).NotTo(HaveOccurred()) + } else { + Expect(err).To(MatchError(experr.Error())) + } + + Expect(got).To(Equal(exp)) + Expect(gotSwapped).To(Equal(expSwapped)) + }, + + Entry("Empty struct", fpair{}, "", "", "", ErrEmptyLeftField), + + Entry("Empty right field", fpair{lf: lf}, "", "", "", ErrEmptRightField), + + // assignable && !convertable + Entry("Field names only, assignable", fpair{ + lf: lf, rf: rf, + assignable: true, + }, "", ar, ars, nil), + + Entry("Field names with types, assignable", fpair{ + lf: lf, rf: rf, lt: lt, rt: rt, + assignable: true, + }, "", ar, ars, nil), + + Entry("Field names with types, left pointer, assignable", fpair{ + lf: lf, rf: rf, lt: lt, rt: rt, + lp: true, rp: false, + assignable: true, + }, "", ar, ars, nil), + + Entry("Field names with types, right pointer, assignable", fpair{ + lf: lf, rf: rf, lt: lt, rt: rt, + lp: false, rp: true, + assignable: true, + }, "", ar, ars, nil), + + Entry("Field names with types, both pointers, assignable", fpair{ + lf: lf, rf: rf, lt: lt, rt: rt, + lp: true, rp: true, + assignable: true, + }, "", ar, ars, nil), + + // assignable && convertable + Entry("Field names only, assignable & convertable", fpair{ + lf: lf, rf: rf, assignable: true, + convertable: true, + }, "", ar, ars, nil), + + Entry("Field names with types, assignable & convertable", fpair{ + lf: lf, rf: rf, lt: lt, rt: rt, + assignable: true, + convertable: true, + }, "", ar, ars, nil), + + Entry("Field names with types, left pointer, assignable & convertable", + fpair{ + lf: lf, rf: rf, lt: lt, rt: rt, + lp: true, rp: false, + assignable: true, + convertable: true, + }, "", ar, ars, nil), + + Entry("Field names with types, right pointer, assignable & convertable", + fpair{ + lf: lf, rf: rf, lt: lt, rt: rt, + lp: false, rp: true, + assignable: true, + convertable: true, + }, "", ar, ars, nil), + + Entry("Field names with types, both pointers, assignable & convertable", + fpair{ + lf: lf, rf: rf, lt: lt, rt: rt, + lp: true, rp: true, + assignable: true, + convertable: true, + }, "", ar, ars, nil), + + // !assignable && convertable + Entry("Field names only, convertable", fpair{ + lf: lf, rf: rf, + convertable: true, + }, "", "", "", ErrEmptyLeftType), + + Entry("Field names only, empty right type, convertable", fpair{ + lf: lf, rf: rf, lt: lt, + convertable: true, + }, "", "", "", ErrEmptyRightType), + + Entry("Field names with types, convertable", fpair{ + lf: lf, rf: rf, lt: lt, rt: rt, + convertable: true, + }, "", cr, crs, nil), + + Entry("Field names with types, left pointer, convertable", fpair{ + lf: lf, rf: rf, lt: lt, rt: rt, + lp: true, rp: false, + convertable: true, + }, "", cr, crs, nil), + + Entry("Field names with types, right pointer, convertable", fpair{ + lf: lf, rf: rf, lt: lt, rt: rt, + lp: false, rp: true, + convertable: true, + }, "", cr, crs, nil), + + Entry("Field names with types, both pointers, convertable", fpair{ + lf: lf, rf: rf, lt: lt, rt: rt, + lp: true, rp: true, + convertable: true, + }, "", cr, crs, nil), + + // !assignable && !convertable + Entry("Field names only", fpair{ + lf: lf, rf: rf, + }, "", "", "", ErrEmptyLeftType), + + Entry("Field names with types", fpair{ + lf: lf, rf: rf, lt: lt, rt: rt, + }, "", + "right: Ltype2Rtype(src.left),", + "left: Rtype2Ltype(src.right),", + nil, + ), + + Entry("Field names with types, left pointer", fpair{ + lf: lf, rf: rf, lt: lt, rt: rt, + lp: true, rp: false, + }, "", + "right: LtypePtr2Rtype(src.left),", + "left: Rtype2LtypePtr(src.right),", + nil, + ), + + Entry("Field names with types, right pointer", fpair{ + lf: lf, rf: rf, lt: lt, rt: rt, + lp: false, rp: true, + }, "", + "right: Ltype2RtypePtr(src.left),", + "left: RtypePtr2Ltype(src.right),", + nil, + ), + + Entry("Field names with types, both pointers", fpair{ + lf: lf, rf: rf, lt: lt, rt: rt, + lp: true, rp: true, + }, "", + "right: LtypePtr2RtypePtr(src.left),", + "left: RtypePtr2LtypePtr(src.right),", + nil, + ), + + Entry("Field names with types & helpers in different package", fpair{ + lf: lf, rf: rf, lt: lt, rt: rt, + }, "helpers", + "right: helpers.Ltype2Rtype(src.left),", + "left: helpers.Rtype2Ltype(src.right),", + nil, + ), + ) + }) + + Describe("match function", func() { + + type input struct { + tag string + expName string + expType string + expErr string + right Fields + vtags []string + } + + DescribeTable("returns field name and type", + func(i input) { + name, typ, err := match(i.tag, i.right, i.vtags) + if i.expErr != "" { + Expect(err).To(MatchError(i.expErr)) + } else { + Expect(err).NotTo(HaveOccurred()) + } + Expect(name).To(Equal(i.expName)) + Expect(typ).To(Equal(i.expType)) + }, + + Entry("Empty tag", input{ + expErr: "empty tag", + }), + + Entry("Empty fields map", input{ + tag: "sometag", + }), + + Entry("Field with sts tag == field name on the right ", input{ + tag: "Field1", + expName: "Field1", + expType: "string", + right: Fields{ + "Field1": Field{Type: bt("string")}, + "Field2": Field{Type: bt("int")}, + }, + }), + + Entry("sts tag == json tag on the right and json is not valid tag", + input{ + tag: "field_2", + right: Fields{ + "Field1": Field{Type: bt("string")}, + "Field2": Field{Type: bt("int"), Tags: Tags{"json": "field_2"}}, + }, + }), + + Entry("sts tag == json tag on the right and json is valid tag", + input{ + tag: "field_2", + expName: "Field2", + expType: "int", + right: Fields{ + "Field1": Field{Type: bt("string")}, + "Field2": Field{Type: bt("int"), Tags: Tags{"json": "field_2"}}, + }, + vtags: []string{"json"}, + }), + + Entry("bar tag == db tag on the right and db is valid tag", input{ + tag: "field_3", + expName: "Field3", + expType: "int", + right: Fields{ + "Field1": Field{Type: bt("string")}, + "Field2": Field{Type: bt("int"), Tags: Tags{"json": "field_2"}}, + "Field3": Field{Type: bt("int"), Tags: Tags{"db": "field_3"}}, + }, + vtags: []string{"db"}, + }), + ) + }) + + Describe("Link function", func() { + + var ( + readStruct = func(fname, sname string) Fields { + f, err := ioutil.ReadFile("./testdata/field/" + fname) + Expect(err).NotTo(HaveOccurred()) + + data, err := Parse("", bytes.NewReader(f), []string{ + "sts", "json", "bar", "db", + }) + Expect(err).NotTo(HaveOccurred()) + + fields, ok := data.Structs[sname] + Expect(ok).To(BeTrue()) + + return fields + } + + lf, rf Fields + ) + + BeforeEach(func() { + lf = readStruct("left.go", "Left") + rf = readStruct("right.go", "Right") + Expect(lf).To(HaveLen(4)) + Expect(rf).To(HaveLen(4)) + }) + + Context("when have two structures with tags", func() { + + It("returns filled pairs", func() { + pairs, err := link(lf, rf, "sts", []string{"json"}) + Expect(err).NotTo(HaveOccurred()) + + sort.Sort(pairs) + + Expect(pairs).To(HaveLen(3)) + + Expect(pairs[0]).To(Equal(fpair{ + lf: "A", rf: "A", + lt: "int", rt: "int", + lp: false, rp: false, + convertable: true, assignable: true, + ord: 0, + })) + + Expect(pairs[1]).To(Equal(fpair{ + lf: "B", rf: "B", + lt: "string", rt: "string", + lp: false, rp: false, + convertable: true, assignable: true, + ord: 1, + })) + + Expect(pairs[2]).To(Equal(fpair{ + lf: "C", rf: "C", + lt: "float32", rt: "float32", + lp: false, rp: false, + convertable: true, assignable: true, + ord: 2, + })) + + }) + + It("fills field with source tag 'bar' and dest tag 'db'", func() { + pairs, err := link(lf, rf, "bar", []string{"db"}) + Expect(err).NotTo(HaveOccurred()) + + sort.Sort(pairs) + + Expect(pairs).To(HaveLen(1)) + + Expect(pairs[0]).To(Equal(fpair{ + lf: "D", rf: "Double", + lt: "int", rt: "int", + lp: false, rp: false, + convertable: true, assignable: true, + ord: 3, + })) + + }) + }) + + }) + +}) diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..1573519 --- /dev/null +++ b/go.mod @@ -0,0 +1,9 @@ +module github.com/ekhabarov/sts + +go 1.13 + +require ( + github.com/onsi/ginkgo v1.11.0 + github.com/onsi/gomega v1.8.1 + golang.org/x/tools v0.0.0-20200131211209-ecb101ed6550 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..f501131 --- /dev/null +++ b/go.sum @@ -0,0 +1,40 @@ +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.11.0 h1:JAKSXpt1YjtLA7YpPiqO9ss6sNXEsPfSGdwN0UHqzrw= +github.com/onsi/ginkgo v1.11.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.12.0 h1:Iw5WCbBcaAAd0fpRb1c9r5YCylv4XDoCSigm1zLevwU= +github.com/onsi/gomega v1.8.1 h1:C5Dqfs/LeauYDX0jJXIe2SWmwCbGzx9yF8C8xy3Lh34= +github.com/onsi/gomega v1.8.1/go.mod h1:Ho0h+IUsWyvy1OpqCwxlQ/21gkhVunqlU8fDGcoTdcA= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd h1:nTDtHvHSdCn1m6ITfMRqtOd/9+7a3s8RBNOZ3eYZzJA= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859 h1:R/3boaszxrf1GEUWTVDzSKVwLmSJpwZ1yqXm8j0v2QI= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e h1:o3PsSEY8E4eXWkXrIP9YJALUkVZqzHJT5DOasTyn8Vs= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d h1:+R4KGOnez64A81RvjARKc4UT5/tI9ujCIVX+P5KiHuI= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/tools v0.0.0-20200131211209-ecb101ed6550 h1:3Kc3/T5DQ/majKzDmb+0NzmbXFhKLaeDTp3KqVPV5Eo= +golang.org/x/tools v0.0.0-20200131211209-ecb101ed6550/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7 h1:9zdDQZ7Thm29KFXgAX/+yaf3eVbP7djjWp/dXAppNCc= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898 h1:/atklqdjdhuosWIl6AIbOeHJjicWYPqR9bpxqxYG2pA= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/main_suite_test.go b/main_suite_test.go new file mode 100644 index 0000000..6f701e1 --- /dev/null +++ b/main_suite_test.go @@ -0,0 +1,46 @@ +package sts + +import ( + "go/types" + "testing" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +func TestSource(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Source Suite") +} + +type BasicType struct { + kind types.BasicKind + info types.BasicInfo + name string +} + +func (b BasicType) String() string { return b.name } +func (b BasicType) Underlying() types.Type { return b } + +func (b *BasicType) Kind() types.BasicKind { return b.kind } +func (b *BasicType) Info() types.BasicInfo { return b.info } +func (b *BasicType) Name() string { return b.name } + +func bt(n string) types.Type { + switch n { + case "int": + return &BasicType{ + kind: types.Int, + info: types.IsInteger, + name: n, + } + + case "string": + return &BasicType{ + kind: types.String, + info: types.IsString, + name: n, + } + } + return nil +} diff --git a/parser.go b/parser.go new file mode 100644 index 0000000..8e89c44 --- /dev/null +++ b/parser.go @@ -0,0 +1,229 @@ +package sts + +import ( + "fmt" + "go/ast" + "go/importer" + "go/parser" + "go/token" + "go/types" + "io" + "log" + "reflect" + "strconv" + "strings" +) + +type ( + // Represents field tags with values. + Tags map[string]string + + // Field contains information about one structure field without field name. + Field struct { + // internal type info. + Type types.Type + // Equals true if field is a pointer. + IsPointer bool + // Field tags with values. + Tags Tags + // Order inside structure. It's used for printing fields with correct order. + Ord uint8 + } + + // Fields is a set of fields of one structure. + // Key is field name. + Fields map[string]Field + + // File contains structures from parsed file. + File struct { + Package string + Structs map[string]Fields + } +) + +// String return structure information as a string. +func (s Fields) String() string { + c := "" + for k, v := range s { + c += fmt.Sprintf( + "// Field: %q, Type: %q, isPointer: %t, Tags: %v\n", + k, v.Type, v.IsPointer, v.Tags, + ) + } + c += "\n" + return c +} + +// typName return type name in format . for FQTN like +// github.com/ekhabarov/sts/examples/nulls.Time. +func typName(t string) string { + s := strings.Split(t, "/") + return s[len(s)-1] +} + +func (fi Field) String() string { + s := strings.Split(fi.Type.String(), "/") + t := s[len(s)-1] + + if fi.IsPointer { + return "*" + t + } + return t +} + +// inspect is a function which is run for each node in source file. See go/ast +// package for details. +func inspect( + output *File, info *types.Info, tags []string, +) func(n ast.Node) bool { + return func(n ast.Node) bool { + if p, ok := n.(*ast.File); ok { + output.Package = p.Name.Name + return true + } + + spec, ok := n.(*ast.TypeSpec) + if !ok || spec.Type == nil { // skip non-types and empty types + return true + } + + s, ok := spec.Type.(*ast.StructType) + if !ok { // skip non-struct types + return true + } + + sname := spec.Name.String() + if output.Structs == nil { + output.Structs = map[string]Fields{} + } + + if _, ok := output.Structs[sname]; !ok { + output.Structs[sname] = Fields{} + } + + embeddedCounter := 0 + ord := uint8(0) + for _, field := range s.Fields.List { + fname := "embedded_" + // Embedded structures have no names. + if field.Names != nil { + fname = field.Names[0].Name + } else { + fname += strconv.Itoa(embeddedCounter) + embeddedCounter++ + } + + var ftags Tags + + if t := field.Tag; t != nil { + ftags = fieldTags(t.Value, tags) + } + + id, fn, typ, ptr := typsw(field.Type) + + if fn != "" { + fname = fn + } + + _ = typ + + output.Structs[sname][fname] = Field{ + Type: info.TypeOf(id), + IsPointer: ptr, + Tags: ftags, + Ord: ord, + } + ord++ + } + return false + } +} + +func fieldTags(tagValue string, list []string) Tags { + tags := Tags{} + + rawtag := strings.Replace(tagValue, "`", "", -1) + + for _, t := range list { + if v := reflect.StructTag(rawtag).Get(t); v != "" { + tags[t] = v + } + } + + return tags +} + +// typsw is a recursive type switch which is used by inspect. +func typsw(fieldType ast.Expr) (id *ast.Ident, fname, typ string, ptr bool) { + switch t := fieldType.(type) { + case *ast.Ident: // simple types e.g. int, string, etc. + id = t + typ = t.Name + return + + case *ast.SelectorExpr: // types like time.Time, time.Duration, nulls.String + id = t.Sel + typ = fmt.Sprintf("%s.%s", t.X.(*ast.Ident).Name, t.Sel.Name) + return + + case *ast.StarExpr: // pointer to something + id, fname, typ, _ = typsw(t.X) + ptr = true + return + + case *ast.ArrayType: + id, fname, typ, ptr = typsw(t.Elt) + return + + default: + typ = fmt.Sprintf("%T", t) + fname = fmt.Sprintf("unsupported_%s_%d", typ, t.Pos()) + } + return +} + +// Parse gets path to source file or content of source file as a io.Reader and +// run inspect functions on it. Function returns list of structures with +// information their about fields. +func Parse(path string, src io.Reader, tags []string) (*File, error) { + fset := token.NewFileSet() + node, err := parser.ParseFile(fset, path, src, 0) + if err != nil { + return nil, err + } + + info := types.Info{ + Types: make(map[ast.Expr]types.TypeAndValue), + Defs: make(map[*ast.Ident]types.Object), + Uses: make(map[*ast.Ident]types.Object), + } + + // Important info about importer + // https://github.com/golang/go/issues/11415#issuecomment-283445198 + // + // Basically, importer.Default() doesn't work when package like + // "github.com/ekhabarov/sts/example/nulls" is imported. + conf := types.Config{Importer: importer.ForCompiler(fset, "source", nil)} + + _, err = conf.Check("source", fset, []*ast.File{node}, &info) + if err != nil { + log.Fatal(err) + } + + f := &File{} + + ast.Inspect(node, inspect(f, &info, tags)) + + return f, nil +} + +// Lookup return structure by name from parsed source file or an error if +// structure with such name not found. +func Lookup(f *File, name string) (Fields, error) { + flds, ok := f.Structs[name] + if !ok { + return flds, fmt.Errorf("structure %q not found", name) + } + + return flds, nil +} diff --git a/parser_test.go b/parser_test.go new file mode 100644 index 0000000..04fd7d2 --- /dev/null +++ b/parser_test.go @@ -0,0 +1,452 @@ +package sts + +import ( + "bytes" + "io/ioutil" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + gs "github.com/onsi/gomega/gstruct" +) + +func loadFile(filepath string) File { + fc, err := ioutil.ReadFile("./testdata/parser-input/" + filepath) + Expect(err).NotTo(HaveOccurred()) + + str, err := Parse(filepath, bytes.NewReader(fc), nil) + Expect(err).NotTo(HaveOccurred()) + + return *str +} + +var _ = Describe("Parser", func() { + + Describe("Load and parse", func() { + + Context("empty.go", func() { + + It("uses package name only", func() { + + str := loadFile("empty.go") + + Expect(str).To(gs.MatchAllFields(gs.Fields{ + "Package": Equal("whatever"), + "Structs": BeNil(), + })) + }) + }) + + Context("non-struct-types.go", func() { + + It("uses package name only", func() { + + str := loadFile("non-struct-types.go") + + Expect(str).To(gs.MatchAllFields(gs.Fields{ + "Package": Equal("whatever"), + "Structs": BeNil(), + })) + }) + }) + + Context("one-struct.go", func() { + + It("return info about one structure", func() { + + str := loadFile("one-struct.go") + + Expect(str).To(gs.MatchAllFields(gs.Fields{ + "Package": Equal("whatever"), + "Structs": gs.MatchAllKeys(gs.Keys{ + "MyStruct": gs.MatchAllKeys(gs.Keys{ + + "I": gs.MatchAllFields(gs.Fields{ + "Type": TypeMatcher("int"), + "IsPointer": BeFalse(), + "Tags": BeZero(), + "Ord": Equal(uint8(0)), + }), + + "PI": gs.MatchAllFields(gs.Fields{ + "Type": TypeMatcher("int"), + "IsPointer": BeTrue(), + "Tags": BeZero(), + "Ord": Equal(uint8(1)), + }), + + "S": gs.MatchAllFields(gs.Fields{ + "Type": TypeMatcher("string"), + "IsPointer": BeFalse(), + "Tags": BeZero(), + "Ord": Equal(uint8(2)), + }), + + "PS": gs.MatchAllFields(gs.Fields{ + "Type": TypeMatcher("string"), + "IsPointer": BeTrue(), + "Tags": BeZero(), + "Ord": Equal(uint8(3)), + }), + }), + }), + })) + }) + }) + + Context("two-structs-one-is-embedded-into-another.go", func() { + + It("return info about two structures and embedded field", func() { + + str := loadFile("two-structs-one-is-embedded-into-another.go") + + Expect(str).To(gs.MatchAllFields(gs.Fields{ + "Package": Equal("whatever"), + "Structs": gs.MatchAllKeys(gs.Keys{ + + "MyStruct": gs.MatchAllKeys(gs.Keys{ + + "I": gs.MatchAllFields(gs.Fields{ + "Type": TypeMatcher("int"), + "IsPointer": BeFalse(), + "Tags": BeZero(), + "Ord": Equal(uint8(0)), + }), + + "S": gs.MatchAllFields(gs.Fields{ + "Type": TypeMatcher("string"), + "IsPointer": BeFalse(), + "Tags": BeZero(), + "Ord": Equal(uint8(1)), + }), + + "embedded_0": gs.MatchAllFields(gs.Fields{ + "Type": TypeMatcher("source.Embedded"), + "IsPointer": BeFalse(), + "Tags": BeZero(), + "Ord": Equal(uint8(2)), + }), + }), + + "Embedded": gs.MatchAllKeys(gs.Keys{ + "CS": gs.MatchAllFields(gs.Fields{ + "Type": TypeMatcher("string"), + "IsPointer": BeFalse(), + "Tags": BeZero(), + "Ord": Equal(uint8(0)), + }), + }), + }), + })) + }) + + }) + + Context("two-independent-structs.go", func() { + + It("return info about two structures", func() { + + str := loadFile("two-independent-structs.go") + + Expect(str).To(gs.MatchAllFields(gs.Fields{ + "Package": Equal("whatever"), + "Structs": gs.MatchAllKeys(gs.Keys{ + + "Second": gs.MatchAllKeys(gs.Keys{ + + "Intf": gs.MatchAllFields(gs.Fields{ + "Type": TypeMatcher("int"), + "IsPointer": BeFalse(), + "Tags": BeZero(), + "Ord": Equal(uint8(0)), + }), + + "Strf": gs.MatchAllFields(gs.Fields{ + "Type": TypeMatcher("string"), + "IsPointer": BeTrue(), + "Tags": BeZero(), + "Ord": Equal(uint8(1)), + }), + }), + + "MyStruct": gs.MatchAllKeys(gs.Keys{ + + "Intf": gs.MatchAllFields(gs.Fields{ + "Type": TypeMatcher("int"), + "IsPointer": BeTrue(), + "Tags": BeZero(), + "Ord": Equal(uint8(0)), + }), + + "StringF": gs.MatchAllFields(gs.Fields{ + "Type": TypeMatcher("string"), + "IsPointer": BeFalse(), + "Tags": BeZero(), + "Ord": Equal(uint8(1)), + }), + }), + }), + })) + }) + }) + + Context("one-struct-fields-are-of-type-SelectorExpr.go", func() { //nolint + + It("return info about one structure", func() { + + str := loadFile("one-struct-fields-are-of-type-SelectorExpr.go") + + Expect(str).To(gs.MatchAllFields(gs.Fields{ + "Package": Equal("whatever"), + "Structs": gs.MatchAllKeys(gs.Keys{ + + "MyStruct": gs.MatchAllKeys(gs.Keys{ + + "Intf": gs.MatchAllFields(gs.Fields{ + "Type": TypeMatcher("int"), + "IsPointer": BeFalse(), + "Tags": BeZero(), + "Ord": Equal(uint8(0)), + }), + + "Strf": gs.MatchAllFields(gs.Fields{ + "Type": TypeMatcher("string"), + "IsPointer": BeFalse(), + "Tags": BeZero(), + "Ord": Equal(uint8(1)), + }), + + "CreatedAt": gs.MatchAllFields(gs.Fields{ + "Type": TypeMatcher("time.Time"), + "IsPointer": BeFalse(), + "Tags": BeZero(), + "Ord": Equal(uint8(2)), + }), + }), + }), + })) + }) + }) + + Context("one-struct-fields-are-of-slice-type.go", func() { + + It("return info about one structure", func() { + + str := loadFile("one-struct-fields-are-of-slice-type.go") + + Expect(str).To(gs.MatchAllFields(gs.Fields{ + "Package": Equal("whatever"), + "Structs": gs.MatchAllKeys(gs.Keys{ + + "MyStruct": gs.MatchAllKeys(gs.Keys{ + + "Intf": gs.MatchAllFields(gs.Fields{ + "Type": TypeMatcher("int"), + "IsPointer": BeFalse(), + "Tags": BeZero(), + "Ord": Equal(uint8(0)), + }), + + "IntSlice": gs.MatchAllFields(gs.Fields{ + "Type": TypeMatcher("int"), + "IsPointer": BeFalse(), + "Tags": BeZero(), + "Ord": Equal(uint8(1)), + }), + }), + }), + })) + }) + }) + + Context("one-struct-fields-are-of-struct-slice-type.go", func() { + + It("return info about one structure", func() { + + str := loadFile("one-struct-fields-are-of-struct-slice-type.go") + + Expect(str).To(gs.MatchAllFields(gs.Fields{ + "Package": Equal("whatever"), + "Structs": gs.MatchAllKeys(gs.Keys{ + + "MyStruct": gs.MatchAllKeys(gs.Keys{ + + "Intf": gs.MatchAllFields(gs.Fields{ + "Type": TypeMatcher("int"), + "IsPointer": BeTrue(), + "Tags": BeZero(), + "Ord": Equal(uint8(0)), + }), + + "TimeSlice": gs.MatchAllFields(gs.Fields{ + "Type": TypeMatcher("time.Time"), + "IsPointer": BeFalse(), + "Tags": BeZero(), + "Ord": Equal(uint8(1)), + }), + }), + }), + })) + }) + }) + + Context("one-struct-fields-are-of-unsupported-slice-type.go", func() { + + It("return info about one structure", func() { + + str := loadFile("one-struct-fields-are-of-unsupported-slice-type.go") + + Expect(str).To(gs.MatchAllFields(gs.Fields{ + "Package": Equal("whatever"), + "Structs": gs.MatchAllKeys(gs.Keys{ + + "MyStruct": gs.MatchAllKeys(gs.Keys{ + + "unsupported_*ast.MapType_55": gs.MatchAllFields(gs.Fields{ + "Type": BeNil(), + "IsPointer": BeFalse(), + "Tags": BeZero(), + "Ord": Equal(uint8(0)), + }), + }), + }), + })) + }) + }) + + Context("one-struct-field-is-of-types-time-time.go", func() { //nolint + + It("return info about one structure", func() { + + str := loadFile("one-struct-field-is-of-types-time-time.go") + + Expect(str).To(gs.MatchAllFields(gs.Fields{ + "Package": Equal("whatever"), + "Structs": gs.MatchAllKeys(gs.Keys{ + + "MyStruct": gs.MatchAllKeys(gs.Keys{ + + "T": gs.MatchAllFields(gs.Fields{ + "Type": TypeMatcher("time.Time"), + "IsPointer": BeFalse(), + "Tags": BeZero(), + "Ord": Equal(uint8(0)), + }), + + "PT": gs.MatchAllFields(gs.Fields{ + "Type": TypeMatcher("time.Time"), + "IsPointer": BeTrue(), + "Tags": BeZero(), + "Ord": Equal(uint8(1)), + }), + + "ThirdPartyType": gs.MatchAllFields(gs.Fields{ + "Type": TypeMatcher("nulls.Time"), + "IsPointer": BeFalse(), + "Tags": BeZero(), + "Ord": Equal(uint8(2)), + }), + }), + }), + })) + }) + }) + + Context("one-struct-with-unsupported-type.go", func() { + + It("return info about one structure", func() { + + str := loadFile("one-struct-with-unsupported-type.go") + + Expect(str).To(gs.MatchAllFields(gs.Fields{ + "Package": Equal("whatever"), + "Structs": gs.MatchAllKeys(gs.Keys{ + + "MyStruct": gs.MatchAllKeys(gs.Keys{ + + "unsupported_*ast.FuncType_50": gs.MatchAllFields(gs.Fields{ + "Type": BeNil(), + "IsPointer": BeFalse(), + "Tags": BeZero(), + "Ord": Equal(uint8(0)), + }), + + "unsupported_*ast.MapType_62": gs.MatchAllFields(gs.Fields{ + "Type": BeNil(), + "IsPointer": BeFalse(), + "Tags": BeZero(), + "Ord": Equal(uint8(1)), + }), + + "unsupported_*ast.MapType_83": gs.MatchAllFields(gs.Fields{ + "Type": BeNil(), + "IsPointer": BeTrue(), + "Tags": BeZero(), + "Ord": Equal(uint8(2)), + }), + }), + }), + })) + }) + + }) + }) + + Describe("Lookup", func() { + + Context("when call Lookup with existing struct", func() { + + It("returns set of fields", func() { + str, err := Parse("file.go", bytes.NewReader([]byte(`package model + +type ( + MyStruct struct { + Intf int + Strf *string + } +)`)), nil) + Expect(err).NotTo(HaveOccurred()) + + fields, err := Lookup(str, "MyStruct") + Expect(err).NotTo(HaveOccurred()) + + Expect(fields).To(gs.MatchAllKeys(gs.Keys{ + + "Intf": gs.MatchAllFields(gs.Fields{ + "Type": TypeMatcher("int"), + "IsPointer": BeFalse(), + "Tags": BeZero(), + "Ord": Equal(uint8(0)), + }), + + "Strf": gs.MatchAllFields(gs.Fields{ + "Type": TypeMatcher("string"), + "IsPointer": BeTrue(), + "Tags": BeZero(), + "Ord": Equal(uint8(1)), + }), + })) + + }) + }) + + Context("when call Lookup with non-existing struct", func() { + + It("returns set of fields", func() { + str, err := Parse("file.go", bytes.NewReader([]byte(`package model + +type ( + MyStruct struct { + ID int + Name *string + } +)`)), nil) + Expect(err).NotTo(HaveOccurred()) + + fields, err := Lookup(str, "NotExists") + Expect(err).To(MatchError(`structure "NotExists" not found`)) + Expect(fields).To(BeNil()) + }) + }) + }) + +}) diff --git a/run.go b/run.go new file mode 100644 index 0000000..a26a259 --- /dev/null +++ b/run.go @@ -0,0 +1,130 @@ +package sts + +import ( + "errors" + "fmt" + "path/filepath" + "strings" + + "golang.org/x/tools/imports" +) + +var ( + format = `format is "/path/to/file.go:struct_name"` + ErrIncorectSourceParam = errors.New(`incorrect source, ` + format) + ErrIncorectDestParam = errors.New(`incorrect destination, ` + format) +) + +// split splits string by ":" into two parts. +func split(s string) (string, string) { + parts := strings.Split(s, ":") + if len(parts) != 2 { + return "", "" + } + + return parts[0], parts[1] +} + +// pkgFromPath returns package name as last elements from path. +func pkgFromPath(p string) string { + s := strings.Split(p, "/") + return s[len(s)-1] +} + +// Tags to inspect: +// * sts +// * custom +func Run( + // source and destination structures in format : + src, dst string, + sourceTag string, // Tag on source structure. + validDstTags string, // Comma-separated list of tags on destination structure. + outputDir string, + hpkg string, // Package name with helper functions. + version string, // App version. + debug bool, // If true, add debug info to output file. +) (string, []byte, error) { + sf, ssn := split(src) + if sf == "" || ssn == "" { + return "", nil, ErrIncorectSourceParam + } + + df, dsn := split(dst) + if df == "" || dsn == "" { + return "", nil, ErrIncorectDestParam + } + + parsedSrc, err := Parse(sf, nil, []string{sourceTag}) + if err != nil { + return "", nil, err + } + + vt := strings.Split(validDstTags, ",") + + parsedDst, err := Parse(df, nil, vt) + if err != nil { + return "", nil, err + } + + abs, err := filepath.Abs(outputDir) + if err != nil { + return "", nil, err + } + opkg := pkgFromPath(abs) + + // do not use package name for structures when destination package is the + // same as output one. + srcp, dstp := parsedSrc.Package, parsedDst.Package + if srcp == opkg { + srcp = "" + dstp = "" + } + + ss, ok := parsedSrc.Structs[ssn] + if !ok { + return "", nil, fmt.Errorf("source structure %s not found", ssn) + } + + ds, ok := parsedDst.Structs[dsn] + if !ok { + return "", nil, fmt.Errorf("destination structure %s not found", dsn) + } + + linkedFields, err := link(ss, ds, sourceTag, vt) + if err != nil { + return "", nil, err + } + + ff := newTemplate(ssn, dsn, srcp, dstp, linkedFields) + + fmt.Fprintf(ff, `// Auto-generated code. DO NOT EDIT!!! +// Generated by sts v%s. + +package %s + +`, version, opkg) + + err = ff.Print(false, debug, hpkg) + if err != nil { + return "", nil, err + } + + err = ff.Print(true, debug, hpkg) + if err != nil { + return "", nil, err + } + + ic, err := imports.Process("output.go", ff.Bytes(), nil) + if err != nil { + return "", nil, err + } + + ff.Reset() + ff.Write(ic) + + ofile := filepath.Join( + outputDir, strings.ToLower(fmt.Sprintf("%s_to_%s.go", ssn, dsn)), + ) + + return ofile, ff.Bytes(), nil +} diff --git a/run_test.go b/run_test.go new file mode 100644 index 0000000..c3657c8 --- /dev/null +++ b/run_test.go @@ -0,0 +1,170 @@ +package sts + +import ( + "io/ioutil" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/ginkgo/extensions/table" + . "github.com/onsi/gomega" +) + +var _ = Describe("Run", func() { + + Describe("split", func() { + + DescribeTable("split", + func(input, exp1, exp2 string) { + got1, got2 := split(input) + Expect(got1).To(Equal(exp1)) + Expect(got2).To(Equal(exp2)) + }, + + Entry("Empty stirng", "", "", ""), + Entry("String without semicolon", "abc", "", ""), + Entry("Semicolon", ":", "", ""), + Entry("Semicolon with 1st part", "abc:", "abc", ""), + Entry("Semicolon with 2nd part", ":def", "", "def"), + Entry("String with 2 parts divided by :", "abc:def", "abc", "def"), + Entry("String with 3 parts divided by :", "abc:def:zzz", "", ""), + ) + }) + + Describe("pkgFromPath", func() { + + DescribeTable("pkgFromPath", + func(input, expected string) { + got := pkgFromPath(input) + Expect(got).To(Equal(expected)) + }, + + Entry("Empty string", "", ""), + Entry("String without slash", "abc", "abc"), + Entry("Root", "/", ""), + Entry("1st level", "/abc", "abc"), + Entry("2nd level", "/abc/def", "def"), + Entry("3rd level", "/abc/def/ggg", "ggg"), + Entry("Relative with dot", "./abc/def", "def"), + Entry("Relative", "abc/def", "def"), + Entry("Deep down", "/a/b/c/d/f/e/g/h/i/j/k/l/m/n/o/p", "p"), + ) + }) + + Describe("Run", func() { + + type input struct { + left string + right string + sourceTag string + destTags string + outputDir string + helperPkg string + version string + expectedName string + expectedErr string + } + + DescribeTable("cases", + + func(in input) { + fname, content, err := Run(in.left, in.right, in.sourceTag, in.destTags, + in.outputDir, in.helperPkg, in.version, false) + if in.expectedErr == "" { + Expect(err).NotTo(HaveOccurred()) + } else { + Expect(err).To(MatchError(in.expectedErr)) + } + + Expect(fname).To(Equal(in.expectedName)) + + if in.expectedName == "" { + return + } + + gldn := "./testdata/run/" + in.expectedName + ".golden" + Expect(gldn).To(BeAnExistingFile()) + + fc, err := ioutil.ReadFile(gldn) + Expect(err).NotTo(HaveOccurred()) + + Expect(string(content)).To(Equal(string(fc))) + }, + + Entry("Empty params", + input{ + expectedErr: `incorrect source, format is "/path/to/file.go:struct_name"`, //nolint + }, + ), + + Entry("Source only", + input{ + left: "a.go:A", + expectedErr: `incorrect destination, format is "/path/to/file.go:struct_name"`, //nolint + }, + ), + + Entry("001: Field without tags", + input{ + left: "./testdata/run/input/001_a.go:A001", + right: "./testdata/run/input/001_b.go:B001", + sourceTag: "sts", + outputDir: ".", + helperPkg: "helpers", + version: "0.0.1", + expectedName: "a001_to_b001.go", + }, + ), + + Entry("002: Some field with tags", + input{ + left: "./testdata/run/input/002_a.go:A002", + right: "./testdata/run/input/002_b.go:B002", + sourceTag: "sts", + outputDir: ".", + helperPkg: "helpers", + version: "0.0.2", + expectedName: "a002_to_b002.go", + }, + ), + + Entry("003: Source struct not found", + input{ + left: "./testdata/run/input/001_a.go:None", + right: "./testdata/run/input/001_b.go:B001", + sourceTag: "sts", + outputDir: ".", + expectedErr: "source structure None not found", + }, + ), + + Entry("004: Destination struct not found", + input{ + left: "./testdata/run/input/001_a.go:A001", + right: "./testdata/run/input/001_b.go:None", + sourceTag: "sts", + outputDir: ".", + expectedErr: "destination structure None not found", + }, + ), + + Entry("005: Source 'foo' tag is mapped to field name on destination", input{ + left: "./testdata/run/input/002_a.go:A002", + right: "./testdata/run/input/foo.go:Foo", + sourceTag: "foo", + outputDir: ".", + version: "0.0.5", + expectedName: "a002_to_foo.go", + }), + + Entry("006: Source 'foo' tag is mapped to 'bar' on destination", input{ + left: "./testdata/run/input/002_a.go:A002", + right: "./testdata/run/input/bar.go:Bar", + sourceTag: "bar", + destTags: "bar", + outputDir: ".", + version: "0.0.6", + expectedName: "a002_to_bar.go", + }), + ) + }) + +}) diff --git a/template.go b/template.go new file mode 100644 index 0000000..fce5b0f --- /dev/null +++ b/template.go @@ -0,0 +1,98 @@ +package sts + +import ( + "bytes" + "fmt" + "go/ast" + "sort" +) + +func newTemplate(lt, rt, lp, rp string, ff []fpair) ftpldata { + return ftpldata{ + Buffer: &bytes.Buffer{}, + lt: lt, + rt: rt, + lp: lp, + rp: rp, + fields: ff, + } +} + +// ftpldata is a data for filling template for basic +// function. +type ftpldata struct { + *bytes.Buffer + lp, rp string // package + lt, rt string // types + fields fpairlist +} + +func (d *ftpldata) Print(swap, debug bool, helperpkg string) error { + d.header(swap) + d.retstmt(swap) + if err := d.fieldmap(swap, debug, helperpkg); err != nil { + return err + } + d.footer() + d.footer() + + return nil +} + +func (d *ftpldata) header(swap bool) { + lp, rp, lt, rt := d.lp, d.rp, d.lt, d.rt + + if rp != "" { + rp += "." + } + + if lp != "" { + lp += "." + } + + if swap { + lp, rp = rp, lp + lt, rt = rt, lt + } + + fmt.Fprintf(d, + "func %[1]s2%[2]s(src %[3]s%[1]s) %[4]s%[2]s {\n", + lt, rt, lp, rp, + ) +} + +// reutrn +func (d *ftpldata) retstmt(swap bool) { + rt, rp := d.rt, d.rp + + if swap { + rt, rp = d.lt, d.lp + } + + if rp != "" { + rp += "." + } + + fmt.Fprintf(d, "return %s%s {\n", rp, rt) +} + +func (d *ftpldata) fieldmap(swap, debug bool, helperpkg string) error { + sort.Sort(d.fields) + for _, f := range d.fields { + if !ast.IsExported(f.lf) || !ast.IsExported(f.rf) { + continue + } + p, err := f.Print(swap, debug, helperpkg) + if err != nil { + return err + } + + fmt.Fprintln(d, p) + } + + return nil +} + +func (d *ftpldata) footer() { + fmt.Fprintln(d, "}") +} diff --git a/template_test.go b/template_test.go new file mode 100644 index 0000000..4ab63c0 --- /dev/null +++ b/template_test.go @@ -0,0 +1,116 @@ +package sts + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("Template", func() { + var ( + data ftpldata + ) + + BeforeEach(func() { + data = newTemplate("lt", "rt", "lp", "rp", []fpair{ + { + lt: "flt", + rt: "frt", + lf: "Flf", + rf: "Frf", + lp: true, + rp: true, + assignable: true, + convertable: true, + ord: 1, + }, + }) + }) + + Context("when newftpldata called", func() { + + It("returns initialized ftpldata object", func() { + + Expect(data.lt).To(Equal("lt")) + Expect(data.rt).To(Equal("rt")) + Expect(data.lp).To(Equal("lp")) + Expect(data.rp).To(Equal("rp")) + + f := data.fields[0] + + Expect(f.lt).To(Equal("flt")) + Expect(f.rt).To(Equal("frt")) + Expect(f.lf).To(Equal("Flf")) + Expect(f.rf).To(Equal("Frf")) + Expect(f.lp).To(BeTrue()) + Expect(f.rp).To(BeTrue()) + Expect(f.convertable).To(BeTrue()) + Expect(f.assignable).To(BeTrue()) + Expect(f.ord).To(Equal(uint8(1))) + }) + }) + + Context("when header method called", func() { + + Context("swapped = false", func() { + It("writes filled function header into output", func() { + data.header(false) + Expect(data.String()).To(Equal("func lt2rt(src lp.lt) rp.rt {\n")) + }) + }) + + Context("swapped = true", func() { + It("writes filled function header into output", func() { + data.header(true) + Expect(data.String()).To(Equal("func rt2lt(src rp.rt) lp.lt {\n")) + }) + }) + }) + + Context("when retstmt method called", func() { + + Context("swapped = false", func() { + It("writes filled function return statement into output", func() { + data.retstmt(false) + Expect(data.String()).To(Equal("return rp.rt {\n")) + }) + }) + + Context("swapped = true", func() { + It("writes filled function header into output", func() { + data.retstmt(true) + Expect(data.String()).To(Equal("return lp.lt {\n")) + }) + }) + }) + + Context("when fieldmap method called", func() { + + Context("swapped = false", func() { + It("writes filled fields into output", func() { + // fieldmap/print method with debug=true tested in field_test.go + err := data.fieldmap(false, false, "") + Expect(err).NotTo(HaveOccurred()) + + Expect(data.String()).To(Equal("Frf: src.Flf,\n")) + }) + }) + + Context("swapped = true", func() { + It("writes filled function header into output", func() { + err := data.fieldmap(true, false, "") + Expect(err).NotTo(HaveOccurred()) + + Expect(data.String()).To(Equal("Flf: src.Frf,\n")) + }) + }) + }) + + Context("when footer method called", func() { + It("writes } into output", func() { + data.footer() + Expect(data.String()).To(Equal("}\n")) + }) + + }) + +}) diff --git a/testdata/field/left.go b/testdata/field/left.go new file mode 100644 index 0000000..fbec688 --- /dev/null +++ b/testdata/field/left.go @@ -0,0 +1,8 @@ +package field + +type Left struct { + A int `sts:"A"` + B string `sts:"B"` + C float32 `sts:"crc"` + D int `bar:"double"` +} diff --git a/testdata/field/right.go b/testdata/field/right.go new file mode 100644 index 0000000..c449923 --- /dev/null +++ b/testdata/field/right.go @@ -0,0 +1,8 @@ +package field + +type Right struct { + A int + B string + C float32 `json:"crc"` + Double int `db:"double"` +} diff --git a/testdata/parser-input/empty.go b/testdata/parser-input/empty.go new file mode 100644 index 0000000..5442027 --- /dev/null +++ b/testdata/parser-input/empty.go @@ -0,0 +1 @@ +package whatever diff --git a/testdata/parser-input/non-struct-types.go b/testdata/parser-input/non-struct-types.go new file mode 100644 index 0000000..daf4426 --- /dev/null +++ b/testdata/parser-input/non-struct-types.go @@ -0,0 +1,4 @@ +package whatever + +type myInt int +type myString string diff --git a/testdata/parser-input/one-struct-field-is-of-types-time-time.go b/testdata/parser-input/one-struct-field-is-of-types-time-time.go new file mode 100644 index 0000000..33c0f36 --- /dev/null +++ b/testdata/parser-input/one-struct-field-is-of-types-time-time.go @@ -0,0 +1,15 @@ +package whatever + +import ( + "time" + + "github.com/ekhabarov/sts/examples/nulls" +) + +type ( + MyStruct struct { + T time.Time + PT *time.Time + ThirdPartyType nulls.Time + } +) diff --git a/testdata/parser-input/one-struct-fields-are-of-slice-type.go b/testdata/parser-input/one-struct-fields-are-of-slice-type.go new file mode 100644 index 0000000..6dd6bdf --- /dev/null +++ b/testdata/parser-input/one-struct-fields-are-of-slice-type.go @@ -0,0 +1,8 @@ +package whatever + +type ( + MyStruct struct { + Intf int + IntSlice []int + } +) diff --git a/testdata/parser-input/one-struct-fields-are-of-struct-slice-type.go b/testdata/parser-input/one-struct-fields-are-of-struct-slice-type.go new file mode 100644 index 0000000..6b7e4c8 --- /dev/null +++ b/testdata/parser-input/one-struct-fields-are-of-struct-slice-type.go @@ -0,0 +1,10 @@ +package whatever + +import "time" + +type ( + MyStruct struct { + Intf *int + TimeSlice []time.Time + } +) diff --git a/testdata/parser-input/one-struct-fields-are-of-type-SelectorExpr.go b/testdata/parser-input/one-struct-fields-are-of-type-SelectorExpr.go new file mode 100644 index 0000000..0cf37cd --- /dev/null +++ b/testdata/parser-input/one-struct-fields-are-of-type-SelectorExpr.go @@ -0,0 +1,11 @@ +package whatever + +import "time" + +type ( + MyStruct struct { + Intf int + Strf string + CreatedAt time.Time + } +) diff --git a/testdata/parser-input/one-struct-fields-are-of-unsupported-slice-type.go b/testdata/parser-input/one-struct-fields-are-of-unsupported-slice-type.go new file mode 100644 index 0000000..01ecaed --- /dev/null +++ b/testdata/parser-input/one-struct-fields-are-of-unsupported-slice-type.go @@ -0,0 +1,7 @@ +package whatever + +type ( + MyStruct struct { + Items []map[string]int + } +) diff --git a/testdata/parser-input/one-struct-with-unsupported-type.go b/testdata/parser-input/one-struct-with-unsupported-type.go new file mode 100644 index 0000000..f733be4 --- /dev/null +++ b/testdata/parser-input/one-struct-with-unsupported-type.go @@ -0,0 +1,9 @@ +package whatever + +type ( + MyStruct struct { + F func() + M map[int]string + PM *map[int]string + } +) diff --git a/testdata/parser-input/one-struct.go b/testdata/parser-input/one-struct.go new file mode 100644 index 0000000..8f2a5f6 --- /dev/null +++ b/testdata/parser-input/one-struct.go @@ -0,0 +1,10 @@ +package whatever + +type ( + MyStruct struct { + I int + PI *int + S string + PS *string + } +) diff --git a/testdata/parser-input/two-independent-structs.go b/testdata/parser-input/two-independent-structs.go new file mode 100644 index 0000000..c4c0483 --- /dev/null +++ b/testdata/parser-input/two-independent-structs.go @@ -0,0 +1,13 @@ +package whatever + +type ( + Second struct { + Intf int + Strf *string + } + + MyStruct struct { + Intf *int + StringF string + } +) diff --git a/testdata/parser-input/two-structs-one-is-embedded-into-another.go b/testdata/parser-input/two-structs-one-is-embedded-into-another.go new file mode 100644 index 0000000..0aeb082 --- /dev/null +++ b/testdata/parser-input/two-structs-one-is-embedded-into-another.go @@ -0,0 +1,13 @@ +package whatever + +type ( + Embedded struct { + CS string + } + + MyStruct struct { + I int + S string + Embedded + } +) diff --git a/testdata/run/a001_to_b001.go.golden b/testdata/run/a001_to_b001.go.golden new file mode 100644 index 0000000..91c01ce --- /dev/null +++ b/testdata/run/a001_to_b001.go.golden @@ -0,0 +1,11 @@ +// Auto-generated code. DO NOT EDIT!!! +// Generated by sts v0.0.1. + +package sts + +func A0012B001(src source.A001) dest.B001 { + return dest.B001{} +} +func B0012A001(src dest.B001) source.A001 { + return source.A001{} +} diff --git a/testdata/run/a002_to_b002.go.golden b/testdata/run/a002_to_b002.go.golden new file mode 100644 index 0000000..59170e3 --- /dev/null +++ b/testdata/run/a002_to_b002.go.golden @@ -0,0 +1,17 @@ +// Auto-generated code. DO NOT EDIT!!! +// Generated by sts v0.0.2. + +package sts + +func A0022B002(src input.A002) input.B002 { + return input.B002{ + I: src.I, + F: src.F, + } +} +func B0022A002(src input.B002) input.A002 { + return input.A002{ + I: src.I, + F: src.F, + } +} diff --git a/testdata/run/a002_to_bar.go.golden b/testdata/run/a002_to_bar.go.golden new file mode 100644 index 0000000..86d5160 --- /dev/null +++ b/testdata/run/a002_to_bar.go.golden @@ -0,0 +1,19 @@ +// Auto-generated code. DO NOT EDIT!!! +// Generated by sts v0.0.6. + +package sts + +func A0022Bar(src input.A002) input.Bar { + return input.Bar{ + BI: src.I, + BS: src.S, + BF: src.F, + } +} +func Bar2A002(src input.Bar) input.A002 { + return input.A002{ + I: src.BI, + S: src.BS, + F: src.BF, + } +} diff --git a/testdata/run/a002_to_foo.go.golden b/testdata/run/a002_to_foo.go.golden new file mode 100644 index 0000000..5f91607 --- /dev/null +++ b/testdata/run/a002_to_foo.go.golden @@ -0,0 +1,19 @@ +// Auto-generated code. DO NOT EDIT!!! +// Generated by sts v0.0.5. + +package sts + +func A0022Foo(src input.A002) input.Foo { + return input.Foo{ + II: src.I, + Str: src.S, + FF: src.F, + } +} +func Foo2A002(src input.Foo) input.A002 { + return input.A002{ + I: src.II, + S: src.Str, + F: src.FF, + } +} diff --git a/testdata/run/a003_to_b003.go.golden b/testdata/run/a003_to_b003.go.golden new file mode 100644 index 0000000..604836d --- /dev/null +++ b/testdata/run/a003_to_b003.go.golden @@ -0,0 +1,19 @@ +// Auto-generated code. DO NOT EDIT!!! +// Generated by sts v0.0.2. + +package sts + +func A0022B002(src input.A002) input.B002 { + return input.B002{ + I: src.I, + S: src.S, + F: src.F, + } +} +func B0022A002(src input.B002) input.A002 { + return input.A002{ + I: src.I, + S: src.S, + F: src.F, + } +} diff --git a/testdata/run/input/001_a.go b/testdata/run/input/001_a.go new file mode 100644 index 0000000..698bd92 --- /dev/null +++ b/testdata/run/input/001_a.go @@ -0,0 +1,5 @@ +package source + +type A001 struct { + C int +} diff --git a/testdata/run/input/001_b.go b/testdata/run/input/001_b.go new file mode 100644 index 0000000..6458ed6 --- /dev/null +++ b/testdata/run/input/001_b.go @@ -0,0 +1,5 @@ +package dest + +type B001 struct { + C int +} diff --git a/testdata/run/input/002_a.go b/testdata/run/input/002_a.go new file mode 100644 index 0000000..99d2da2 --- /dev/null +++ b/testdata/run/input/002_a.go @@ -0,0 +1,7 @@ +package input + +type A002 struct { + I int `sts:"I" foo:"II" bar:"i"` + S string `sts:"s" foo:"Str" bar:"s"` // invalid value "s", yet + F float32 `sts:"F" foo:"FF" bar:"f"` +} diff --git a/testdata/run/input/002_b.go b/testdata/run/input/002_b.go new file mode 100644 index 0000000..c77634c --- /dev/null +++ b/testdata/run/input/002_b.go @@ -0,0 +1,7 @@ +package input + +type B002 struct { + I int + S string + F float32 +} diff --git a/testdata/run/input/bar.go b/testdata/run/input/bar.go new file mode 100644 index 0000000..b07a6f0 --- /dev/null +++ b/testdata/run/input/bar.go @@ -0,0 +1,7 @@ +package input + +type Bar struct { + BI int `bar:"i"` + BS string `bar:"s"` + BF float32 `bar:"f"` +} diff --git a/testdata/run/input/foo.go b/testdata/run/input/foo.go new file mode 100644 index 0000000..8a2bda4 --- /dev/null +++ b/testdata/run/input/foo.go @@ -0,0 +1,7 @@ +package input + +type Foo struct { + II int + Str string + FF float32 +} diff --git a/type_matcher_test.go b/type_matcher_test.go new file mode 100644 index 0000000..26953a3 --- /dev/null +++ b/type_matcher_test.go @@ -0,0 +1,38 @@ +package sts + +import ( + "fmt" + "go/types" + + tt "github.com/onsi/gomega/types" +) + +func TypeMatcher(exp interface{}) tt.GomegaMatcher { + return &typMatcher{ + expected: exp, + } +} + +type typMatcher struct { + expected interface{} +} + +func (t *typMatcher) Match(actual interface{}) (bool, error) { + s, ok := actual.(types.Type) + if !ok { + return false, fmt.Errorf( + "actual value of type %T should implement types.Type interface", + actual, + ) + } + + return typName(s.String()) == t.expected.(string), nil +} + +func (t *typMatcher) FailureMessage(actual interface{}) string { + return fmt.Sprintf("Expected %q is equal to %q", actual, t.expected) +} + +func (t *typMatcher) NegatedFailureMessage(actual interface{}) string { + return fmt.Sprintf("Expected %q is not equal to %q", actual, t.expected) +}