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 }