github.com/avenga/couper@v1.12.2/config/generate/main.go (about) 1 //go:build exclude 2 3 package main 4 5 import ( 6 "bufio" 7 "bytes" 8 "encoding/json" 9 "fmt" 10 "net/url" 11 "os" 12 "path/filepath" 13 "reflect" 14 "regexp" 15 "sort" 16 "strings" 17 18 "github.com/algolia/algoliasearch-client-go/v3/algolia/search" 19 20 "github.com/avenga/couper/config" 21 "github.com/avenga/couper/config/meta" 22 ) 23 24 type entry struct { 25 Attributes []interface{} `json:"attributes"` 26 Blocks []interface{} `json:"blocks"` 27 Description string `json:"description"` 28 ID string `json:"objectID"` 29 Name string `json:"name"` 30 Type string `json:"type"` 31 URL string `json:"url"` 32 } 33 34 type attr struct { 35 Default string `json:"default"` 36 Description string `json:"description"` 37 Name string `json:"name"` 38 Type string `json:"type"` 39 } 40 41 type block struct { 42 Description string `json:"description"` 43 Name string `json:"name"` 44 } 45 46 const ( 47 searchAppID = "MSIN2HU7WH" 48 searchIndex = "docs" 49 searchClientKey = "SEARCH_CLIENT_API_KEY" 50 51 configurationPath = "docs/website/content/2.configuration" 52 docsBlockPath = configurationPath + "/4.block" 53 54 urlBasePath = "/configuration/" 55 ) 56 57 // export md: 1) search for ::attribute, replace if exist or append at end 58 func main() { 59 60 client := search.NewClient(searchAppID, os.Getenv(searchClientKey)) 61 index := client.InitIndex(searchIndex) 62 63 filenameRegex := regexp.MustCompile(`(URL|JWT|OpenAPI|[a-z0-9]+)`) 64 bracesRegex := regexp.MustCompile(`{([^}]*)}`) 65 66 attributesMap := map[string][]reflect.StructField{ 67 "RequestHeadersAttributes": newFields(&meta.RequestHeadersAttributes{}), 68 "ResponseHeadersAttributes": newFields(&meta.ResponseHeadersAttributes{}), 69 "FormParamsAttributes": newFields(&meta.FormParamsAttributes{}), 70 "QueryParamsAttributes": newFields(&meta.QueryParamsAttributes{}), 71 "LogFieldsAttribute": newFields(&meta.LogFieldsAttribute{}), 72 } 73 74 blockNamesMap := map[string]string{ 75 "oauth2_ac": "beta_oauth2", 76 "oauth2_req_auth": "oauth2", 77 } 78 79 processedFiles := make(map[string]struct{}) 80 81 for _, impl := range []interface{}{ 82 &config.API{}, 83 &config.Backend{}, 84 &config.BackendTLS{}, 85 &config.BasicAuth{}, 86 &config.CORS{}, 87 &config.Defaults{}, 88 &config.Definitions{}, 89 &config.Endpoint{}, 90 &config.ErrorHandler{}, 91 &config.Files{}, 92 &config.Health{}, 93 &config.JWTSigningProfile{}, 94 &config.JWT{}, 95 &config.Job{}, 96 &config.OAuth2AC{}, 97 &config.OAuth2ReqAuth{}, 98 &config.OIDC{}, 99 &config.OpenAPI{}, 100 &config.Proxy{}, 101 &config.RateLimit{}, 102 &config.Request{}, 103 &config.Response{}, 104 &config.SAML{}, 105 &config.Server{}, 106 &config.ClientCertificate{}, 107 &config.ServerCertificate{}, 108 &config.ServerTLS{}, 109 &config.Settings{}, 110 &config.Spa{}, 111 &config.TokenRequest{}, 112 &config.Websockets{}, 113 } { 114 t := reflect.TypeOf(impl).Elem() 115 name := reflect.TypeOf(impl).String() 116 name = strings.TrimPrefix(name, "*config.") 117 blockName := strings.ToLower(strings.Trim(filenameRegex.ReplaceAllString(name, "${1}_"), "_")) 118 119 if _, exists := blockNamesMap[blockName]; exists { 120 blockName = blockNamesMap[blockName] 121 } 122 123 urlPath, _ := url.JoinPath(urlBasePath, "block", blockName) 124 result := entry{ 125 Name: blockName, 126 URL: strings.ToLower(urlPath), 127 Type: "block", 128 } 129 130 result.ID = result.URL 131 132 var fields []reflect.StructField 133 fields = collectFields(t, fields) 134 135 inlineType, ok := impl.(config.Inline) 136 if ok { 137 it := reflect.TypeOf(inlineType.Inline()).Elem() 138 for i := 0; i < it.NumField(); i++ { 139 field := it.Field(i) 140 if _, ok := attributesMap[field.Name]; ok { 141 fields = append(fields, attributesMap[field.Name]...) 142 } else { 143 fields = append(fields, field) 144 } 145 } 146 } 147 148 for _, field := range fields { 149 if field.Tag.Get("docs") == "" { 150 continue 151 } 152 153 hclParts := strings.Split(field.Tag.Get("hcl"), ",") 154 if len(hclParts) == 0 { 155 continue 156 } 157 158 name := hclParts[0] 159 fieldDescription := field.Tag.Get("docs") 160 fieldDescription = bracesRegex.ReplaceAllString(fieldDescription, "`${1}`") 161 162 if len(hclParts) > 1 && hclParts[1] == "block" { 163 b := block{ 164 Description: fieldDescription, 165 Name: name, 166 } 167 result.Blocks = append(result.Blocks, b) 168 continue 169 } 170 171 fieldType := field.Tag.Get("type") 172 if fieldType == "" { 173 ft := strings.Replace(field.Type.String(), "*", "", 1) 174 if ft == "config.List" { 175 ft = "[]string" 176 } 177 if ft[:2] == "[]" { 178 ft = "tuple (" + ft[2:] + ")" 179 } else if strings.Contains(ft, "int") { 180 ft = "number" 181 } else if ft != "string" && ft != "bool" { 182 ft = "object" 183 } 184 fieldType = ft 185 } 186 187 fieldDefault := field.Tag.Get("default") 188 if fieldDefault == "" && fieldType == "bool" { 189 fieldDefault = "false" 190 } else if fieldDefault == "" && strings.HasPrefix(fieldType, "tuple ") { 191 fieldDefault = "[]" 192 } else if fieldDefault != "" && (fieldType == "string" || fieldType == "duration") { 193 fieldDefault = `"` + fieldDefault + `"` 194 } 195 196 a := attr{ 197 Default: fieldDefault, 198 Description: fieldDescription, 199 Name: name, 200 Type: fieldType, 201 } 202 result.Attributes = append(result.Attributes, a) 203 } 204 205 sort.Sort(byName(result.Attributes)) 206 if result.Blocks != nil { 207 sort.Sort(byName(result.Blocks)) 208 } 209 210 var bAttr, bBlock *bytes.Buffer 211 212 if result.Attributes != nil { 213 bAttr = &bytes.Buffer{} 214 enc := json.NewEncoder(bAttr) 215 enc.SetEscapeHTML(false) 216 enc.SetIndent("", " ") 217 if err := enc.Encode(result.Attributes); err != nil { 218 panic(err) 219 } 220 } 221 222 if result.Blocks != nil { 223 bBlock = &bytes.Buffer{} 224 enc := json.NewEncoder(bBlock) 225 enc.SetEscapeHTML(false) 226 enc.SetIndent("", " ") 227 if err := enc.Encode(result.Blocks); err != nil { 228 panic(err) 229 } 230 } 231 232 // TODO: write func 233 file, err := os.OpenFile(filepath.Join(docsBlockPath, blockName+".md"), os.O_RDWR|os.O_CREATE, 0666) 234 if err != nil { 235 panic(err) 236 } 237 238 fileBytes := &bytes.Buffer{} 239 240 scanner := bufio.NewScanner(file) 241 var skipMode, seenAttr, seenBlock bool 242 for scanner.Scan() { 243 line := scanner.Text() 244 245 if bAttr != nil && strings.HasPrefix(line, "::attributes") { 246 fileBytes.WriteString(fmt.Sprintf(`::attributes 247 --- 248 values: %s 249 --- 250 :: 251 `, bAttr.String())) 252 skipMode = true 253 seenAttr = true 254 continue 255 } else if bBlock != nil && strings.HasPrefix(line, "::blocks") { 256 fileBytes.WriteString(fmt.Sprintf(`::blocks 257 --- 258 values: %s 259 --- 260 :: 261 `, bBlock.String())) 262 skipMode = true 263 seenBlock = true 264 continue 265 } 266 267 if skipMode && line == "::" { 268 skipMode = false 269 continue 270 } 271 272 if !skipMode { 273 fileBytes.Write(scanner.Bytes()) 274 fileBytes.Write([]byte("\n")) 275 } 276 } 277 278 if bAttr != nil && !seenAttr { // TODO: from func/template 279 fileBytes.WriteString(fmt.Sprintf(` 280 ::attributes 281 --- 282 values: %s 283 --- 284 :: 285 `, bAttr.String())) 286 } 287 if bBlock != nil && !seenBlock { // TODO: from func/template 288 fileBytes.WriteString(fmt.Sprintf(` 289 ::blocks 290 --- 291 values: %s 292 --- 293 :: 294 `, bBlock.String())) 295 } 296 297 size, err := file.WriteAt(fileBytes.Bytes(), 0) 298 if err != nil { 299 panic(err) 300 } 301 err = os.Truncate(file.Name(), int64(size)) 302 if err != nil { 303 panic(err) 304 } 305 306 processedFiles[file.Name()] = struct{}{} 307 println("Attributes/Blocks written: "+blockName+":\r\t\t\t\t\t", file.Name()) 308 309 if os.Getenv(searchClientKey) != "" { 310 _, err = index.SaveObjects(result) //, opt.AutoGenerateObjectIDIfNotExist(true)) 311 if err != nil { 312 panic(err) 313 } 314 println("SearchIndex updated") 315 } 316 } 317 318 if os.Getenv(searchClientKey) == "" { 319 return 320 } 321 322 // index non generated markdown 323 indexDirectory(configurationPath, "", processedFiles, index) 324 indexDirectory(docsBlockPath, "block", processedFiles, index) 325 } 326 327 func collectFields(t reflect.Type, fields []reflect.StructField) []reflect.StructField { 328 for i := 0; i < t.NumField(); i++ { 329 field := t.Field(i) 330 if field.Anonymous { 331 fields = append(fields, collectFields(field.Type, fields)...) 332 } else { 333 fields = append(fields, field) 334 } 335 } 336 return fields 337 } 338 339 var mdHeaderRegex = regexp.MustCompile(`#(.+)\n(\n(.+)\n)`) 340 var mdFileRegex = regexp.MustCompile(`\d?\.?(.+)\.md`) 341 342 func indexDirectory(dirPath, docType string, processedFiles map[string]struct{}, index *search.Index) { 343 dirEntries, err := os.ReadDir(dirPath) 344 if err != nil { 345 panic(err) 346 } 347 348 for _, dirEntry := range dirEntries { 349 if dirEntry.IsDir() { 350 continue 351 } 352 353 entryPath := filepath.Join(dirPath, dirEntry.Name()) 354 if _, ok := processedFiles[entryPath]; ok { 355 continue 356 } 357 358 println("Indexing from file: " + dirEntry.Name()) 359 fileContent, rerr := os.ReadFile(entryPath) 360 if rerr != nil { 361 panic(err) 362 } 363 println(dirEntry.Name()) 364 fileName := mdFileRegex.FindStringSubmatch(dirEntry.Name())[1] 365 dt := docType 366 if dt == "" { 367 dt = fileName 368 } else { 369 fileName, _ = url.JoinPath(dt, fileName) 370 } 371 title, description, indexTable := headerFromMeta(fileContent) 372 if title == "" && description == "" { 373 matches := mdHeaderRegex.FindSubmatch(fileContent) 374 description = string(bytes.ToLower(matches[3])) 375 title = string(bytes.ToLower(matches[1])) 376 } 377 378 urlPath, _ := url.JoinPath(urlBasePath, fileName) 379 result := &entry{ 380 Attributes: attributesFromTable(fileContent, indexTable), 381 Description: description, 382 ID: urlPath, 383 Name: title, 384 Type: dt, 385 URL: urlPath, 386 } 387 388 // debug 389 if index == nil { 390 b, merr := json.Marshal(result) 391 if merr != nil { 392 panic(merr) 393 } 394 println(string(b)) 395 } else { 396 _, err = index.SaveObjects(result) 397 if err != nil { 398 panic(err) 399 } 400 println("SearchIndex updated") 401 } 402 } 403 } 404 405 func headerFromMeta(content []byte) (title string, description string, indexTable bool) { 406 var metaSep = []byte(`---`) 407 if !bytes.HasPrefix(content, metaSep) { 408 return 409 } 410 endIdx := bytes.LastIndex(content, metaSep) 411 s := bufio.NewScanner(bytes.NewReader(content[3:endIdx])) 412 for s.Scan() { 413 t := s.Text() 414 if strings.HasPrefix(t, "title") { 415 title = strings.Split(t, ": ")[1] 416 } else if strings.HasPrefix(t, "description") { 417 description = strings.Split(t, ": ")[1] 418 } else if strings.HasPrefix(t, "indexTable") { 419 indexTable = t == "indexTable: true" 420 } 421 422 } 423 return 424 } 425 426 var tableEntryRegex = regexp.MustCompile(`^\|\s\x60(.+)\x60\s+\|\s(.+)\s\|\s(.+)\.\s+\|`) 427 428 func attributesFromTable(content []byte, parse bool) []interface{} { 429 if !parse { 430 return nil 431 } 432 attrs := make([]interface{}, 0) 433 s := bufio.NewScanner(bytes.NewReader(content)) 434 var tableHeadSeen bool 435 for s.Scan() { 436 // scan to table header 437 line := s.Text() 438 if !tableHeadSeen { 439 if strings.HasPrefix(line, "|:-") { 440 tableHeadSeen = true 441 } 442 continue 443 } 444 if line[0] != '|' { 445 break 446 } 447 matches := tableEntryRegex.FindStringSubmatch(line) 448 if len(matches) < 4 { 449 continue 450 } 451 attrs = append(attrs, attr{ 452 Description: strings.TrimSpace(matches[3]), 453 Name: strings.TrimSpace(matches[1]), 454 Type: strings.TrimSpace(matches[2]), 455 }) 456 } 457 sort.Sort(byName(attrs)) 458 return attrs 459 } 460 461 type byName []interface{} 462 463 func (entries byName) Len() int { 464 return len(entries) 465 } 466 func (entries byName) Swap(i, j int) { 467 entries[i], entries[j] = entries[j], entries[i] 468 } 469 func (entries byName) Less(i, j int) bool { 470 left := reflect.ValueOf(entries[i]).FieldByName("Name").String() 471 right := reflect.ValueOf(entries[j]).FieldByName("Name").String() 472 return left < right 473 } 474 475 func newFields(impl interface{}) []reflect.StructField { 476 it := reflect.TypeOf(impl).Elem() 477 var fields []reflect.StructField 478 for i := 0; i < it.NumField(); i++ { 479 fields = append(fields, it.Field(i)) 480 } 481 return fields 482 }