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  }