1// Copyright 2018 The CUE Authors
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15package cmd
16
17import (
18	"errors"
19	"fmt"
20	"io/ioutil"
21	"os"
22
23	"github.com/spf13/cobra"
24
25	"cuelang.org/go/cue/format"
26	"cuelang.org/go/cue/load"
27	"cuelang.org/go/internal/diff"
28	"cuelang.org/go/tools/trim"
29)
30
31// TODO:
32// - remove the limitations mentioned in the documentation
33// - implement verification post-processing as extra safety
34
35// newTrimCmd creates a trim command
36func newTrimCmd(c *Command) *cobra.Command {
37	cmd := &cobra.Command{
38		Use:   "trim",
39		Short: "remove superfluous fields",
40		Long: `trim removes fields from structs that can be inferred from constraints
41
42A field, struct, or list is removed if it is implied by a constraint, such
43as from an optional field matching a required field, a list type value,
44a comprehension or any other implied content. It will modify the files in place.
45
46
47Limitations
48
49Removal is on a best effort basis. Some caveats:
50- Fields in implied content may refer to fields within the struct in which
51  they are included, but are only resolved on a best-effort basis.
52- Disjunctions that contain structs in implied content cannot be used to
53  remove fields.
54- There is currently no verification step: manual verification is required.
55
56Examples:
57
58	$ cat <<EOF > foo.cue
59	light: [string]: {
60		room:          string
61		brightnessOff: *0.0 | >=0 & <=100.0
62		brightnessOn:  *100.0 | >=0 & <=100.0
63	}
64
65	light: ceiling50: {
66		room:          "MasterBedroom"
67		brightnessOff: 0.0    // this line
68		brightnessOn:  100.0  // and this line will be removed
69	}
70	EOF
71
72	$ cue trim foo.cue
73	$ cat foo.cue
74	light: [string]: {
75		room:          string
76		brightnessOff: *0.0 | >=0 & <=100.0
77		brightnessOn:  *100.0 | >=0 & <=100.0
78	}
79
80	light: ceiling50: {
81		room: "MasterBedroom"
82	}
83
84It is guaranteed that the resulting files give the same output as before the
85removal.
86`,
87		RunE: mkRunE(c, runTrim),
88	}
89
90	addOutFlags(cmd.Flags(), false)
91
92	return cmd
93}
94
95func runTrim(cmd *Command, args []string) error {
96	binst := loadFromArgs(cmd, args, nil)
97	if binst == nil {
98		return nil
99	}
100	instances := buildInstances(cmd, binst)
101
102	dst := flagOutFile.String(cmd)
103	if dst != "" && dst != "-" && !flagForce.Bool(cmd) {
104		switch _, err := os.Stat(dst); {
105		case os.IsNotExist(err):
106		default:
107			return fmt.Errorf("error writing %q: file already exists", dst)
108		}
109	}
110
111	overlay := map[string]load.Source{}
112
113	for i, inst := range binst {
114		root := instances[i]
115		err := trim.Files(inst.Files, root, &trim.Config{
116			Trace: flagTrace.Bool(cmd),
117		})
118		if err != nil {
119			return err
120		}
121
122		for _, f := range inst.Files {
123			overlay[f.Filename] = load.FromFile(f)
124		}
125
126	}
127
128	cfg := *defaultConfig.loadCfg
129	cfg.Overlay = overlay
130	tinsts := buildInstances(cmd, load.Instances(args, &cfg))
131	if len(tinsts) != len(binst) {
132		return errors.New("unexpected number of new instances")
133	}
134	if !flagIgnore.Bool(cmd) {
135		for i, p := range instances {
136			k, script := diff.Final.Diff(p.Value(), tinsts[i].Value())
137			if k != diff.Identity {
138				diff.Print(os.Stdout, script)
139				fmt.Println("Aborting trim, output differs after trimming. This is a bug! Use -i to force trim.")
140				fmt.Println("You can file a bug here: https://github.com/cuelang/cue/issues/new?assignees=&labels=NeedsInvestigation&template=bug_report.md&title=")
141				os.Exit(1)
142			}
143		}
144	}
145
146	if flagDryrun.Bool(cmd) {
147		return nil
148	}
149
150	for _, inst := range binst {
151		for _, f := range inst.Files {
152			filename := f.Filename
153
154			opts := []format.Option{}
155			if flagSimplify.Bool(cmd) {
156				opts = append(opts, format.Simplify())
157			}
158
159			b, err := format.Node(f, opts...)
160			if err != nil {
161				return fmt.Errorf("error formatting file: %v", err)
162			}
163
164			if dst == "-" {
165				_, err := cmd.OutOrStdout().Write(b)
166				if err != nil {
167					return err
168				}
169				continue
170			} else if dst != "" {
171				filename = dst
172			}
173
174			err = ioutil.WriteFile(filename, b, 0644)
175			if err != nil {
176				return err
177			}
178		}
179	}
180	return nil
181}
182