github.com/aacfactory/fns@v1.2.86-0.20240310083819-80d667fc0a17/cmd/generates/modules/service.go (about) 1 /* 2 * Copyright 2023 Wang Min Xiang 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 * 16 */ 17 18 package modules 19 20 import ( 21 "fmt" 22 "github.com/aacfactory/cases" 23 "github.com/aacfactory/errors" 24 "github.com/aacfactory/fns/cmd/generates/files" 25 "github.com/aacfactory/fns/cmd/generates/sources" 26 "go/ast" 27 "os" 28 "path/filepath" 29 "sort" 30 "strings" 31 ) 32 33 func Load(mod *sources.Module, dir string) (services Services, err error) { 34 dir = filepath.ToSlash(filepath.Join(mod.Dir, dir)) 35 entries, readServicesDirErr := os.ReadDir(dir) 36 if readServicesDirErr != nil { 37 err = errors.Warning("read services dir failed").WithCause(readServicesDirErr).WithMeta("dir", dir) 38 return 39 } 40 if entries == nil || len(entries) == 0 { 41 return 42 } 43 group := make(map[string]*Service) 44 for _, entry := range entries { 45 if !entry.IsDir() { 46 continue 47 } 48 path := filepath.ToSlash(filepath.Join(mod.Path, "modules", entry.Name())) 49 docFilename := filepath.ToSlash(filepath.Join(mod.Dir, "modules", entry.Name(), "doc.go")) 50 if !files.ExistFile(docFilename) { 51 continue 52 } 53 service, loaded, loadErr := tryLoadService(mod, path) 54 if loadErr != nil { 55 err = errors.Warning("load service failed").WithCause(loadErr).WithMeta("file", docFilename) 56 return 57 } 58 if !loaded { 59 continue 60 } 61 _, exist := group[service.Name] 62 if exist { 63 err = errors.Warning("load service failed").WithCause(errors.Warning("modules: services was duplicated")).WithMeta("service", service.Name) 64 return 65 } 66 group[service.Name] = service 67 } 68 services = make([]*Service, 0, 1) 69 for _, service := range group { 70 services = append(services, service) 71 } 72 sort.Sort(services) 73 return 74 } 75 76 func tryLoadService(mod *sources.Module, path string) (service *Service, has bool, err error) { 77 f, filename, readErr := mod.Sources().ReadFile(path, "doc.go") 78 if readErr != nil { 79 err = errors.Warning("modules: parse service failed").WithCause(readErr).WithMeta("path", path).WithMeta("file", "doc.go") 80 return 81 } 82 _, pkg := filepath.Split(path) 83 if pkg != f.Name.Name { 84 err = errors.Warning("modules: parse service failed").WithCause(errors.Warning("pkg must be same as dir name")).WithMeta("path", path).WithMeta("file", "doc.go") 85 return 86 } 87 88 doc := f.Doc.Text() 89 if doc == "" { 90 return 91 } 92 annotations, parseAnnotationsErr := sources.ParseAnnotations(doc) 93 if parseAnnotationsErr != nil { 94 err = errors.Warning("modules: parse service failed").WithCause(parseAnnotationsErr).WithMeta("path", path).WithMeta("file", "doc.go") 95 return 96 } 97 98 name, hasName := annotations.Get("service") 99 if !hasName { 100 return 101 } 102 has = true 103 title := "" 104 description := "" 105 internal := false 106 titleAnno, hasTitle := annotations.Get("title") 107 if hasTitle && len(titleAnno.Params) > 0 { 108 title = titleAnno.Params[0] 109 } 110 descriptionAnno, hasDescription := annotations.Get("description") 111 if hasDescription && len(descriptionAnno.Params) > 0 { 112 description = descriptionAnno.Params[0] 113 } 114 internalAnno, hasInternal := annotations.Get("internal") 115 if hasInternal && len(descriptionAnno.Params) > 0 { 116 if len(internalAnno.Params) > 0 { 117 internal = true 118 } 119 internal = internalAnno.Params[0] == "true" 120 } 121 122 service = &Service{ 123 mod: mod, 124 Dir: filepath.Dir(filename), 125 Path: path, 126 PathIdent: f.Name.Name, 127 Name: strings.ToLower(name.Params[0]), 128 Internal: internal, 129 Title: title, 130 Description: description, 131 Imports: sources.Imports{}, 132 Functions: make([]*Function, 0, 1), 133 Components: make([]*Component, 0, 1), 134 } 135 loadFunctionsErr := service.loadFunctions() 136 if loadFunctionsErr != nil { 137 err = errors.Warning("modules: parse service failed").WithCause(loadFunctionsErr).WithMeta("path", path).WithMeta("file", "doc.go") 138 return 139 } 140 loadComponentsErr := service.loadComponents() 141 if loadComponentsErr != nil { 142 err = errors.Warning("modules: parse service failed").WithCause(loadComponentsErr).WithMeta("path", path).WithMeta("file", "doc.go") 143 return 144 } 145 sort.Sort(service.Functions) 146 sort.Sort(service.Components) 147 148 service.mergeImports() 149 return 150 } 151 152 type Service struct { 153 mod *sources.Module 154 Dir string 155 Path string 156 PathIdent string 157 Name string 158 Internal bool 159 Title string 160 Description string 161 Imports sources.Imports 162 Functions Functions 163 Components Components 164 } 165 166 func (service *Service) loadFunctions() (err error) { 167 err = service.mod.Sources().ReadDir(service.Path, func(file *ast.File, filename string) (err error) { 168 if file.Decls == nil || len(file.Decls) == 0 { 169 return 170 } 171 fileImports := sources.NewImportsFromAstFileImports(file.Imports) 172 for _, decl := range file.Decls { 173 funcDecl, ok := decl.(*ast.FuncDecl) 174 if !ok { 175 continue 176 } 177 if funcDecl.Recv != nil { 178 continue 179 } 180 if funcDecl.Doc == nil { 181 continue 182 } 183 doc := funcDecl.Doc.Text() 184 if !strings.Contains(doc, "@fn") { 185 continue 186 } 187 ident := funcDecl.Name.Name 188 if ast.IsExported(ident) { 189 err = errors.Warning("modules: parse func name failed"). 190 WithMeta("file", filename). 191 WithMeta("func", ident). 192 WithCause(errors.Warning("modules: func name must not be exported")) 193 return 194 } 195 nameAtoms, parseNameErr := cases.LowerCamel().Parse(ident) 196 if parseNameErr != nil { 197 err = errors.Warning("modules: parse func name failed"). 198 WithMeta("file", filename). 199 WithMeta("func", ident). 200 WithCause(parseNameErr) 201 return 202 } 203 proxyIdent := cases.Camel().Format(nameAtoms) 204 proxyAsyncIdent := fmt.Sprintf("%sAsync", proxyIdent) 205 constIdent := fmt.Sprintf("_%sFnName", ident) 206 handlerIdent := fmt.Sprintf("_%s", ident) 207 annotations, parseAnnotationsErr := sources.ParseAnnotations(doc) 208 if parseAnnotationsErr != nil { 209 err = errors.Warning("modules: parse func annotations failed"). 210 WithMeta("file", filename). 211 WithMeta("func", ident). 212 WithCause(parseAnnotationsErr) 213 return 214 } 215 fn := &Function{ 216 mod: service.mod, 217 hostServiceName: service.Name, 218 path: service.Path, 219 filename: filename, 220 file: file, 221 imports: fileImports, 222 decl: funcDecl, 223 Ident: ident, 224 VarIdent: constIdent, 225 ProxyIdent: proxyIdent, 226 ProxyAsyncIdent: proxyAsyncIdent, 227 HandlerIdent: handlerIdent, 228 Annotations: annotations, 229 Param: nil, 230 Result: nil, 231 } 232 service.Functions = append(service.Functions, fn) 233 } 234 return 235 }) 236 return 237 } 238 239 func (service *Service) loadComponents() (err error) { 240 componentsPath := fmt.Sprintf("%s/components", service.Path) 241 dir, dirErr := service.mod.Sources().DestinationPath(componentsPath) 242 if dirErr != nil { 243 err = errors.Warning("modules: read service components dir failed").WithCause(dirErr).WithMeta("service", service.Path) 244 return 245 } 246 if !files.ExistFile(dir) { 247 return 248 } 249 readErr := service.mod.Sources().ReadDir(componentsPath, func(file *ast.File, filename string) (err error) { 250 if file.Decls == nil || len(file.Decls) == 0 { 251 return 252 } 253 for _, decl := range file.Decls { 254 genDecl, ok := decl.(*ast.GenDecl) 255 if !ok { 256 continue 257 } 258 specs := genDecl.Specs 259 if specs == nil || len(specs) == 0 { 260 continue 261 } 262 for _, spec := range specs { 263 ts, tsOk := spec.(*ast.TypeSpec) 264 if !tsOk { 265 continue 266 } 267 doc := "" 268 if ts.Doc == nil || ts.Doc.Text() == "" { 269 if len(specs) == 1 && genDecl.Doc != nil && genDecl.Doc.Text() != "" { 270 doc = genDecl.Doc.Text() 271 } 272 } else { 273 doc = ts.Doc.Text() 274 } 275 if !strings.Contains(doc, "@component") { 276 continue 277 } 278 ident := ts.Name.Name 279 if !ast.IsExported(ident) { 280 err = errors.Warning("modules: parse component name failed"). 281 WithMeta("file", filename). 282 WithMeta("component", ident). 283 WithCause(errors.Warning("modules: component name must be exported")) 284 return 285 } 286 service.Components = append(service.Components, &Component{ 287 Indent: ident, 288 }) 289 } 290 } 291 return 292 }) 293 if readErr != nil { 294 err = errors.Warning("modules: read service components dir failed").WithCause(readErr).WithMeta("service", service.Path) 295 return 296 } 297 return 298 } 299 300 func (service *Service) mergeImports() { 301 importer := sources.Imports{} 302 importer.Add(&sources.Import{ 303 Path: "github.com/aacfactory/fns/context", 304 Alias: "", 305 }) 306 importer.Add(&sources.Import{ 307 Path: "github.com/aacfactory/errors", 308 Alias: "", 309 }) 310 importer.Add(&sources.Import{ 311 Path: "github.com/aacfactory/fns/services", 312 Alias: "", 313 }) 314 importer.Add(&sources.Import{ 315 Path: "github.com/aacfactory/fns/services/documents", 316 Alias: "", 317 }) 318 imports := make([]sources.Imports, 0, 1) 319 imports = append(imports, importer) 320 for _, function := range service.Functions { 321 imports = append(imports, function.imports) 322 } 323 service.Imports = sources.MergeImports(imports) 324 return 325 } 326 327 type Services []*Service 328 329 func (services Services) Len() int { 330 return len(services) 331 } 332 333 func (services Services) Less(i, j int) bool { 334 return services[i].Name < services[j].Name 335 } 336 337 func (services Services) Swap(i, j int) { 338 services[i], services[j] = services[j], services[i] 339 return 340 }