Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 23 additions & 9 deletions generators/xml/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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: <postgresql_type>:<go_import>.<go_type>
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 файл
Expand Down
164 changes: 109 additions & 55 deletions generators/xml/generator.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.*"}
}
}
Expand All @@ -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 {
Expand All @@ -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
}
}
}

Expand All @@ -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
Expand Down
115 changes: 115 additions & 0 deletions generators/xml/generator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down