Skip to content

Commit

Permalink
modules: tt.yaml modules/directory support list
Browse files Browse the repository at this point in the history
Closes #1012

@TarantoolBot document
Title: Allow multiple directories with modules

The logic of working with configuration file tt.yaml has been updated,
now  modules/directory can be specified both as a single line and
as a list.

**Usage example**
Old behavior:
```yaml
modules:
  directory: modules
```
Possibility to specify a list of directories:
```yaml
modules:
  directory:
  - modules
  - /ext/path/modules
  - other_modules
```

If a relative path is specified, the search is performed in
the subfolders below, relative to the tt.yaml file. If an absolute
path is specified, external modules are searched according to
the specified path.
  • Loading branch information
dmyger committed Dec 6, 2024
1 parent cdf2d13 commit 8d60a6a
Show file tree
Hide file tree
Showing 21 changed files with 520 additions and 36 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.

### Added

- `tt.yaml`: allows to specify a list of modules directories.

### Changed

### Fixed
Expand Down
38 changes: 38 additions & 0 deletions cli/cfg/dump_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,44 @@ ee:
credential_path: ""
templates:
- path: %[1]s/templates
repo:
rocks: ""
distfiles: %[1]s/distfiles
`, configDir),
wantErr: false,
},
{
name: "Config dump with list modules",
args: args{
&cmdcontext.CmdCtx{
Cli: cmdcontext.CliCtx{
ConfigPath: "./testdata/tt_cfg3.yaml",
},
},
&DumpCtx{RawDump: false},
getCliOpts(t, "testdata/tt_cfg3.yaml"),
},
wantWriter: fmt.Sprintf(`./testdata/tt_cfg3.yaml:
env:
bin_dir: %[1]s/bin
inc_dir: %[1]s/include
instances_enabled: %[1]s
restart_on_failure: false
tarantoolctl_layout: false
modules:
directory:
- /root/modules
- /some/other/modules
app:
run_dir: var/run
log_dir: var/log
wal_dir: var/lib
memtx_dir: var/lib
vinyl_dir: var/lib
ee:
credential_path: ""
templates:
- path: %[1]s/templates
repo:
rocks: ""
distfiles: %[1]s/distfiles
Expand Down
4 changes: 4 additions & 0 deletions cli/cfg/testdata/tt_cfg3.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
modules:
directory:
- /root/modules
- /some/other/modules
4 changes: 2 additions & 2 deletions cli/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,9 @@ package config

// ModuleOpts is used to store all module options.
type ModulesOpts struct {
// Directory is a path to directory where the external modules
// Directories is a list of paths to directories where the external modules
// are stored.
Directory string
Directories FieldStringArrayType `mapstructure:"directory" yaml:"directory"`
}

// EEOpts is used to store tarantool-ee options.
Expand Down
68 changes: 68 additions & 0 deletions cli/config/single_or_array.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package config

import (
"encoding/json"

"gopkg.in/yaml.v3"
)

// SingleOrArray is a helper type for flexible customization of fields that can contain either
// a single value or a list of values, of the same original data type.
//
// Solution based on: https://gist.github.com/SVilgelm/0854d06308e36228857d08571d20aaf1
type SingleOrArray[T any] []T

// NewSingleOrArray creates SingleOrArray object.
func NewSingleOrArray[T any](v ...T) SingleOrArray[T] {
return append([]T{}, v...)
}

// UnmarshalJSON implements json.Unmarshaler interface.
func (o *SingleOrArray[T]) UnmarshalJSON(data []byte) error {
var ret []T
if json.Unmarshal(data, &ret) != nil {
var s T
if err := json.Unmarshal(data, &s); err != nil {
return err
}
ret = []T{s}
}
*o = ret
return nil
}

// MarshalJSON implements json.Marshaler interface.
func (o SingleOrArray[T]) MarshalJSON() ([]byte, error) {
if len(o) == 1 {
return json.Marshal(o[0])
}
return json.Marshal([]T(o))
}

// UnmarshalYAML implements yaml.Unmarshaler interface.
func (o *SingleOrArray[T]) UnmarshalYAML(node *yaml.Node) error {
var ret []T
if node.Decode(&ret) != nil {
var s T
if err := node.Decode(&s); err != nil {
return err
}
ret = []T{s}
}
*o = ret
return nil
}

// MarshalYAML implements yaml.Marshaler interface.
func (o SingleOrArray[T]) MarshalYAML() (any, error) {
var v any
v = []T(o)
if len(o) == 1 {
v = o[0]
}
return v, nil
}

// FieldStringArrayType is alias for the custom type used `SingleOrArray` with strings
// to handle as a single string as well as a list of strings.
type FieldStringArrayType = SingleOrArray[string]
248 changes: 248 additions & 0 deletions cli/config/single_or_array_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,248 @@
package config_test

import (
"bytes"
"encoding/json"
"testing"

"github.com/stretchr/testify/require"
"github.com/tarantool/tt/cli/config"
"gopkg.in/yaml.v3"
)

