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  }