diff --git a/generators/xml/README.md b/generators/xml/README.md index b7e7d29..0fe5816 100644 --- a/generators/xml/README.md +++ b/generators/xml/README.md @@ -17,22 +17,36 @@ mfd-generator xml -h Create or update project base with namespaces and entities Usage: - mfd xml [flags] + mfd-generator xml [flags] Flags: - -v, --verbose print sql queries - -c, --conn string connection string to postgres database, e.g. postgres://usr:pwd@localhost:5432/db - -m, --mfd string mfd file path - -t, --tables strings table names for model generation separated by comma - use 'schema_name.*' to generate model for every table in model (default [public.*]) - -n, --namespaces string use this parameter to set table & namespace in format "users=users,projects;shop=orders,prices" - -p, --print print namespace - tables association - -h, --help help for xml + -v, --verbose print sql queries + -c, --conn string connection string to postgres database, e.g. postgres://usr:pwd@localhost:5432/db + -m, --mfd string mfd file path + -t, --tables strings table names for model generation separated by comma + use 'schema_name.*' to generate model for every table in model + -n, --namespaces string use this parameter to set table & namespace in format "users=users,projects;shop=orders,prices" + -g, --gopgver int go-pg version (default 9) + -q, --quiet string quiet mode. ignored when --namespaces (-n) flag is set. possible values: + - all - will use namespace entity mapping from mfd, entities not present in mfd file will be ignored + - new - generator will prompt namespace for entities not present in mfd file + --custom-types strings set custom types separated by comma + format: :. + examples: uuid:github.com/google/uuid.UUID,point:src/model.Point,bytea:string + + -p, --print print namespace - tables association + -h, --help help for xml ``` `-t, --tables` - позволяет вводить исходные таблицы для генератора через запятую, если не указана схема для таблицы, то будет использоваться public. `*` - для генерирования всех таблиц в схеме, например: `public.*,geo.locations,geo.cities` `-n, --namespaces` - сайлент-режим, позволяет задать ассоциацию неймспейс - таблица. Формат; `namespace1=table1,table2;namespace2=table3,table4`, флаг имеет приоритет над внутренней таблицей TableMapping в заполнении Packages +`-q, --quiet` - тихий режим: позволяет запускать генерацию без интерактивного выбора неймспейсов. Игнорируется, если задан `-n, --namespaces`. Если `-n` не задан, режим работает вместе с `TableMapping` из mfd файла. Возможные значения: +- `all` - берёт неймспейсы только из `TableMapping` и из уже существующих в проекте сущностей. Таблицы, которых там нет, пропускаются и в проект не добавляются. Ничего не спрашивает. +- `new` - тоже берёт неймспейсы из `TableMapping` и существующих сущностей, но для новых таблиц (которых нет ни в маппинге, ни в проекте) спрашивает неймспейс интерактивно. + +Если не задан ни `-q`, ни `-n`, генератор спрашивает неймспейс для каждой таблицы. Для `-q new` без явного `-t` читаются все таблицы (`public.*`) — иначе для новых таблиц не у чего было бы спросить неймспейс. + `-p, --print` - на основе загруженного проекта выводит ассоциации неймспейс - таблица в формате, подходящем для флага `-n, --namespaces`. Не запускает генератор ### MFD файл diff --git a/generators/xml/generator.go b/generators/xml/generator.go index c84403f..c0a79c2 100644 --- a/generators/xml/generator.go +++ b/generators/xml/generator.go @@ -168,6 +168,54 @@ func parseNamespacesFlag(v string) map[string]string { return mp } +type nsAction int + +const ( + nsAssign nsAction = iota // namespace is decided — use it as is + nsSkip // table must be skipped + nsPrompt // namespace must be asked interactively +) + +// decideNamespace resolves the target namespace for a table from the configured +// mode (-n / -q), the mfd TableMapping and the existing project state. It performs +// no IO: nsPrompt signals that the caller must ask the user interactively. +func decideNamespace(opts Options, tableMapping map[string]string, table, existingNS string, hasExisting bool) (string, nsAction) { + // -n preset: strict mapping, skip everything not listed. + if opts.Packages != nil { + if ns, ok := opts.Packages[table]; ok { + return ns, nsAssign + } + return "", nsSkip + } + + // -n is not set: consult TableMapping from mfd combined with --quiet mode. + mappedNS, mappedOK := tableMapping[table] + + switch opts.Quiet { + case quietAll: + switch { + case mappedOK: + return mappedNS, nsAssign + case hasExisting: + return existingNS, nsAssign + default: + return "", nsSkip + } + case quietNew: + switch { + case mappedOK: + return mappedNS, nsAssign + case hasExisting: + return existingNS, nsAssign + default: + return "", nsPrompt + } + default: + // no --quiet: prompt for every table. + return "", nsPrompt + } +} + // Generate runs generator func (g *Generator) Generate() (err error) { var logger *log.Logger @@ -190,15 +238,26 @@ func (g *Generator) Generate() (err error) { return nil } - if g.options.Packages == nil { - // if options.Packages is nil check TableMapping.Packages - g.options.Packages = project.TableMapping.Packages() - // fill tables from namespaces if not set - if len(g.options.Tables) == 0 && len(g.options.Packages) != 0 { + // tableMapping is the namespace mapping from the mfd file (TableMapping section). + // it is consulted only when -n is not set, in combination with --quiet mode. + tableMapping := project.TableMapping.Packages() + + // fill tables from db source when not explicitly set via -t. + if len(g.options.Tables) == 0 { + switch { + case g.options.Packages != nil: + // -n is set: read only listed tables. for table := range g.options.Packages { g.options.Tables = append(g.options.Tables, table) } - } else if len(g.options.Tables) == 0 { + case g.options.Quiet == quietAll && len(tableMapping) > 0: + // quiet=all relies entirely on existing mapping/entities; no point + // reading tables that would be skipped anyway. + for table := range tableMapping { + g.options.Tables = append(g.options.Tables, table) + } + default: + // quiet=new or default: read all tables so the prompt can run for new ones. g.options.Tables = []string{"public.*"} } } @@ -211,6 +270,36 @@ func (g *Generator) Generate() (err error) { return fmt.Errorf("read database, err=%w", err) } + if err = g.fillNamespaces(project, entities, addedCustomTypes); err != nil { + return err + } + + // suggesting searches && fk links + project.SuggestArrayLinks() + project.UpdateLinks() + + // validate names + if err := project.ValidateNames(); err != nil { + return err + } + + if err := project.IsConsistent(); err != nil { + return fmt.Errorf("%w. fk table should be either be in project or selected for generatation", err) + } + + // saving mfd file + if err = mfd.SaveMFD(g.options.Output, project); err != nil { + return err + } + + return mfd.SaveProjectXML(g.options.Output, project) +} + +// fillNamespaces resolves a namespace for every entity read from the database +// (prompting the user when decideNamespace requires it) and adds it to the project. +func (g *Generator) fillNamespaces(project *mfd.Project, entities []model.Entity, addedCustomTypes mfd.CustomTypes) (err error) { + tableMapping := project.TableMapping.Packages() + set := mfd.NewSet() // filling set for _, namespace := range project.Namespaces { @@ -219,42 +308,25 @@ func (g *Generator) Generate() (err error) { for _, entity := range entities { exiting := project.EntityByTable(entity.PGFullName) + existingNS := "" if exiting != nil { + existingNS = exiting.Namespace set.Prepend(exiting.Namespace) } - var namespace string - - if g.options.Packages != nil { - // getting namespace from preset - var ok bool - if namespace, ok = g.options.Packages[entity.PGFullName]; !ok { - continue + namespace, action := decideNamespace(g.options, tableMapping, entity.PGFullName, existingNS, exiting != nil) + switch action { + case nsSkip: + continue // loop + case nsPrompt: + // asking namespace from prompt + if namespace, err = g.PromptNS(entity.PGFullName, set.Elements()); err != nil { + // may happen only in ctrl+c + return fmt.Errorf("prompt namespace, err=%w", err) } - } else { - switch g.options.Quiet { - case quietAll: - if exiting != nil { - namespace = exiting.Namespace - break // case - } + // if user choose to skip + if namespace == "skip" { continue // loop - case quietNew: - if exiting != nil { - namespace = exiting.Namespace - break // case - } - fallthrough // to default - default: - // asking namespace from prompt - if namespace, err = g.PromptNS(entity.PGFullName, set.Elements()); err != nil { - // may happen only in ctrl+c - return fmt.Errorf("prompt namespace, err=%w", err) - } - // if user choose to skip - if namespace == "skip" { - continue // loop - } } } @@ -265,25 +337,7 @@ func (g *Generator) Generate() (err error) { project.AddEntity(namespace, PackEntity(namespace, entity, exiting, addedCustomTypes)) } - // suggesting searches && fk links - project.SuggestArrayLinks() - project.UpdateLinks() - - // validate names - if err := project.ValidateNames(); err != nil { - return err - } - - if err := project.IsConsistent(); err != nil { - return fmt.Errorf("%w. fk table should be either be in project or selected for generatation", err) - } - - // saving mfd file - if err = mfd.SaveMFD(g.options.Output, project); err != nil { - return err - } - - return mfd.SaveProjectXML(g.options.Output, project) + return nil } // PromptNS prompting namespace in console diff --git a/generators/xml/generator_test.go b/generators/xml/generator_test.go index bb45717..365f569 100644 --- a/generators/xml/generator_test.go +++ b/generators/xml/generator_test.go @@ -64,6 +64,121 @@ func TestGenerator_Generate(t *testing.T) { }) } +// TestDecideNamespace checks the pure namespace-resolution logic that drives +// --quiet (-q) and --namespaces (-n). It replaces the previous DB-backed test +// that swapped an interactive-prompt seam on the Generator: every invariant the +// seam used to verify (which tables are mapped, skipped or prompted, and that -n +// overrides -q/TableMapping) is now asserted directly, with no DB and no mocks. +func TestDecideNamespace(t *testing.T) { + // tableMapping mirrors a project where news/categories/tags are pre-mapped to "portal". + tableMapping := map[string]string{ + "news": "portal", + "categories": "portal", + "tags": "portal", + } + + tests := []struct { + name string + opts Options + table string + existingNS string + hasExisting bool + wantNS string + wantAction nsAction + }{ + { + name: "-q new: mapped table is assigned silently", + opts: Options{Quiet: quietNew}, + table: "news", + wantNS: "portal", + wantAction: nsAssign, + }, + { + name: "-q new: unmapped table already in project keeps its namespace", + opts: Options{Quiet: quietNew}, + table: "comments", + existingNS: "blog", + hasExisting: true, + wantNS: "blog", + wantAction: nsAssign, + }, + { + name: "-q new: brand new table must be prompted", + opts: Options{Quiet: quietNew}, + table: "vfsFiles", + wantAction: nsPrompt, + }, + { + name: "-q all: mapped table is assigned", + opts: Options{Quiet: quietAll}, + table: "news", + wantNS: "portal", + wantAction: nsAssign, + }, + { + name: "-q all: unmapped table already in project keeps its namespace", + opts: Options{Quiet: quietAll}, + table: "comments", + existingNS: "blog", + hasExisting: true, + wantNS: "blog", + wantAction: nsAssign, + }, + { + name: "-q all: unmapped new table is skipped", + opts: Options{Quiet: quietAll}, + table: "vfsFolders", + wantAction: nsSkip, + }, + { + name: "-n: listed table is assigned", + opts: Options{Packages: map[string]string{"encryptionKeys": "custom"}}, + table: "encryptionKeys", + wantNS: "custom", + wantAction: nsAssign, + }, + { + name: "-n: table missing from preset is skipped", + opts: Options{Packages: map[string]string{"encryptionKeys": "custom"}}, + table: "vfsFolders", + wantAction: nsSkip, + }, + { + name: "-n overrides TableMapping", + opts: Options{Quiet: quietNew, Packages: map[string]string{"news": "custom"}}, + table: "news", + wantNS: "custom", + wantAction: nsAssign, + }, + { + name: "default (no -q/-n): new table is prompted", + opts: Options{}, + table: "news", + wantAction: nsPrompt, + }, + { + name: "default (no -q/-n): even an existing table is prompted", + opts: Options{}, + table: "comments", + existingNS: "blog", + hasExisting: true, + wantAction: nsPrompt, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + gotNS, gotAction := decideNamespace(tt.opts, tableMapping, tt.table, tt.existingNS, tt.hasExisting) + if gotAction != tt.wantAction { + t.Errorf("action = %d, want %d", gotAction, tt.wantAction) + } + if gotNS != tt.wantNS { + t.Errorf("namespace = %q, want %q", gotNS, tt.wantNS) + } + }) + } +} + func helperLoadBytes(t *testing.T, path string) []byte { bytes, err := os.ReadFile(path) if err != nil {