github.com/nikron/prototool@v1.3.0/internal/settings/config_provider.go (about) 1 // Copyright (c) 2018 Uber Technologies, Inc. 2 // 3 // Permission is hereby granted, free of charge, to any person obtaining a copy 4 // of this software and associated documentation files (the "Software"), to deal 5 // in the Software without restriction, including without limitation the rights 6 // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 // copies of the Software, and to permit persons to whom the Software is 8 // furnished to do so, subject to the following conditions: 9 // 10 // The above copyright notice and this permission notice shall be included in 11 // all copies or substantial portions of the Software. 12 // 13 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 // THE SOFTWARE. 20 21 package settings 22 23 import ( 24 "bytes" 25 "encoding/json" 26 "fmt" 27 "io/ioutil" 28 "os" 29 "path/filepath" 30 "sort" 31 "strings" 32 33 "github.com/uber/prototool/internal/strs" 34 "go.uber.org/zap" 35 "gopkg.in/yaml.v2" 36 ) 37 38 type configProvider struct { 39 logger *zap.Logger 40 } 41 42 func newConfigProvider(options ...ConfigProviderOption) *configProvider { 43 configProvider := &configProvider{ 44 logger: zap.NewNop(), 45 } 46 for _, option := range options { 47 option(configProvider) 48 } 49 return configProvider 50 } 51 52 func (c *configProvider) GetForDir(dirPath string) (Config, error) { 53 filePath, err := c.GetFilePathForDir(dirPath) 54 if err != nil { 55 return Config{}, err 56 } 57 if filePath == "" { 58 return Config{}, nil 59 } 60 return c.Get(filePath) 61 } 62 63 func (c *configProvider) GetFilePathForDir(dirPath string) (string, error) { 64 if !filepath.IsAbs(dirPath) { 65 return "", fmt.Errorf("%s is not an absolute path", dirPath) 66 } 67 return getFilePathForDir(filepath.Clean(dirPath)) 68 } 69 70 func (c *configProvider) Get(filePath string) (Config, error) { 71 if !filepath.IsAbs(filePath) { 72 return Config{}, fmt.Errorf("%s is not an absolute path", filePath) 73 } 74 filePath = filepath.Clean(filePath) 75 return get(filePath) 76 } 77 78 func (c *configProvider) GetForData(dirPath string, externalConfigData string) (Config, error) { 79 if !filepath.IsAbs(dirPath) { 80 return Config{}, fmt.Errorf("%s is not an absolute path", dirPath) 81 } 82 dirPath = filepath.Clean(dirPath) 83 var externalConfig ExternalConfig 84 if err := jsonUnmarshalStrict([]byte(externalConfigData), &externalConfig); err != nil { 85 return Config{}, err 86 } 87 return externalConfigToConfig(externalConfig, dirPath) 88 } 89 90 func (c *configProvider) GetExcludePrefixesForDir(dirPath string) ([]string, error) { 91 if !filepath.IsAbs(dirPath) { 92 return nil, fmt.Errorf("%s is not an absolute path", dirPath) 93 } 94 dirPath = filepath.Clean(dirPath) 95 return getExcludePrefixesForDir(dirPath) 96 } 97 98 func (c *configProvider) GetExcludePrefixesForData(dirPath string, externalConfigData string) ([]string, error) { 99 if !filepath.IsAbs(dirPath) { 100 return nil, fmt.Errorf("%s is not an absolute path", dirPath) 101 } 102 dirPath = filepath.Clean(dirPath) 103 var externalConfig ExternalConfig 104 if err := jsonUnmarshalStrict([]byte(externalConfigData), &externalConfig); err != nil { 105 return nil, err 106 } 107 return getExcludePrefixes(externalConfig.Excludes, dirPath) 108 } 109 110 // getFilePathForDir tries to find a file named by one of the ConfigFilenames starting in the 111 // given directory, and going up a directory until hitting root. 112 // 113 // The directory must be an absolute path. 114 // 115 // If no such file is found, "" is returned. 116 // If multiple files named by one of the ConfigFilenames are found in the same 117 // directory, error is returned. 118 func getFilePathForDir(dirPath string) (string, error) { 119 for { 120 filePath, err := getSingleFilePathForDir(dirPath) 121 if err != nil { 122 return "", err 123 } 124 if filePath != "" { 125 return filePath, nil 126 } 127 if dirPath == "/" { 128 return "", nil 129 } 130 dirPath = filepath.Dir(dirPath) 131 } 132 } 133 134 // getSingleFilePathForDir gets the file named by one of the ConfigFilenames in the 135 // given directory. Having multiple such files results in an error being returned. If no file is 136 // found, this returns "". 137 func getSingleFilePathForDir(dirPath string) (string, error) { 138 var filePaths []string 139 for _, configFilename := range ConfigFilenames { 140 filePath := filepath.Join(dirPath, configFilename) 141 if _, err := os.Stat(filePath); err == nil { 142 filePaths = append(filePaths, filePath) 143 } 144 } 145 switch len(filePaths) { 146 case 0: 147 return "", nil 148 case 1: 149 return filePaths[0], nil 150 default: 151 return "", fmt.Errorf("multiple configuration files in the same directory: %v", filePaths) 152 } 153 } 154 155 // get reads the config at the given path. 156 // 157 // This is expected to be in YAML or JSON format, which is denoted by the file extension. 158 func get(filePath string) (Config, error) { 159 externalConfig, err := getExternalConfig(filePath) 160 if err != nil { 161 return Config{}, err 162 } 163 return externalConfigToConfig(externalConfig, filepath.Dir(filePath)) 164 } 165 166 func getExternalConfig(filePath string) (ExternalConfig, error) { 167 data, err := ioutil.ReadFile(filePath) 168 if err != nil { 169 return ExternalConfig{}, err 170 } 171 if len(data) == 0 { 172 return ExternalConfig{}, nil 173 } 174 externalConfig := ExternalConfig{} 175 switch filepath.Ext(filePath) { 176 case ".json": 177 if err := jsonUnmarshalStrict(data, &externalConfig); err != nil { 178 return ExternalConfig{}, err 179 } 180 return externalConfig, nil 181 case ".yaml": 182 if err := yaml.UnmarshalStrict(data, &externalConfig); err != nil { 183 return ExternalConfig{}, err 184 } 185 return externalConfig, nil 186 default: 187 return ExternalConfig{}, fmt.Errorf("unknown config file extension, must be .json or .yaml: %s", filePath) 188 } 189 } 190 191 // externalConfigToConfig converts an ExternalConfig to a Config. 192 // 193 // This will return a valid Config, or an error. 194 func externalConfigToConfig(e ExternalConfig, dirPath string) (Config, error) { 195 excludePrefixes, err := getExcludePrefixes(e.Excludes, dirPath) 196 if err != nil { 197 return Config{}, err 198 } 199 includePaths := make([]string, 0, len(e.Protoc.Includes)) 200 for _, includePath := range strs.DedupeSort(e.Protoc.Includes, nil) { 201 if !filepath.IsAbs(includePath) { 202 includePath = filepath.Join(dirPath, includePath) 203 } 204 includePath = filepath.Clean(includePath) 205 includePaths = append(includePaths, includePath) 206 } 207 ignoreIDToFilePaths := make(map[string][]string) 208 for _, ignore := range e.Lint.Ignores { 209 id := strings.ToUpper(ignore.ID) 210 for _, protoFilePath := range ignore.Files { 211 if !filepath.IsAbs(protoFilePath) { 212 protoFilePath = filepath.Join(dirPath, protoFilePath) 213 } 214 protoFilePath = filepath.Clean(protoFilePath) 215 if _, ok := ignoreIDToFilePaths[id]; !ok { 216 ignoreIDToFilePaths[id] = make([]string, 0) 217 } 218 ignoreIDToFilePaths[id] = append(ignoreIDToFilePaths[id], protoFilePath) 219 } 220 } 221 222 genPlugins := make([]GenPlugin, len(e.Gen.Plugins)) 223 for i, plugin := range e.Gen.Plugins { 224 genPluginType, err := ParseGenPluginType(plugin.Type) 225 if err != nil { 226 return Config{}, err 227 } 228 if plugin.Output == "" { 229 return Config{}, fmt.Errorf("output path required for plugin %s", plugin.Name) 230 } 231 var relPath, absPath string 232 if filepath.IsAbs(plugin.Output) { 233 absPath = filepath.Clean(plugin.Output) 234 relPath, err = filepath.Rel(dirPath, absPath) 235 if err != nil { 236 return Config{}, fmt.Errorf("failed to resolve plugin %q output absolute path %q to a relative path with base %q: %v", plugin.Name, absPath, dirPath, err) 237 } 238 } else { 239 relPath = plugin.Output 240 absPath = filepath.Clean(filepath.Join(dirPath, relPath)) 241 } 242 genPlugins[i] = GenPlugin{ 243 Name: plugin.Name, 244 Path: plugin.Path, 245 Type: genPluginType, 246 Flags: plugin.Flags, 247 OutputPath: OutputPath{ 248 RelPath: relPath, 249 AbsPath: absPath, 250 }, 251 } 252 } 253 sort.Slice(genPlugins, func(i int, j int) bool { return genPlugins[i].Name < genPlugins[j].Name }) 254 255 createDirPathToBasePackage := make(map[string]string) 256 for _, pkg := range e.Create.Packages { 257 relDirPath := pkg.Directory 258 basePackage := pkg.Name 259 if relDirPath == "" { 260 return Config{}, fmt.Errorf("directory for create package is empty") 261 } 262 if basePackage == "" { 263 return Config{}, fmt.Errorf("name for create package is empty") 264 } 265 if filepath.IsAbs(relDirPath) { 266 return Config{}, fmt.Errorf("directory for create package must be relative: %s", relDirPath) 267 } 268 createDirPathToBasePackage[filepath.Clean(filepath.Join(dirPath, relDirPath))] = basePackage 269 } 270 // to make testing easier 271 if len(createDirPathToBasePackage) == 0 { 272 createDirPathToBasePackage = nil 273 } 274 275 config := Config{ 276 DirPath: dirPath, 277 ExcludePrefixes: excludePrefixes, 278 Compile: CompileConfig{ 279 ProtobufVersion: e.Protoc.Version, 280 IncludePaths: includePaths, 281 IncludeWellKnownTypes: true, // Always include the well-known types. 282 AllowUnusedImports: e.Protoc.AllowUnusedImports, 283 }, 284 Create: CreateConfig{ 285 DirPathToBasePackage: createDirPathToBasePackage, 286 }, 287 Lint: LintConfig{ 288 IncludeIDs: strs.DedupeSort(e.Lint.Rules.Add, strings.ToUpper), 289 ExcludeIDs: strs.DedupeSort(e.Lint.Rules.Remove, strings.ToUpper), 290 NoDefault: e.Lint.Rules.NoDefault, 291 IgnoreIDToFilePaths: ignoreIDToFilePaths, 292 }, 293 Gen: GenConfig{ 294 GoPluginOptions: GenGoPluginOptions{ 295 ImportPath: e.Gen.GoOptions.ImportPath, 296 ExtraModifiers: e.Gen.GoOptions.ExtraModifiers, 297 }, 298 Plugins: genPlugins, 299 }, 300 } 301 302 for _, genPlugin := range config.Gen.Plugins { 303 // TODO: technically protoc-gen-protoc-gen-foo is a valid 304 // plugin binary with name protoc-gen-foo, but do we want 305 // to error if protoc-gen- is a prefix of a name? 306 // I think this will be a common enough mistake that we 307 // can remove this later. Or, do we want names to include 308 // the protoc-gen- part? 309 if strings.HasPrefix(genPlugin.Name, "protoc-gen-") { 310 return Config{}, fmt.Errorf("plugin name provided was %s, do not include the protoc-gen- prefix", genPlugin.Name) 311 } 312 if _, ok := _genPluginTypeToString[genPlugin.Type]; !ok { 313 return Config{}, fmt.Errorf("unknown GenPluginType: %v", genPlugin.Type) 314 } 315 if (genPlugin.Type.IsGo() || genPlugin.Type.IsGogo()) && config.Gen.GoPluginOptions.ImportPath == "" { 316 return Config{}, fmt.Errorf("go plugin %s specified but no import path provided", genPlugin.Name) 317 } 318 } 319 320 if intersection := strs.Intersection(config.Lint.IncludeIDs, config.Lint.ExcludeIDs); len(intersection) > 0 { 321 return Config{}, fmt.Errorf("config had intersection of %v between lint_include and lint_exclude", intersection) 322 } 323 return config, nil 324 } 325 326 func getExcludePrefixesForDir(dirPath string) ([]string, error) { 327 filePath, err := getSingleFilePathForDir(dirPath) 328 if err != nil { 329 return nil, err 330 } 331 if filePath == "" { 332 return []string{}, nil 333 } 334 externalConfig, err := getExternalConfig(filePath) 335 if err != nil { 336 return nil, err 337 } 338 return getExcludePrefixes(externalConfig.Excludes, dirPath) 339 } 340 341 func getExcludePrefixes(excludes []string, dirPath string) ([]string, error) { 342 excludePrefixes := make([]string, 0, len(excludes)) 343 for _, excludePrefix := range strs.DedupeSort(excludes, nil) { 344 if !filepath.IsAbs(excludePrefix) { 345 excludePrefix = filepath.Join(dirPath, excludePrefix) 346 } 347 excludePrefix = filepath.Clean(excludePrefix) 348 if excludePrefix == dirPath { 349 return nil, fmt.Errorf("cannot exclude directory of config file: %s", dirPath) 350 } 351 if !strings.HasPrefix(excludePrefix, dirPath) { 352 return nil, fmt.Errorf("cannot exclude directory outside of config file directory %s: %s", dirPath, excludePrefix) 353 } 354 excludePrefixes = append(excludePrefixes, excludePrefix) 355 } 356 return excludePrefixes, nil 357 } 358 359 // jsonUnmarshalStrict makes sure there are no unknown fields when unmarshalling. 360 // This matches what yaml.UnmarshalStrict does basically. 361 // json.Unmarshal allows unknown fields. 362 func jsonUnmarshalStrict(data []byte, v interface{}) error { 363 decoder := json.NewDecoder(bytes.NewReader(data)) 364 decoder.DisallowUnknownFields() 365 return decoder.Decode(v) 366 }