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
46 changes: 43 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,9 @@ one or more structs have circular references.

To change a method name of deep copying, use `--method` option.

To use a configuration file instead of command-line flags, use `--config` option.
The configuration file should be in YAML format. See `config.example.yaml` for an example.

## Usage

Pass either path to the folder containing the types or the module name:
Expand All @@ -62,19 +65,56 @@ deep-copy <flags> /path/to/package/containing/type
deep-copy <flags> github.com/globusdigital/deep-copy
deep-copy <flags> github.com/globusdigital/deep-copy/some/sub/packages
```

Here is the full set of supported flags:

```bash
deep-copy \
[--config /path/to/config.yaml] \
[-o /output/path.go] \
[--method DeepCopy] \
[--pointer-receiver] \
[--skip Selector1,Selector.Two --skip Selector2[i], Selector.Three[k]]
[--type Type1 --type Type2\ \
[--tags mytag,anotherTag ] \ \
[--skip Selector1,Selector.Two --skip Selector2[i],Selector.Three[k]] \
[--type Type1 --type Type2] \
[--tags mytag,anotherTag] \
/path/to/package/containing/type
```

### Configuration File

Instead of using command-line flags, you can use a YAML configuration file:

```bash
deep-copy --config config.yaml /path/to/package/containing/type
```

Example configuration file (`config.example.yaml`):

```yaml
pointer-receiver: true
maxdepth: 5
method: DeepCopy
type:
- MyType
skip:
- Field1
- Field2
output: output.go
build-tags:
- custom
- build
```

All fields in the configuration file are optional.

**Priority order**: When both a configuration file and command-line flags are provided, the priority is as follows (highest to lowest):

1. **Command-line flags** (highest priority)
2. Configuration file values
3. Default values (lowest priority)

For example, if `config.yaml` contains `method: DeepCopy` and you run `deep-copy --config config.yaml --method Clone`, the `--method Clone` flag will take precedence, and `Clone` will be used as the method name.

## Example

Given the following types:
Expand Down
14 changes: 14 additions & 0 deletions config.example.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# yaml-language-server: $schema=config.schema.json

pointer-receiver: true
maxdepth: 5
method: DeepCopy
type:
- MyType
skip:
- Field1
- Field2
output: output.go
build-tags:
- custom
- build
86 changes: 86 additions & 0 deletions config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package main

import (
"flag"
"fmt"
"os"
"strings"

"gopkg.in/yaml.v3"
)

type config struct {
PointerReceiver *bool `yaml:"pointer-receiver,omitempty"`
MaxDepth *int `yaml:"maxdepth,omitempty"`
Method *string `yaml:"method,omitempty"`

Types []string `yaml:"type,omitempty"`
Skips []string `yaml:"skip,omitempty"`
OutputPath *string `yaml:"output,omitempty"`
BuildTags []string `yaml:"build-tags,omitempty"`
}

func loadConfig() error {
flagsSetOnCLI := make(map[string]struct{})
flag.Visit(func(f *flag.Flag) { flagsSetOnCLI[f.Name] = struct{}{} })
return loadConfigFile(strings.TrimSpace(*configFileF), flagsSetOnCLI)
}

// loadConfigFile reads YAML from path and merges into package-level flags.
// flagsSetOnCLI is the set of flag names that appeared on the command line; those are not overwritten from the file.
// An empty path is a no-op.
func loadConfigFile(configPath string, flagsSetOnCLI map[string]struct{}) error {
if strings.TrimSpace(configPath) == "" {
return nil
}

file, err := os.Open(configPath)
if err != nil {
return fmt.Errorf("opening config file: %w", err)
}
defer file.Close()

var cfg config
decoder := yaml.NewDecoder(file)
if err := decoder.Decode(&cfg); err != nil {
return fmt.Errorf("decoding config file: %w", err)
}

mergePtr(flagsSetOnCLI, "pointer-receiver", cfg.PointerReceiver, pointerReceiverF)
mergePtr(flagsSetOnCLI, "maxdepth", cfg.MaxDepth, maxDepthF)
mergePtr(flagsSetOnCLI, "method", cfg.Method, methodF)

if len(cfg.Types) > 0 && !flagWasSetOnCLI(flagsSetOnCLI, "type") {
typesF = typesVal(cfg.Types)
}
if len(cfg.Skips) > 0 && !flagWasSetOnCLI(flagsSetOnCLI, "skip") {
skipsF = skipsVal{}
for _, skip := range cfg.Skips {
if err := skipsF.Set(skip); err != nil {
return fmt.Errorf("parsing skip value: %w", err)
}
}
}
if cfg.OutputPath != nil && !flagWasSetOnCLI(flagsSetOnCLI, "o") {
if err := outputF.Set(*cfg.OutputPath); err != nil {
return fmt.Errorf("setting output: %w", err)
}
}
if len(cfg.BuildTags) > 0 && !flagWasSetOnCLI(flagsSetOnCLI, "tags") {
buildTagsF = buildTagsVal(cfg.BuildTags)
}

return nil
}

func flagWasSetOnCLI(flagsSetOnCLI map[string]struct{}, name string) bool {
_, ok := flagsSetOnCLI[name]
return ok
}

func mergePtr[T any](flagsSetOnCLI map[string]struct{}, name string, src, dst *T) {
if src == nil || flagWasSetOnCLI(flagsSetOnCLI, name) {
return
}
*dst = *src
}
48 changes: 48 additions & 0 deletions config.schema.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "https://github.com/globusdigital/deep-copy/config.schema.json",
"title": "Deep Copy Configuration Schema",
"description": "Configuration schema for deep-copy tool",
"type": "object",
"properties": {
"pointer-receiver": {
"type": "boolean",
"description": "Specify whether to use a pointer receiver for the generated method. The flag also governs whether the return type is a pointer as well."
},
"maxdepth": {
"type": "integer",
"description": "Specify the maximum depth of deep copying. It stops deep copying at a given depth, with a warning message indicating where the deep copying has been stopped. This is especially useful when one or more structs have circular references.",
"minimum": 0
},
"method": {
"type": "string",
"description": "Change the method name of deep copying. Defaults to 'DeepCopy'."
},
"type": {
"type": "array",
"description": "List of type names to generate deep copy methods for. Multiple types can be specified for the given package.",
"items": {
"type": "string"
}
},
"skip": {
"type": "array",
"description": "field/slice/map selectors to shallow copy instead of deep copy, one YAML string per entry. Within each string, use commas to separate multiple selectors (same as repeated --skip flags on the CLI). Match the number of entries to the number of types when using multiple types. Use field selectors like 'B' to skip a field, 'B.I' to skip an inner field, '[i]' for slice members, and '[k]' for map members.",
"items": {
"type": "string"
}
},
"output": {
"type": "string",
"description": "Output file path for the generated code. Defaults to STDOUT if not specified."
},
"build-tags": {
"type": "array",
"description": "Build tags to add to the generated code file (one tag per array element; same as repeating --tags on the CLI).",
"items": {
"type": "string"
}
}
},
"additionalProperties": false
}
Loading
Loading