type singleOrArrayCase[T any] struct {
name string
data []byte
expected config.SingleOrArray[T]
wantErr bool
}

func testSingleOrArrayJSON[T any](t *testing.T, tests []singleOrArrayCase[T]) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()

var o config.SingleOrArray[T]
err := json.Unmarshal(tt.data, &o)
if tt.wantErr {
require.Error(t, err)
return
} else {
require.NoError(t, err)
require.Equal(t, tt.expected, o)
}
newData, err := json.Marshal(&o)
require.NoError(t, err)
require.Equal(t, tt.data, newData)
})
}
}

func TestSingleOrArrayJSON(t *testing.T) {
t.Run("string", func(t *testing.T) {
testSingleOrArrayJSON(t, []singleOrArrayCase[string]{
{
name: "single",
data: []byte(`"single"`),
expected: config.NewSingleOrArray("single"),
},
{
name: "multi",
data: []byte(`["first","second"]`),
expected: config.NewSingleOrArray("first", "second"),
},
{
name: "null",
data: []byte(`null`),
},
{
name: "int for string",
data: []byte(`42`),
wantErr: true,
},
{
name: "array of int for string",
data: []byte(`[42, 103]`),
wantErr: true,
},
{
name: "empty for string",
data: []byte(``),
wantErr: true,
},
})
})

t.Run("int", func(t *testing.T) {
testSingleOrArrayJSON(t, []singleOrArrayCase[int]{
{
name: "single",
data: []byte(`1`),
expected: config.NewSingleOrArray(1),
},
{
name: "multi",
data: []byte(`[1,2]`),
expected: config.NewSingleOrArray(1, 2),
},
{
name: "null",
data: []byte(`null`),
},
{
name: "string for int",
data: []byte(`"single"`),
wantErr: true,
},
{
name: "array of string for int",
data: []byte(`["first","second"]`),
wantErr: true,
},
{
name: "empty for int",
data: []byte(``),
wantErr: true,
},
})
})

type Foo struct {
A string
B int
}
t.Run("struct", func(t *testing.T) {
testSingleOrArrayJSON(t, []singleOrArrayCase[Foo]{
{
name: "single",
data: []byte(`{"A":"single","B":42}`),
expected: config.NewSingleOrArray(Foo{A: "single", B: 42}),
},
{
name: "multi",
data: []byte(`[{"A":"first","B":1},{"A":"second","B":2}]`),
expected: config.NewSingleOrArray(Foo{A: "first", B: 1}, Foo{A: "second", B: 2}),
},
{
name: "null",
data: []byte(`null`),
},
{
name: "string for struct",
data: []byte(`"single"`),
wantErr: true,
},
{
name: "array of string for struct",
data: []byte(`["first","second"]`),
wantErr: true,
},
{
name: "empty for struct",
data: []byte(``),
wantErr: true,
},
})
})
}

func testSingleOrArrayYAML[T any](t *testing.T, tests []singleOrArrayCase[T]) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()

var o config.SingleOrArray[T]
err := yaml.Unmarshal(tt.data, &o)
if tt.wantErr {
require.Error(t, err)
return
} else {
require.NoError(t, err)
require.Equal(t, tt.expected, o)
}
newData, err := yaml.Marshal(&o)
newData = bytes.TrimSpace(newData)
require.NoError(t, err)
require.Equal(t, tt.data, newData)
})
}
}

func TestSingleOrArrayYAML(t *testing.T) {
t.Run("string", func(t *testing.T) {
testSingleOrArrayYAML(t, []singleOrArrayCase[string]{
{
name: "single",
data: []byte(`single`),
expected: config.NewSingleOrArray("single"),
},
{
name: "multi",
data: []byte(`- first
- second`),
expected: config.NewSingleOrArray("first", "second"),
},
})
})

t.Run("int", func(t *testing.T) {
testSingleOrArrayYAML(t, []singleOrArrayCase[int]{
{
name: "single",
data: []byte(`1`),
expected: config.NewSingleOrArray(1),
},
{
name: "multi",
data: []byte(`- 1
- 2`),
expected: config.NewSingleOrArray(1, 2),
},
{
name: "string for int",
data: []byte(`single`),
wantErr: true,
},
{
name: "array of string for int",
data: []byte(`- first
- second`),
wantErr: true,
},
})
})

type Foo struct {
A string
B int
}
t.Run("struct", func(t *testing.T) {
testSingleOrArrayYAML(t, []singleOrArrayCase[Foo]{
{
name: "single",
data: []byte(`a: single
b: 42`),
expected: config.NewSingleOrArray(Foo{A: "single", B: 42}),
},
{
name: "multi",
data: []byte(`- a: first
b: 1
- a: second
b: 2`),
expected: config.NewSingleOrArray(Foo{A: "first", B: 1}, Foo{A: "second", B: 2}),
},
{
name: "string for struct",
data: []byte(`single`),
wantErr: true,
},
{
name: "array of string for struct",
data: []byte(`- first
- second`),
wantErr: true,
},
})
})
}
Loading

0 comments on commit 8d60a6a

Please sign in to comment.