github.com/aavshr/aws-sdk-go@v1.41.3/private/model/api/docstring.go (about)

     1  //go:build codegen
     2  // +build codegen
     3  
     4  package api
     5  
     6  import (
     7  	"bufio"
     8  	"encoding/json"
     9  	"fmt"
    10  	"html"
    11  	"io"
    12  	"os"
    13  	"regexp"
    14  	"strings"
    15  
    16  	xhtml "golang.org/x/net/html"
    17  	"golang.org/x/net/html/atom"
    18  )
    19  
    20  type apiDocumentation struct {
    21  	Operations map[string]string
    22  	Service    string
    23  	Shapes     map[string]shapeDocumentation
    24  }
    25  
    26  type shapeDocumentation struct {
    27  	Base string
    28  	Refs map[string]string
    29  }
    30  
    31  // AttachDocs attaches documentation from a JSON filename.
    32  func (a *API) AttachDocs(filename string) error {
    33  	var d apiDocumentation
    34  
    35  	f, err := os.Open(filename)
    36  	defer f.Close()
    37  	if err != nil {
    38  		return err
    39  	}
    40  	err = json.NewDecoder(f).Decode(&d)
    41  	if err != nil {
    42  		return fmt.Errorf("failed to decode %s, err: %v", filename, err)
    43  	}
    44  
    45  	return d.setup(a)
    46  }
    47  
    48  func (d *apiDocumentation) setup(a *API) error {
    49  	a.Documentation = docstring(d.Service)
    50  
    51  	for opName, doc := range d.Operations {
    52  		if _, ok := a.Operations[opName]; !ok {
    53  			continue
    54  		}
    55  		a.Operations[opName].Documentation = docstring(doc)
    56  	}
    57  
    58  	for shapeName, docShape := range d.Shapes {
    59  		if s, ok := a.Shapes[shapeName]; ok {
    60  			s.Documentation = docstring(docShape.Base)
    61  		}
    62  
    63  		for ref, doc := range docShape.Refs {
    64  			if doc == "" {
    65  				continue
    66  			}
    67  
    68  			parts := strings.Split(ref, "$")
    69  			if len(parts) != 2 {
    70  				fmt.Fprintf(os.Stderr,
    71  					"Shape Doc %s has unexpected reference format, %q\n",
    72  					shapeName, ref)
    73  				continue
    74  			}
    75  
    76  			if s, ok := a.Shapes[parts[0]]; ok && len(s.MemberRefs) != 0 {
    77  				if m, ok := s.MemberRefs[parts[1]]; ok && m.ShapeName == shapeName {
    78  					m.Documentation = docstring(doc)
    79  				}
    80  			}
    81  		}
    82  	}
    83  
    84  	return nil
    85  }
    86  
    87  var reNewline = regexp.MustCompile(`\r?\n`)
    88  var reMultiSpace = regexp.MustCompile(`\s+`)
    89  var reComments = regexp.MustCompile(`<!--.*?-->`)
    90  var reFullnameBlock = regexp.MustCompile(`<fullname>(.+?)<\/fullname>`)
    91  var reFullname = regexp.MustCompile(`<fullname>(.*?)</fullname>`)
    92  var reExamples = regexp.MustCompile(`<examples?>.+?<\/examples?>`)
    93  var reEndNL = regexp.MustCompile(`\n+$`)
    94  
    95  // docstring rewrites a string to insert godocs formatting.
    96  func docstring(doc string) string {
    97  	doc = strings.TrimSpace(doc)
    98  	if doc == "" {
    99  		return ""
   100  	}
   101  
   102  	doc = reNewline.ReplaceAllString(doc, "")
   103  	doc = reMultiSpace.ReplaceAllString(doc, " ")
   104  	doc = reComments.ReplaceAllString(doc, "")
   105  
   106  	var fullname string
   107  	parts := reFullnameBlock.FindStringSubmatch(doc)
   108  	if len(parts) > 1 {
   109  		fullname = parts[1]
   110  	}
   111  	// Remove full name block from doc string
   112  	doc = reFullname.ReplaceAllString(doc, "")
   113  
   114  	doc = reExamples.ReplaceAllString(doc, "")
   115  	doc = generateDoc(doc)
   116  	doc = reEndNL.ReplaceAllString(doc, "")
   117  	doc = html.UnescapeString(doc)
   118  
   119  	// Replace doc with full name if doc is empty.
   120  	if len(doc) == 0 {
   121  		doc = fullname
   122  	}
   123  
   124  	return commentify(doc)
   125  }
   126  
   127  const (
   128  	indent = "   "
   129  )
   130  
   131  // commentify converts a string to a Go comment
   132  func commentify(doc string) string {
   133  	if len(doc) == 0 {
   134  		return ""
   135  	}
   136  
   137  	lines := strings.Split(doc, "\n")
   138  	out := make([]string, 0, len(lines))
   139  	for i := 0; i < len(lines); i++ {
   140  		line := lines[i]
   141  
   142  		if i > 0 && line == "" && lines[i-1] == "" {
   143  			continue
   144  		}
   145  		out = append(out, line)
   146  	}
   147  
   148  	if len(out) > 0 {
   149  		out[0] = "// " + out[0]
   150  		return strings.Join(out, "\n// ")
   151  	}
   152  	return ""
   153  }
   154  
   155  func wrap(text string, length int) string {
   156  	var b strings.Builder
   157  
   158  	s := bufio.NewScanner(strings.NewReader(text))
   159  	for s.Scan() {
   160  		line := s.Text()
   161  
   162  		// cleanup the line's spaces
   163  		var i int
   164  		for i = 0; i < len(line); i++ {
   165  			c := line[i]
   166  			// Ignore leading spaces, e.g indents.
   167  			if !(c == ' ' || c == '\t') {
   168  				break
   169  			}
   170  		}
   171  		line = line[:i] + strings.Join(strings.Fields(line[i:]), " ")
   172  		splitLine(&b, line, length)
   173  	}
   174  
   175  	return strings.TrimRight(b.String(), "\n")
   176  }
   177  
   178  func splitLine(w stringWriter, line string, length int) {
   179  	leading := getLeadingWhitespace(line)
   180  
   181  	line = line[len(leading):]
   182  	length -= len(leading)
   183  
   184  	const splitOn = " "
   185  	for len(line) > length {
   186  		// Find the next whitespace to the length
   187  		idx := strings.Index(line[length:], splitOn)
   188  		if idx == -1 {
   189  			break
   190  		}
   191  		offset := length + idx
   192  
   193  		if v := line[offset+len(splitOn):]; len(v) == 1 && strings.ContainsAny(v, `,.!?'"`) {
   194  			// Workaround for long lines with space before the punctuation mark.
   195  			break
   196  		}
   197  
   198  		w.WriteString(leading)
   199  		w.WriteString(line[:offset])
   200  		w.WriteByte('\n')
   201  		line = strings.TrimLeft(line[offset+len(splitOn):], " \t")
   202  	}
   203  
   204  	if len(line) > 0 {
   205  		w.WriteString(leading)
   206  		w.WriteString(line)
   207  	}
   208  	// Add the newline back in that was stripped out by scanner.
   209  	w.WriteByte('\n')
   210  }
   211  
   212  func getLeadingWhitespace(v string) string {
   213  	var o strings.Builder
   214  	for _, c := range v {
   215  		if c == ' ' || c == '\t' {
   216  			o.WriteRune(c)
   217  		} else {
   218  			break
   219  		}
   220  	}
   221  
   222  	return o.String()
   223  }
   224  
   225  // generateDoc will generate the proper doc string for html encoded or plain text doc entries.
   226  func generateDoc(htmlSrc string) string {
   227  	tokenizer := xhtml.NewTokenizer(strings.NewReader(htmlSrc))
   228  	var builder strings.Builder
   229  	if err := encodeHTMLToText(&builder, tokenizer); err != nil {
   230  		panic(fmt.Sprintf("failed to generated docs, %v", err))
   231  	}
   232  
   233  	return wrap(strings.Trim(builder.String(), "\n"), 72)
   234  }
   235  
   236  type stringWriter interface {
   237  	Write([]byte) (int, error)
   238  	WriteByte(byte) error
   239  	WriteRune(rune) (int, error)
   240  	WriteString(string) (int, error)
   241  }
   242  
   243  func encodeHTMLToText(w stringWriter, z *xhtml.Tokenizer) error {
   244  	encoder := newHTMLTokenEncoder(w)
   245  	defer encoder.Flush()
   246  
   247  	for {
   248  		tt := z.Next()
   249  		if tt == xhtml.ErrorToken {
   250  			if err := z.Err(); err == io.EOF {
   251  				return nil
   252  			} else if err != nil {
   253  				return err
   254  			}
   255  		}
   256  
   257  		if err := encoder.Encode(z.Token()); err != nil {
   258  			return err
   259  		}
   260  	}
   261  }
   262  
   263  type htmlTokenHandler interface {
   264  	OnStartTagToken(xhtml.Token) htmlTokenHandler
   265  	OnEndTagToken(xhtml.Token, bool)
   266  	OnSelfClosingTagToken(xhtml.Token)
   267  	OnTextTagToken(xhtml.Token)
   268  }
   269  
   270  type htmlTokenEncoder struct {
   271  	w           stringWriter
   272  	depth       int
   273  	handlers    []tokenHandlerItem
   274  	baseHandler tokenHandlerItem
   275  }
   276  
   277  type tokenHandlerItem struct {
   278  	handler htmlTokenHandler
   279  	depth   int
   280  }
   281  
   282  func newHTMLTokenEncoder(w stringWriter) *htmlTokenEncoder {
   283  	baseHandler := newBlockTokenHandler(w)
   284  	baseHandler.rootBlock = true
   285  
   286  	return &htmlTokenEncoder{
   287  		w: w,
   288  		baseHandler: tokenHandlerItem{
   289  			handler: baseHandler,
   290  		},
   291  	}
   292  }
   293  
   294  func (e *htmlTokenEncoder) Flush() error {
   295  	e.baseHandler.handler.OnEndTagToken(xhtml.Token{Type: xhtml.TextToken}, true)
   296  	return nil
   297  }
   298  
   299  func (e *htmlTokenEncoder) Encode(token xhtml.Token) error {
   300  	h := e.baseHandler
   301  	if len(e.handlers) != 0 {
   302  		h = e.handlers[len(e.handlers)-1]
   303  	}
   304  
   305  	switch token.Type {
   306  	case xhtml.StartTagToken:
   307  		e.depth++
   308  
   309  		next := h.handler.OnStartTagToken(token)
   310  		if next != nil {
   311  			e.handlers = append(e.handlers, tokenHandlerItem{
   312  				handler: next,
   313  				depth:   e.depth,
   314  			})
   315  		}
   316  
   317  	case xhtml.EndTagToken:
   318  		handlerBlockClosing := e.depth == h.depth
   319  
   320  		h.handler.OnEndTagToken(token, handlerBlockClosing)
   321  
   322  		// Remove all but the root handler as the handler is no longer needed.
   323  		if handlerBlockClosing {
   324  			e.handlers = e.handlers[:len(e.handlers)-1]
   325  		}
   326  		e.depth--
   327  
   328  	case xhtml.SelfClosingTagToken:
   329  		h.handler.OnSelfClosingTagToken(token)
   330  
   331  	case xhtml.TextToken:
   332  		h.handler.OnTextTagToken(token)
   333  	}
   334  
   335  	return nil
   336  }
   337  
   338  type baseTokenHandler struct {
   339  	w stringWriter
   340  }
   341  
   342  func (e *baseTokenHandler) OnStartTagToken(token xhtml.Token) htmlTokenHandler { return nil }
   343  func (e *baseTokenHandler) OnEndTagToken(token xhtml.Token, blockClosing bool) {}
   344  func (e *baseTokenHandler) OnSelfClosingTagToken(token xhtml.Token)            {}
   345  func (e *baseTokenHandler) OnTextTagToken(token xhtml.Token) {
   346  	e.w.WriteString(token.Data)
   347  }
   348  
   349  type blockTokenHandler struct {
   350  	baseTokenHandler
   351  
   352  	rootBlock  bool
   353  	origWriter stringWriter
   354  	strBuilder *strings.Builder
   355  
   356  	started                bool
   357  	newlineBeforeNextBlock bool
   358  }
   359  
   360  func newBlockTokenHandler(w stringWriter) *blockTokenHandler {
   361  	strBuilder := &strings.Builder{}
   362  	return &blockTokenHandler{
   363  		origWriter: w,
   364  		strBuilder: strBuilder,
   365  		baseTokenHandler: baseTokenHandler{
   366  			w: strBuilder,
   367  		},
   368  	}
   369  }
   370  func (e *blockTokenHandler) OnStartTagToken(token xhtml.Token) htmlTokenHandler {
   371  	e.started = true
   372  	if e.newlineBeforeNextBlock {
   373  		e.w.WriteString("\n")
   374  		e.newlineBeforeNextBlock = false
   375  	}
   376  
   377  	switch token.DataAtom {
   378  	case atom.A:
   379  		return newLinkTokenHandler(e.w, token)
   380  	case atom.Ul:
   381  		e.w.WriteString("\n")
   382  		e.newlineBeforeNextBlock = true
   383  		return newListTokenHandler(e.w)
   384  
   385  	case atom.Div, atom.Dt, atom.P, atom.H1, atom.H2, atom.H3, atom.H4, atom.H5, atom.H6:
   386  		e.w.WriteString("\n")
   387  		e.newlineBeforeNextBlock = true
   388  		return newBlockTokenHandler(e.w)
   389  
   390  	case atom.Pre, atom.Code:
   391  		if e.rootBlock {
   392  			e.w.WriteString("\n")
   393  			e.w.WriteString(indent)
   394  			e.newlineBeforeNextBlock = true
   395  		}
   396  		return newBlockTokenHandler(e.w)
   397  	}
   398  
   399  	return nil
   400  }
   401  func (e *blockTokenHandler) OnEndTagToken(token xhtml.Token, blockClosing bool) {
   402  	if !blockClosing {
   403  		return
   404  	}
   405  
   406  	e.origWriter.WriteString(e.strBuilder.String())
   407  	if e.newlineBeforeNextBlock {
   408  		e.origWriter.WriteString("\n")
   409  		e.newlineBeforeNextBlock = false
   410  	}
   411  
   412  	e.strBuilder.Reset()
   413  }
   414  
   415  func (e *blockTokenHandler) OnTextTagToken(token xhtml.Token) {
   416  	if e.newlineBeforeNextBlock {
   417  		e.w.WriteString("\n")
   418  		e.newlineBeforeNextBlock = false
   419  	}
   420  	if !e.started {
   421  		token.Data = strings.TrimLeft(token.Data, " \t\n")
   422  	}
   423  	if len(token.Data) != 0 {
   424  		e.started = true
   425  	}
   426  	e.baseTokenHandler.OnTextTagToken(token)
   427  }
   428  
   429  type linkTokenHandler struct {
   430  	baseTokenHandler
   431  	linkToken xhtml.Token
   432  }
   433  
   434  func newLinkTokenHandler(w stringWriter, token xhtml.Token) *linkTokenHandler {
   435  	return &linkTokenHandler{
   436  		baseTokenHandler: baseTokenHandler{
   437  			w: w,
   438  		},
   439  		linkToken: token,
   440  	}
   441  }
   442  func (e *linkTokenHandler) OnEndTagToken(token xhtml.Token, blockClosing bool) {
   443  	if !blockClosing {
   444  		return
   445  	}
   446  
   447  	if href, ok := getHTMLTokenAttr(e.linkToken.Attr, "href"); ok && len(href) != 0 {
   448  		fmt.Fprintf(e.w, " (%s)", strings.TrimSpace(href))
   449  	}
   450  }
   451  
   452  type listTokenHandler struct {
   453  	baseTokenHandler
   454  
   455  	items int
   456  }
   457  
   458  func newListTokenHandler(w stringWriter) *listTokenHandler {
   459  	return &listTokenHandler{
   460  		baseTokenHandler: baseTokenHandler{
   461  			w: w,
   462  		},
   463  	}
   464  }
   465  func (e *listTokenHandler) OnStartTagToken(token xhtml.Token) htmlTokenHandler {
   466  	switch token.DataAtom {
   467  	case atom.Li:
   468  		if e.items >= 1 {
   469  			e.w.WriteString("\n\n")
   470  		}
   471  		e.items++
   472  		return newListItemTokenHandler(e.w)
   473  	}
   474  	return nil
   475  }
   476  
   477  func (e *listTokenHandler) OnTextTagToken(token xhtml.Token) {
   478  	// Squash whitespace between list and items
   479  }
   480  
   481  type listItemTokenHandler struct {
   482  	baseTokenHandler
   483  
   484  	origWriter stringWriter
   485  	strBuilder *strings.Builder
   486  }
   487  
   488  func newListItemTokenHandler(w stringWriter) *listItemTokenHandler {
   489  	strBuilder := &strings.Builder{}
   490  	return &listItemTokenHandler{
   491  		origWriter: w,
   492  		strBuilder: strBuilder,
   493  		baseTokenHandler: baseTokenHandler{
   494  			w: strBuilder,
   495  		},
   496  	}
   497  }
   498  func (e *listItemTokenHandler) OnStartTagToken(token xhtml.Token) htmlTokenHandler {
   499  	switch token.DataAtom {
   500  	case atom.P:
   501  		return newBlockTokenHandler(e.w)
   502  	}
   503  	return nil
   504  }
   505  func (e *listItemTokenHandler) OnEndTagToken(token xhtml.Token, blockClosing bool) {
   506  	if !blockClosing {
   507  		return
   508  	}
   509  
   510  	e.origWriter.WriteString(indent + "* ")
   511  	e.origWriter.WriteString(strings.TrimSpace(e.strBuilder.String()))
   512  }
   513  
   514  type trimSpaceTokenHandler struct {
   515  	baseTokenHandler
   516  
   517  	origWriter stringWriter
   518  	strBuilder *strings.Builder
   519  }
   520  
   521  func newTrimSpaceTokenHandler(w stringWriter) *trimSpaceTokenHandler {
   522  	strBuilder := &strings.Builder{}
   523  	return &trimSpaceTokenHandler{
   524  		origWriter: w,
   525  		strBuilder: strBuilder,
   526  		baseTokenHandler: baseTokenHandler{
   527  			w: strBuilder,
   528  		},
   529  	}
   530  }
   531  func (e *trimSpaceTokenHandler) OnEndTagToken(token xhtml.Token, blockClosing bool) {
   532  	if !blockClosing {
   533  		return
   534  	}
   535  
   536  	e.origWriter.WriteString(strings.TrimSpace(e.strBuilder.String()))
   537  }
   538  
   539  func getHTMLTokenAttr(attr []xhtml.Attribute, name string) (string, bool) {
   540  	for _, a := range attr {
   541  		if strings.EqualFold(a.Key, name) {
   542  			return a.Val, true
   543  		}
   544  	}
   545  	return "", false
   546  }