diff --git a/CHANGELOG.md b/CHANGELOG.md index 59c03a249..1c49e868f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## UNRELEASED - Runtime +- feat: Apply in-code documentation to generated GraphQL [#519](https://github.com/hypermodeinc/modus/pull/519) - feat: Reduce logger output during development [#576](https://github.com/hypermodeinc/modus/pull/576) - chore: Trigger internal release pipeline at the end of the release-runtime workflow [#577](https://github.com/hypermodeinc/modus/pull/577) - feat: Add API explorer to runtime [#578](https://github.com/hypermodeinc/modus/pull/578) diff --git a/lib/metadata/metadata.go b/lib/metadata/metadata.go index f8f8dccb5..1a4124ea3 100644 --- a/lib/metadata/metadata.go +++ b/lib/metadata/metadata.go @@ -34,16 +34,22 @@ type Metadata struct { Types TypeMap `json:"types,omitempty"` } +type Docs struct { + Lines []string `json:"lines"` +} + type Function struct { Name string `json:"-"` Parameters []*Parameter `json:"parameters,omitempty"` Results []*Result `json:"results,omitempty"` + Docs *Docs `json:"docs,omitempty"` } type TypeDefinition struct { Name string `json:"-"` Id uint32 `json:"id,omitempty"` Fields []*Field `json:"fields,omitempty"` + Docs *Docs `json:"docs,omitempty"` } type Parameter struct { @@ -60,6 +66,7 @@ type Result struct { type Field struct { Name string `json:"name"` Type string `json:"type"` + Docs *Docs `json:"docs,omitempty"` } func (p *Parameter) UnmarshalJSON(data []byte) error { @@ -118,9 +125,9 @@ func (m *Metadata) SdkVersion() string { func (m *Metadata) GetTypeDefinition(typ string) (*TypeDefinition, error) { switch typ { case "[]byte": - return &TypeDefinition{typ, 1, nil}, nil + return &TypeDefinition{typ, 1, nil, nil}, nil case "string": - return &TypeDefinition{typ, 2, nil}, nil + return &TypeDefinition{typ, 2, nil, nil}, nil } def, ok := m.Types[typ] diff --git a/runtime/graphql/schemagen/schemagen.go b/runtime/graphql/schemagen/schemagen.go index b501b72fa..12751a5c2 100644 --- a/runtime/graphql/schemagen/schemagen.go +++ b/runtime/graphql/schemagen/schemagen.go @@ -118,10 +118,16 @@ func transformTypes(types metadata.TypeMap, lti langsupport.LanguageTypeInfo, fo continue } - typeDefs[name] = &TypeDefinition{ + typeDef := &TypeDefinition{ Name: name, Fields: fields, } + + if t.Docs != nil { + typeDef.DocLines = t.Docs.Lines + } + + typeDefs[name] = typeDef } return typeDefs, errors } @@ -131,12 +137,14 @@ type FieldDefinition struct { Type string Arguments []*ArgumentDefinition Function string + DocLines []string } type TypeDefinition struct { Name string Fields []*FieldDefinition IsMapType bool + DocLines []string } type ArgumentDefinition struct { @@ -186,6 +194,10 @@ func transformFunctions(functions metadata.FunctionMap, inputTypeDefs, resultTyp Function: fn.Name, } + if fn.Docs != nil { + field.DocLines = fn.Docs.Lines + } + if filter(field) { if isMutation(fn.Name) { mutationFields = append(mutationFields, field) @@ -329,14 +341,34 @@ func writeSchema(buf *bytes.Buffer, root *RootObjects, scalarTypes []string, inp buf.WriteByte('\n') } } - // write input types for _, t := range inputTypeDefs { buf.WriteByte('\n') + + if len(t.DocLines) > 0 { + buf.WriteString("\"\"\"\n") + for _, line := range t.DocLines { + buf.WriteString(line) + buf.WriteByte('\n') + } + buf.WriteString("\"\"\"\n") + } + buf.WriteString("input ") buf.WriteString(t.Name) buf.WriteString(" {\n") for _, f := range t.Fields { + + if len(f.DocLines) > 0 { + buf.WriteString(" \"\"\"\n") + for _, line := range f.DocLines { + buf.WriteString(" ") + buf.WriteString(line) + buf.WriteByte('\n') + } + buf.WriteString(" \"\"\"\n") + } + buf.WriteString(" ") buf.WriteString(f.Name) buf.WriteString(": ") @@ -349,10 +381,31 @@ func writeSchema(buf *bytes.Buffer, root *RootObjects, scalarTypes []string, inp // write result types for _, t := range resultTypeDefs { buf.WriteByte('\n') + + if len(t.DocLines) > 0 { + buf.WriteString("\"\"\"\n") + for _, line := range t.DocLines { + buf.WriteString(line) + buf.WriteByte('\n') + } + buf.WriteString("\"\"\"\n") + } + buf.WriteString("type ") buf.WriteString(t.Name) buf.WriteString(" {\n") for _, f := range t.Fields { + + if len(f.DocLines) > 0 { + buf.WriteString(" \"\"\"\n") + for _, line := range f.DocLines { + buf.WriteString(" ") + buf.WriteString(line) + buf.WriteByte('\n') + } + buf.WriteString(" \"\"\"\n") + } + buf.WriteString(" ") buf.WriteString(f.Name) buf.WriteString(": ") @@ -364,6 +417,17 @@ func writeSchema(buf *bytes.Buffer, root *RootObjects, scalarTypes []string, inp } func writeField(buf *bytes.Buffer, field *FieldDefinition) { + + if len(field.DocLines) > 0 { + buf.WriteString(" \"\"\"\n") + for _, line := range field.DocLines { + buf.WriteString(" ") + buf.WriteString(line) + buf.WriteByte('\n') + } + buf.WriteString(" \"\"\"\n") + } + buf.WriteString(" ") buf.WriteString(field.Name) if len(field.Arguments) > 0 { @@ -486,10 +550,17 @@ func convertFields(fields []*metadata.Field, lti langsupport.LanguageTypeInfo, t if err != nil { return nil, err } - results[i] = &FieldDefinition{ + + fieldDef := &FieldDefinition{ Name: f.Name, Type: t, } + + if f.Docs != nil { + fieldDef.DocLines = f.Docs.Lines + } + + results[i] = fieldDef } return results, nil } diff --git a/sdk/assemblyscript/examples/simple/assembly/index.ts b/sdk/assemblyscript/examples/simple/assembly/index.ts index 9136530e9..f45ca7c39 100644 --- a/sdk/assemblyscript/examples/simple/assembly/index.ts +++ b/sdk/assemblyscript/examples/simple/assembly/index.ts @@ -6,17 +6,23 @@ import { Person } from "./person"; -// This function adds two 32-bit signed integers together, and returns the result. +/** + * Adds two integers together, and returns the result. + */ export function add(a: i32, b: i32): i32 { return a + b; } -// This function takes a first name and a last name, and concatenates them to returns a full name. +/** + * Combines the first and last name of a person, and returns the full name. + */ export function getFullName(firstName: string, lastName: string): string { return `${firstName} ${lastName}`; } -// This function makes a list of people, and returns it. +/** + * Gets a list of people. + */ export function getPeople(): Person[] { return [ new Person("Bob", "Smith"), @@ -25,7 +31,9 @@ export function getPeople(): Person[] { ]; } -// This function returns a random person from the list of people. +/** + * Gets a random person from the list of people. + */ export function getRandomPerson(): Person { const people = getPeople(); const index = Math.floor(Math.random() * people.length); @@ -33,7 +41,9 @@ export function getRandomPerson(): Person { return person; } -// This function demonstrates various ways to log messages and errors. +/** + * Demonstrates logging error messages at different levels. + */ export function testErrors(): void { // This is a simple log message. It has no level. console.log("This is a simple log message."); diff --git a/sdk/assemblyscript/examples/simple/assembly/person.ts b/sdk/assemblyscript/examples/simple/assembly/person.ts index d6f3ac550..6f9d4408f 100644 --- a/sdk/assemblyscript/examples/simple/assembly/person.ts +++ b/sdk/assemblyscript/examples/simple/assembly/person.ts @@ -7,9 +7,24 @@ // This class is used in the examples. // It is separated just to demonstrated how to use multiple files. // Note, this is just one way to define a class in AssemblyScript. + +/** + * A simple object representing a person. + */ export class Person { + /** + * The first name of the person. + */ firstName: string; + + /** + * The last name of the person. + */ lastName: string; + + /** + * The full name of the person. + */ fullName: string; constructor(firstName: string, lastName: string) { diff --git a/sdk/assemblyscript/src/transform/src/extractor.ts b/sdk/assemblyscript/src/transform/src/extractor.ts index 2262bb6a7..f7faeea39 100644 --- a/sdk/assemblyscript/src/transform/src/extractor.ts +++ b/sdk/assemblyscript/src/transform/src/extractor.ts @@ -11,8 +11,13 @@ import binaryen from "assemblyscript/lib/binaryen.js"; import { ArrayLiteralExpression, Class, + ClassDeclaration, + CommentKind, + CommentNode, + CommonFlags, ElementKind, Expression, + FieldDeclaration, FloatLiteralExpression, Function as Func, FunctionDeclaration, @@ -22,9 +27,13 @@ import { NodeKind, Program, Property, + Range, + SourceKind, StringLiteralExpression, } from "assemblyscript/dist/assemblyscript.js"; import { + Docs, + Field, FunctionSignature, JsonLiteral, Parameter, @@ -40,9 +49,15 @@ export class Extractor { program: Program; transform: ModusTransform; - constructor(transform: ModusTransform, module: binaryen.Module) { - this.program = transform.program; + constructor(transform: ModusTransform) { this.binaryen = transform.binaryen; + } + + initHook(program: Program): void { + this.program = program; + } + + compileHook(module: binaryen.Module): void { this.module = module; } @@ -95,7 +110,7 @@ export class Extractor { return { exportFns: exportedFunctions, importFns: importedFunctions, - types, + types: types.map((v) => this.getTypeDocs(v)), }; } @@ -154,10 +169,13 @@ export class Extractor { return instance as Property; }) .filter((p) => p && p.isField) - .map((f) => ({ - name: f.name, - type: f.type.toString(), - })); + .map( + (f) => + { + name: f.name, + type: f.type.toString(), + }, + ); } private getExportedFunctions() { @@ -216,9 +234,169 @@ export class Extractor { }); } - return new FunctionSignature(e.name, params, [ + const signature = new FunctionSignature(e.name, params, [ { type: f.signature.returnType.toString() }, ]); + + signature.docs = this.getDocsFromFunction(signature); + return signature; + } + private getDocsFromFunction(signature: FunctionSignature) { + let docs: Docs | null = null; + + for (const source of this.program.sources.filter( + (v) => v.sourceKind == SourceKind.UserEntry, + )) { + for (const node of source.statements.filter( + (v) => + v.kind == NodeKind.FunctionDeclaration && + (v).flags >= CommonFlags.Export, + ) as FunctionDeclaration[]) { + const source = node.range.source; + if (node.flags <= CommonFlags.Import) continue; + if (node.name.text == signature.name) { + const nodeIndex = source.statements.indexOf(node); + const prevNode = source.statements[Math.max(nodeIndex - 1, 0)]; + + const start = nodeIndex > 0 ? prevNode.range.end : 0; + const end = node.range.start; + + const newRange = new Range(start, end); + newRange.source = source; + const commentNodes = this.parseComments(newRange); + if (!commentNodes.length) return; + docs = Docs.from(commentNodes); + } + } + } + return docs; + } + private getTypeDocs(type: TypeDefinition): TypeDefinition { + const name = (() => { + if (type.name.startsWith("~lib/")) return null; + return type.name.slice( + Math.max(type.name.lastIndexOf("<"), type.name.lastIndexOf("/") + 1), + Math.max(type.name.indexOf(">"), type.name.length), + ); + })(); + if (!name) return type; + for (const _node of Array.from(this.program.managedClasses.values())) { + if (_node.name != name) continue; + const node = _node.declaration as ClassDeclaration; + const source = node.range.source; + const nodeIndex = source.statements.indexOf(node); + const prevNode = source.statements[Math.max(nodeIndex - 1, 0)]; + + const start = nodeIndex > 0 ? prevNode.range.end : 0; + const end = node.range.start; + + const newRange = new Range(start, end); + newRange.source = source; + const commentNodes = this.parseComments(newRange); + if (!commentNodes.length) break; + type.docs = Docs.from(commentNodes); + + if (node.members.length) { + const memberDocs = this.getFieldsDocs( + node.members.filter( + (v) => v.kind == NodeKind.FieldDeclaration, + ) as FieldDeclaration[], + node, + ); + if (!memberDocs) continue; + for (let i = 0; i < memberDocs.length; i++) { + const docs = memberDocs[i]; + if (docs) { + const index = type.fields.findIndex( + (v) => v.name == node.members[i].name.text, + ); + if (index < 0) continue; + type.fields[index].docs = docs; + } + } + } + } + return type; + } + private getFieldsDocs( + nodes: FieldDeclaration[], + parent: ClassDeclaration, + ): (Docs | null)[] { + const docs = new Array(nodes.length).fill(null); + for (let i = 0; i < nodes.length; i++) { + const node = nodes[i]; + const source = node.range.source; + + const start = i == 0 ? parent.range.start : nodes[i - 1].range.end; + const end = node.range.start; + + const newRange = new Range(start, end); + newRange.source = source; + const commentNodes = this.parseComments(newRange); + if (!commentNodes.length) continue; + docs[i] = Docs.from(commentNodes); + } + return docs; + } + private parseComments(range: Range): CommentNode[] { + const nodes: CommentNode[] = []; + let text = range.source.text.slice(range.start, range.end).trim(); + const start = Math.min( + text.indexOf("/*") === -1 ? Infinity : text.indexOf("/*"), + text.indexOf("//") === -1 ? Infinity : text.indexOf("//"), + ); + if (start !== Infinity) text = text.slice(start); + let commentKind: CommentKind; + + if (text.startsWith("//")) { + commentKind = text.startsWith("///") + ? CommentKind.Triple + : CommentKind.Line; + + const end = range.source.text.indexOf("\n", range.start + 1); + if (end === -1) return []; + range.start = range.source.text.indexOf("//", range.start); + const newRange = new Range(range.start, end); + newRange.source = range.source; + const node = new CommentNode( + commentKind, + newRange.source.text.slice(newRange.start, newRange.end), + newRange, + ); + + nodes.push(node); + + if (end < range.end) { + const newRange = new Range(end, range.end); + newRange.source = range.source; + nodes.push(...this.parseComments(newRange)); + } + } else if (text.startsWith("/*")) { + commentKind = CommentKind.Block; + const end = range.source.text.indexOf("*/", range.start) + 2; + if (end === 1) return []; + + range.start = range.source.text.indexOf("/**", range.start); + const newRange = new Range(range.start, end); + newRange.source = range.source; + const node = new CommentNode( + commentKind, + newRange.source.text.slice(newRange.start, newRange.end), + newRange, + ); + + nodes.push(node); + + if (end < range.end) { + const newRange = new Range(end, range.end); + newRange.source = range.source; + nodes.push(...this.parseComments(newRange)); + } + } else { + return []; + } + + return nodes; } } diff --git a/sdk/assemblyscript/src/transform/src/index.ts b/sdk/assemblyscript/src/transform/src/index.ts index f67d13bb6..7bdc1523c 100644 --- a/sdk/assemblyscript/src/transform/src/index.ts +++ b/sdk/assemblyscript/src/transform/src/index.ts @@ -11,11 +11,17 @@ import { Transform } from "assemblyscript/dist/transform.js"; import { Metadata } from "./metadata.js"; import { Extractor } from "./extractor.js"; import binaryen from "assemblyscript/lib/binaryen.js"; +import { Program } from "types:assemblyscript/src/program"; export default class ModusTransform extends Transform { + private extractor = new Extractor(this); + afterInitialize(program: Program): void | Promise { + this.extractor.initHook(program); + } afterCompile(module: binaryen.Module) { - const extractor = new Extractor(this, module); - const info = extractor.getProgramInfo(); + this.extractor.compileHook(module); + + const info = this.extractor.getProgramInfo(); const m = Metadata.generate(); m.addExportFn(info.exportFns); diff --git a/sdk/assemblyscript/src/transform/src/types.ts b/sdk/assemblyscript/src/transform/src/types.ts index 189b6636d..cfbef451b 100644 --- a/sdk/assemblyscript/src/transform/src/types.ts +++ b/sdk/assemblyscript/src/transform/src/types.ts @@ -7,6 +7,10 @@ * SPDX-License-Identifier: Apache-2.0 */ +import { + CommentKind, + CommentNode, +} from "assemblyscript/dist/assemblyscript.js"; import { getTypeName } from "./extractor.js"; export class ProgramInfo { @@ -20,11 +24,28 @@ export class Result { public type: string; } +export class Docs { + constructor(public lines: string[]) {} + static from(nodes: CommentNode[]): Docs | null { + for (const node of nodes.reverse()) { + if (node.commentKind != CommentKind.Block || !node.text.startsWith("/**")) + continue; + const lines = node.text + .split("\n") + .filter((v) => v.trim().startsWith("* ")) + .map((v) => v.trim().slice(2)); + return new Docs(lines); + } + return null; + } +} + export class FunctionSignature { constructor( public name: string, public parameters: Parameter[], public results: Result[], + public docs: Docs | undefined = undefined, ) {} toString() { @@ -56,6 +77,10 @@ export class FunctionSignature { output["results"] = this.results; } + if (this.docs) { + output["docs"] = this.docs; + } + return output; } } @@ -65,6 +90,7 @@ export class TypeDefinition { public name: string, public id: number, public fields?: Field[], + public docs: Docs | undefined = undefined, ) {} toString() { @@ -83,6 +109,7 @@ export class TypeDefinition { return { id: this.id, fields: this.fields, + docs: this.docs, }; } @@ -105,9 +132,19 @@ export interface Parameter { default?: JsonLiteral; } -interface Field { - name: string; - type: string; +export class Field { + constructor( + public name: string, + public type: string, + public docs: Docs | undefined = undefined, + ) {} + toJSON() { + return { + name: this.name, + type: this.type, + docs: this.docs, + }; + } } export const typeMap = new Map([ diff --git a/sdk/go/examples/simple/main.go b/sdk/go/examples/simple/main.go index f478260dd..327b39775 100644 --- a/sdk/go/examples/simple/main.go +++ b/sdk/go/examples/simple/main.go @@ -16,14 +16,18 @@ import ( "github.com/hypermodeinc/modus/sdk/go/pkg/console" ) +// Logs a message. func LogMessage(message string) { console.Log(message) } +// Adds two integers together and returns the result. func Add(x, y int) int { return x + y } +// Adds three integers together and returns the result. +// The third integer is optional. func Add3(a, b int, c *int) int { if c != nil { return a + b + *c @@ -31,6 +35,7 @@ func Add3(a, b int, c *int) int { return a + b } +// Adds any number of integers together and returns the result. func AddN(args ...int) int { sum := 0 for _, arg := range args { @@ -42,18 +47,23 @@ func AddN(args ...int) int { // this indirection is so we can mock time.Now in tests var nowFunc = time.Now +// Returns the current time. func GetCurrentTime() time.Time { return nowFunc() } +// Returns the current time formatted as a string. func GetCurrentTimeFormatted() string { return nowFunc().Format(time.DateTime) } +// Combines the first and last name of a person, and returns the full name. func GetFullName(firstName, lastName string) string { return firstName + " " + lastName } +// Says hello to a person by name. +// If the name is not provided, it will say hello without a name. func SayHello(name *string) string { if name == nil { return "Hello!" @@ -62,12 +72,20 @@ func SayHello(name *string) string { } } +// A simple object representing a person. type Person struct { + + // The person's first name. FirstName string `json:"firstName"` - LastName string `json:"lastName"` - Age int `json:"age"` + + // The person's last name. + LastName string `json:"lastName"` + + // The person's age. + Age int `json:"age"` } +// Gets a person object. func GetPerson() Person { return Person{ FirstName: "John", @@ -76,12 +94,14 @@ func GetPerson() Person { } } +// Gets a random person object from a list of people. func GetRandomPerson() Person { people := GetPeople() i := rand.Intn(len(people)) return people[i] } +// Gets a list of people. func GetPeople() []Person { return []Person{ { @@ -102,17 +122,21 @@ func GetPeople() []Person { } } +// Gets the name and age of a person. func GetNameAndAge() (name string, age int) { p := GetPerson() return GetFullName(p.FirstName, p.LastName), p.Age } -// This is the preferred way to handle errors in functions. -// Simply declare an error interface as the last return value. -// You can use any object that implements the Go error interface. -// For example, you can create a new error with errors.New("message"), -// or with fmt.Errorf("message with %s", "parameters"). +// Tests returning an error. func TestNormalError(input string) (string, error) { + + // This is the preferred way to handle errors in functions. + // Simply declare an error interface as the last return value. + // You can use any object that implements the Go error interface. + // For example, you can create a new error with errors.New("message"), + // or with fmt.Errorf("message with %s", "parameters"). + if input == "" { return "", errors.New("input is empty") } @@ -120,9 +144,12 @@ func TestNormalError(input string) (string, error) { return output, nil } -// This is an alternative way to handle errors in functions. -// It is identical in behavior to TestNormalError, but is not Go idiomatic. +// Tests an alternative way to handle errors in functions. func TestAlternativeError(input string) string { + + // This is an alternative way to handle errors in functions. + // It is identical in behavior to TestNormalError, but is not Go idiomatic. + if input == "" { console.Error("input is empty") return "" @@ -131,22 +158,29 @@ func TestAlternativeError(input string) string { return output } -// This panics, will log the message as "fatal" and exits the function. -// Generally, you should not panic. +// Tests a panic. func TestPanic() { + + // This panics, will log the message as "fatal" and exits the function. + // Generally, you should not panic. + panic("This is a message from a panic.\nThis is a second line from a panic.\n") } -// If you need to exit prematurely without panicking, you can use os.Exit. -// However, you cannot return any values from the function, so if you want -// to log an error message, you should do so before calling os.Exit. -// The exit code should be 0 for success, and non-zero for failure. +// Tests an exit with a non-zero exit code. func TestExit() { + + // If you need to exit prematurely without panicking, you can use os.Exit. + // However, you cannot return any values from the function, so if you want + // to log an error message, you should do so before calling os.Exit. + // The exit code should be 0 for success, and non-zero for failure. + console.Error("This is an error message.") os.Exit(1) println("This line will not be executed.") } +// Tests logging at different levels. func TestLogging() { // This is a simple log message. It has no level. console.Log("This is a simple log message.") diff --git a/sdk/go/tools/modus-go-build/extractor/extractor.go b/sdk/go/tools/modus-go-build/extractor/extractor.go index 6d5c44ad4..f5bcaae1e 100644 --- a/sdk/go/tools/modus-go-build/extractor/extractor.go +++ b/sdk/go/tools/modus-go-build/extractor/extractor.go @@ -29,14 +29,14 @@ func CollectProgramInfo(config *config.Config, meta *metadata.Metadata, wasmFunc for name, f := range getExportedFunctions(pkgs) { if _, ok := wasmFunctions.Exports[name]; ok { - meta.FnExports[name] = transformFunc(name, f) + meta.FnExports[name] = transformFunc(name, f, pkgs) findRequiredTypes(f, requiredTypes) } } for name, f := range getImportedFunctions(pkgs) { if _, ok := wasmFunctions.Imports[name]; ok { - meta.FnImports[name] = transformFunc(name, f) + meta.FnImports[name] = transformFunc(name, f, pkgs) findRequiredTypes(f, requiredTypes) } } @@ -44,7 +44,7 @@ func CollectProgramInfo(config *config.Config, meta *metadata.Metadata, wasmFunc // proxy imports overwrite regular imports for name, f := range getProxyImportFunctions(pkgs) { if _, ok := meta.FnImports[name]; ok { - meta.FnImports[name] = transformFunc(name, f) + meta.FnImports[name] = transformFunc(name, f, pkgs) findRequiredTypes(f, requiredTypes) } } @@ -55,7 +55,7 @@ func CollectProgramInfo(config *config.Config, meta *metadata.Metadata, wasmFunc for _, name := range keys { t := requiredTypes[name] if s, ok := t.(*types.Struct); ok && !wellKnownTypes[name] { - t := transformStruct(name, s) + t := transformStruct(name, s, pkgs) t.Id = id meta.Types[name] = t } else { diff --git a/sdk/go/tools/modus-go-build/extractor/functions.go b/sdk/go/tools/modus-go-build/extractor/functions.go index 182af31a6..c6520110a 100644 --- a/sdk/go/tools/modus-go-build/extractor/functions.go +++ b/sdk/go/tools/modus-go-build/extractor/functions.go @@ -26,6 +26,23 @@ var wellKnownTypes = map[string]bool{ "time.Duration": true, } +func getFuncDeclaration(fn *types.Func, pkgs map[string]*packages.Package) *ast.FuncDecl { + fnName := strings.TrimPrefix(fn.Name(), "__modus_") + pkg := pkgs[fn.Pkg().Path()] + + for _, file := range pkg.Syntax { + for _, decl := range file.Decls { + if fd, ok := decl.(*ast.FuncDecl); ok { + if fd.Name.Name == fnName { + return fd + } + } + } + } + + return nil +} + func getExportedFunctions(pkgs map[string]*packages.Package) map[string]*types.Func { results := make(map[string]*types.Func) for _, pkg := range pkgs { diff --git a/sdk/go/tools/modus-go-build/extractor/transform.go b/sdk/go/tools/modus-go-build/extractor/transform.go index d79d9420f..757aa9e60 100644 --- a/sdk/go/tools/modus-go-build/extractor/transform.go +++ b/sdk/go/tools/modus-go-build/extractor/transform.go @@ -10,34 +10,49 @@ package extractor import ( + "go/ast" + "go/token" "go/types" + "strings" "github.com/hypermodeinc/modus/sdk/go/tools/modus-go-build/metadata" "github.com/hypermodeinc/modus/sdk/go/tools/modus-go-build/utils" + "golang.org/x/tools/go/packages" ) -func transformStruct(name string, s *types.Struct) *metadata.TypeDefinition { +func transformStruct(name string, s *types.Struct, pkgs map[string]*packages.Package) *metadata.TypeDefinition { if s == nil { return nil } - fields := make([]*metadata.Field, s.NumFields()) + structDecl, structType := getStructDeclarationAndType(name, pkgs) + if structDecl == nil || structType == nil { + return nil + } + + structDocs := getDocs(structDecl.Doc) + fields := make([]*metadata.Field, s.NumFields()) for i := 0; i < s.NumFields(); i++ { f := s.Field(i) + + fieldDocs := getDocs(structType.Fields.List[i].Doc) + fields[i] = &metadata.Field{ Name: utils.CamelCase(f.Name()), Type: f.Type().String(), + Docs: fieldDocs, } } return &metadata.TypeDefinition{ Name: name, Fields: fields, + Docs: structDocs, } } -func transformFunc(name string, f *types.Func) *metadata.Function { +func transformFunc(name string, f *types.Func, pkgs map[string]*packages.Package) *metadata.Function { if f == nil { return nil } @@ -46,8 +61,14 @@ func transformFunc(name string, f *types.Func) *metadata.Function { params := sig.Params() results := sig.Results() + funcDecl := getFuncDeclaration(f, pkgs) + if funcDecl == nil { + return nil + } + ret := metadata.Function{ Name: name, + Docs: getDocs(funcDecl.Doc), } if params != nil { @@ -74,3 +95,56 @@ func transformFunc(name string, f *types.Func) *metadata.Function { return &ret } + +func getStructDeclarationAndType(name string, pkgs map[string]*packages.Package) (*ast.GenDecl, *ast.StructType) { + objName := name[strings.LastIndex(name, ".")+1:] + pkgName := utils.GetPackageNamesForType(name)[0] + pkg := pkgs[pkgName] + + for _, file := range pkg.Syntax { + for _, decl := range file.Decls { + if genDecl, ok := decl.(*ast.GenDecl); ok && genDecl.Tok == token.TYPE { + for _, spec := range genDecl.Specs { + if typeSpec, ok := spec.(*ast.TypeSpec); ok { + if typeSpec.Name.Name == objName { + if structType, ok := typeSpec.Type.(*ast.StructType); ok { + return genDecl, structType + } + } + } + } + } + } + } + + return nil, nil +} + +func getDocs(comments *ast.CommentGroup) *metadata.Docs { + if comments == nil { + return nil + } + + var lines []string + for _, comment := range comments.List { + txt := comment.Text + if strings.HasPrefix(txt, "// ") { + txt = strings.TrimPrefix(txt, "// ") + txt = strings.TrimSpace(txt) + lines = append(lines, txt) + } else if strings.HasPrefix(txt, "/*") { + txt = strings.TrimPrefix(txt, "/*") + txt = strings.TrimSuffix(txt, "*/") + txt = strings.TrimSpace(txt) + lines = append(lines, strings.Split(txt, "\n")...) + } + } + + if len(lines) == 0 { + return nil + } + + return &metadata.Docs{ + Lines: lines, + } +} diff --git a/sdk/go/tools/modus-go-build/metadata/metadata.go b/sdk/go/tools/modus-go-build/metadata/metadata.go index 8f11af3da..9f2529d7c 100644 --- a/sdk/go/tools/modus-go-build/metadata/metadata.go +++ b/sdk/go/tools/modus-go-build/metadata/metadata.go @@ -37,16 +37,22 @@ type Metadata struct { Types TypeMap `json:"types,omitempty"` } +type Docs struct { + Lines []string `json:"lines"` +} + type Function struct { Name string `json:"-"` Parameters []*Parameter `json:"parameters,omitempty"` Results []*Result `json:"results,omitempty"` + Docs *Docs `json:"docs,omitempty"` } type TypeDefinition struct { Id uint32 `json:"id"` Name string `json:"-"` Fields []*Field `json:"fields,omitempty"` + Docs *Docs `json:"docs,omitempty"` } type Parameter struct { @@ -64,6 +70,7 @@ type Result struct { type Field struct { Name string `json:"name"` Type string `json:"type"` + Docs *Docs `json:"docs,omitempty"` } func NewMetadata() *Metadata {