github.com/easysoft/zendata@v0.0.0-20240513203326-705bd5a7fd67/internal/pkg/service/mock.go (about)

     1  package service
     2  
     3  import (
     4  	"context"
     5  	"encoding/json"
     6  	"fmt"
     7  	"os"
     8  	"path/filepath"
     9  	"strings"
    10  
    11  	consts "github.com/easysoft/zendata/internal/pkg/const"
    12  	"github.com/easysoft/zendata/internal/pkg/domain"
    13  	fileUtils "github.com/easysoft/zendata/pkg/utils/file"
    14  	logUtils "github.com/easysoft/zendata/pkg/utils/log"
    15  	shellUtils "github.com/easysoft/zendata/pkg/utils/shell"
    16  	"github.com/easysoft/zendata/pkg/utils/vari"
    17  	"github.com/getkin/kin-openapi/openapi3"
    18  	"gopkg.in/yaml.v2"
    19  )
    20  
    21  type MockService struct {
    22  	FileService *FileService `inject:""`
    23  }
    24  
    25  func (s *MockService) GenMockDef(input string) (
    26  	name, mockDefPath, zendataDefPath string, err error) { // return the last ones for client spec uploading
    27  
    28  	var files []string
    29  	fileUtils.GetFilesByExtInDir(input, ".yaml,.json", &files)
    30  
    31  	ctx := context.Background()
    32  	loader := &openapi3.Loader{Context: ctx, IsExternalRefsAllowed: true}
    33  
    34  	for _, f := range files {
    35  		if filepath.Ext(f) == ".json" {
    36  			f = s.convertPostmanSpec(f)
    37  		}
    38  
    39  		doc3, err := loader.LoadFromFile(f)
    40  		if err != nil {
    41  			logUtils.PrintTo(fmt.Sprintf("skip file %s which is not a vaild openapi3, swagger and postman spec.", f))
    42  			continue
    43  		}
    44  
    45  		fileName := filepath.Base(f)
    46  		dir := filepath.Dir(f)
    47  		if vari.GlobalVars.Output != "" {
    48  			dir = vari.GlobalVars.Output
    49  		}
    50  		mockDefPath, zendataDefPath = s.getFilePaths(fileName, dir)
    51  
    52  		zendataDef := domain.DefData{}
    53  		zendataDef.ClsInfo.Title = doc3.Info.Title
    54  
    55  		mockDef := domain.MockData{}
    56  		mockDef.Title = doc3.Info.Title
    57  		name = mockDef.Title
    58  
    59  		if mockDef.Paths == nil {
    60  			mockDef.Paths = map[string]map[string]map[string]map[string]*domain.EndPoint{}
    61  		}
    62  
    63  		for pathStr, pathItem := range doc3.Paths {
    64  			mp := map[string]map[string]map[string]*domain.EndPoint{}
    65  
    66  			if pathItem.Connect != nil {
    67  				s.setEndPoint(pathItem.Connect, &zendataDef, zendataDefPath, domain.Connect, &mp)
    68  			}
    69  
    70  			if pathItem.Delete != nil {
    71  				s.setEndPoint(pathItem.Delete, &zendataDef, zendataDefPath, domain.Delete, &mp)
    72  			}
    73  
    74  			if pathItem.Get != nil {
    75  				s.setEndPoint(pathItem.Get, &zendataDef, zendataDefPath, domain.Get, &mp)
    76  			}
    77  
    78  			if pathItem.Head != nil {
    79  				s.setEndPoint(pathItem.Head, &zendataDef, zendataDefPath, domain.Head, &mp)
    80  			}
    81  
    82  			if pathItem.Options != nil {
    83  				s.setEndPoint(pathItem.Options, &zendataDef, zendataDefPath, domain.Options, &mp)
    84  			}
    85  
    86  			if pathItem.Patch != nil {
    87  				s.setEndPoint(pathItem.Patch, &zendataDef, zendataDefPath, domain.Patch, &mp)
    88  			}
    89  
    90  			if pathItem.Post != nil {
    91  				s.setEndPoint(pathItem.Post, &zendataDef, zendataDefPath, domain.Post, &mp)
    92  			}
    93  
    94  			if pathItem.Put != nil {
    95  				s.setEndPoint(pathItem.Put, &zendataDef, zendataDefPath, domain.Put, &mp)
    96  			}
    97  
    98  			if pathItem.Trace != nil {
    99  				s.setEndPoint(pathItem.Trace, &zendataDef, zendataDefPath, domain.Trace, &mp)
   100  			}
   101  
   102  			mockDef.Paths[pathStr] = mp
   103  		}
   104  
   105  		s.saveFile(mockDef, mockDefPath)
   106  		s.saveFile(zendataDef, zendataDefPath)
   107  	}
   108  
   109  	return
   110  }
   111  
   112  func (s *MockService) convertPostmanSpec(input string) (ret string) {
   113  	// npm i postman-to-openapi -g
   114  
   115  	ret = input
   116  
   117  	content, _ := os.ReadFile(input)
   118  
   119  	if strings.Contains(string(content), "_postman_id") {
   120  		ret = ret + ".yaml"
   121  		cmd := fmt.Sprintf("p2o %s -f %s", input, ret)
   122  
   123  		shellUtils.ExecInDir(cmd, filepath.Dir(ret))
   124  	}
   125  
   126  	return
   127  }
   128  
   129  func (s *MockService) setEndPoint(operation *openapi3.Operation, zendataDef *domain.DefData,
   130  	zendataDefPath string, method domain.HttpMethod, mp *map[string]map[string]map[string]*domain.EndPoint) {
   131  
   132  	codeToEndpointMap := s.createEndPoint(operation, zendataDef, zendataDefPath, method)
   133  	(*mp)[method.String()] = codeToEndpointMap
   134  }
   135  
   136  func (s *MockService) createEndPoint(operation *openapi3.Operation, zendataDef *domain.DefData,
   137  	zendataDefPath string, method domain.HttpMethod) (
   138  	mockDef map[string]map[string]*domain.EndPoint) {
   139  
   140  	mockDef = map[string]map[string]*domain.EndPoint{}
   141  
   142  	for code, val := range operation.Responses {
   143  		// map[string]*ResponseRef
   144  
   145  		for mediaType, mediaItem := range val.Value.Content {
   146  			if mediaItem == nil {
   147  				continue
   148  			}
   149  			// mediaType is like "responses => 501 => content => application/json"
   150  
   151  			// zendata def
   152  			fields := s.genZendataDefFromMedia(mediaItem)
   153  			zendataDef.Fields = append(zendataDef.Fields, fields...) // maybe has no children from properties
   154  
   155  			// mock def
   156  			endpoint := s.genMockDefFromMedia(mediaItem, fields)
   157  			endpoint.Method = method
   158  			endpoint.MediaType = mediaType
   159  			endpoint.Config = filepath.Base(zendataDefPath) // set a relative path
   160  			endpoint.Lines = 10
   161  
   162  			if mockDef[code] == nil {
   163  				mockDef[code] = map[string]*domain.EndPoint{}
   164  			}
   165  			mockDef[code][mediaType] = &endpoint
   166  		}
   167  	}
   168  
   169  	return
   170  }
   171  
   172  func (s *MockService) genZendataDefFromMedia(item *openapi3.MediaType) (fields []domain.DefField) {
   173  	schemaNode := item.Schema
   174  	//exampleNode := item.Example
   175  	//examplesNode := item.Examples
   176  	//encodingNode := item.Encoding
   177  
   178  	if schemaNode != nil {
   179  		s.getFieldFromSchema(&fields, schemaNode)
   180  	}
   181  
   182  	return
   183  }
   184  
   185  func (s *MockService) genMockDefFromMedia(item *openapi3.MediaType, fields []domain.DefField) (endpoint domain.EndPoint) {
   186  	var fieldNames []string
   187  	for _, f := range fields {
   188  		fieldNames = append(fieldNames, f.Field)
   189  	}
   190  	endpoint.Fields = strings.Join(fieldNames, ",")
   191  
   192  	schemaNode := item.Schema
   193  	exampleNode := item.Example
   194  	examplesNode := item.Examples
   195  	//encodingNode := item.Encoding
   196  
   197  	if schemaNode != nil {
   198  		if schemaNode.Value.Type == string(consts.SchemaTypeArray) {
   199  			endpoint.Type = consts.SchemaTypeArray
   200  		} else if schemaNode.Value.Type == string(consts.SchemaTypeObject) {
   201  			endpoint.Type = consts.SchemaTypeObject
   202  		} else if schemaNode.Value.Type != "" {
   203  			endpoint.Type = consts.OpenApiSchemaType(schemaNode.Value.Type)
   204  		} else {
   205  			endpoint.Type = consts.SchemaTypeObject
   206  		}
   207  	}
   208  
   209  	endpoint.Samples = map[string]string{}
   210  	if schemaNode != nil && schemaNode.Value.Example != nil { // from schema's example
   211  		s.getExample(schemaNode.Value.Example, &endpoint.Samples)
   212  	}
   213  
   214  	if exampleNode != nil {
   215  		s.getExample(exampleNode, &endpoint.Samples)
   216  	}
   217  	if examplesNode != nil {
   218  		s.getExamples(examplesNode, &endpoint.Samples)
   219  	}
   220  
   221  	return
   222  }
   223  
   224  func (s *MockService) getFieldFromSchema(fields *[]domain.DefField, schemaNodes ...*openapi3.SchemaRef) {
   225  	propsMap := map[string]bool{}
   226  
   227  	for _, schemaNode := range schemaNodes {
   228  		if len(schemaNode.Value.Properties) > 0 { // properties based
   229  			for propName, prop := range schemaNode.Value.Properties {
   230  				if propsMap[propName] {
   231  					continue
   232  				}
   233  
   234  				field := domain.DefField{}
   235  				field.Field = propName // s.getSchemaNameFromRef(schemaNode.Ref) + "~" + propName
   236  				propsMap[propName] = true
   237  
   238  				if prop.Ref == "" { // leaf property
   239  					s.getRangeByTypeFormat(consts.OpenApiDataType(prop.Value.Type),
   240  						prop.Value.Enum, prop.Value.Default, prop.Value.Min, prop.Value.Max, &field)
   241  				} else { // refer to another schema
   242  					s.getFieldFromSchema(&field.Fields, prop)
   243  				}
   244  
   245  				*fields = append(*fields, field)
   246  			}
   247  
   248  		} else if schemaNode.Value.OneOf != nil {
   249  			s.getFieldFromSchema(fields, schemaNode.Value.OneOf[0])
   250  
   251  		} else if schemaNode.Value.AllOf != nil {
   252  			s.getFieldFromSchema(fields, schemaNode.Value.AllOf...)
   253  
   254  		} else if schemaNode.Value.AnyOf != nil {
   255  			arr := openapi3.SchemaRefs{schemaNode.Value.AnyOf[0]}
   256  			if len(schemaNode.Value.AnyOf) > 1 {
   257  				arr = append(arr, schemaNode.Value.AnyOf[len(schemaNode.Value.AnyOf)-1])
   258  			}
   259  
   260  			s.getFieldFromSchema(fields, arr...)
   261  
   262  		}
   263  
   264  		// items based
   265  		if schemaNode.Value.Items != nil {
   266  			s.getFieldFromItems(fields, schemaNode.Value.Items)
   267  		}
   268  	}
   269  
   270  	return
   271  }
   272  
   273  func (s *MockService) getSchemaNameFromRef(ref string) (ret string) {
   274  	arr := strings.Split(ref, "/")
   275  	ret = arr[len(arr)-1]
   276  
   277  	return
   278  }
   279  
   280  func (s *MockService) getFieldFromItems(fields *[]domain.DefField, itemsDef *openapi3.SchemaRef) {
   281  	s.getFieldFromSchema(fields, itemsDef)
   282  
   283  	return
   284  }
   285  
   286  func (s *MockService) getRangeByTypeFormat(typ consts.OpenApiDataType,
   287  	enums []interface{}, defaultVal interface{},
   288  	min, max *float64,
   289  	field *domain.DefField) {
   290  	if enums != nil {
   291  		field.Range = s.getRangeFromEnum(enums)
   292  		return
   293  	}
   294  
   295  	if consts.OpenApiDataTypeInteger == typ {
   296  		start, end := s.getStartEnd(1, 99, min, max, typ)
   297  		field.Range = fmt.Sprintf("%d-%d", start, end)
   298  
   299  	} else if consts.OpenApiDataTypeLong == typ {
   300  		maxInt64 := ^uint64(0)
   301  		maxDefault := maxInt64
   302  		minDefault := maxInt64 - 10000
   303  		start, end := s.getStartEnd(minDefault, maxDefault, min, max, typ)
   304  		field.Range = fmt.Sprintf("%d-%d", start, end)
   305  
   306  	} else if consts.OpenApiDataTypeFloat == typ {
   307  		start, end := s.getStartEnd(1.01, 99, min, max, typ)
   308  		field.Range = fmt.Sprintf("%f-%f", start, end)
   309  
   310  	} else if consts.OpenApiDataTypeDouble == typ {
   311  		start, end := s.getStartEnd(1.000000000000009, 99, min, max, typ)
   312  		field.Range = fmt.Sprintf("%f-%f", start, end)
   313  
   314  	} else if consts.OpenApiDataTypeString == typ {
   315  		field.Range = "a-z"
   316  		field.Loop = "6-8"
   317  
   318  	} else if consts.OpenApiDataTypeByte == typ {
   319  		start, end := s.getStartEnd('a', 'z', min, max, typ)
   320  		field.Range = fmt.Sprintf("%c-%c", start, end)
   321  
   322  	} else if consts.OpenApiDataTypeBinary == typ {
   323  		field.Format = "binary"
   324  
   325  	} else if consts.OpenApiDataTypeBoolean == typ {
   326  		field.Range = "[true,false]"
   327  
   328  	} else if consts.OpenApiDataTypeDate == typ {
   329  		field.Range = "20230101 000000-20230101 235959:60"
   330  		field.Type = "timestamp"
   331  		field.Format = "YY/MM/DD"
   332  
   333  	} else if consts.OpenApiDataTypeDateTime == typ {
   334  		field.Range = "20230101 000000-20230101 235959:60"
   335  		field.Type = "timestamp"
   336  		field.Format = "YY/MM/DD hh:mm:ss"
   337  
   338  	} else if consts.OpenApiDataTypePassword == typ {
   339  		field.Format = "password(8)"
   340  
   341  	}
   342  
   343  	if defaultVal != nil && (consts.OpenApiDataTypeInteger == typ || consts.OpenApiDataTypeLong == typ ||
   344  		consts.OpenApiDataTypeFloat == typ || consts.OpenApiDataTypeDouble == typ ||
   345  		consts.OpenApiDataTypeString == typ || consts.OpenApiDataTypeByte == typ) {
   346  
   347  		field.Range = fmt.Sprintf("%v, ", defaultVal) + field.Range
   348  	}
   349  }
   350  
   351  func (s *MockService) getRangeFromEnum(enums []interface{}) (ret string) {
   352  	var arr []string
   353  	for _, e := range enums {
   354  		arr = append(arr, fmt.Sprintf("%v", e))
   355  	}
   356  
   357  	ret = fmt.Sprintf("[%s]", strings.Join(arr, ","))
   358  
   359  	return
   360  }
   361  
   362  func (s *MockService) getFilePaths(name string, dir string) (mockPath, zendataPath string) {
   363  	ext := filepath.Ext(name)
   364  
   365  	zendataPath = strings.ReplaceAll(name, ext, "-zd"+ext)
   366  	zendataPath = filepath.Join(dir, zendataPath)
   367  
   368  	mockPath = strings.ReplaceAll(name, ext, "-mock"+ext)
   369  	mockPath = filepath.Join(dir, mockPath)
   370  
   371  	return
   372  }
   373  
   374  func (s *MockService) saveFile(obj interface{}, pth string) {
   375  	fileUtils.MkDirIfNeeded(filepath.Dir(pth))
   376  
   377  	bytes, err := yaml.Marshal(obj)
   378  	str := string(bytes)
   379  	if err != nil {
   380  		str = err.Error()
   381  	}
   382  
   383  	fileUtils.WriteFile(pth, str)
   384  }
   385  
   386  func (s *MockService) getStartEnd(startDefault, endDefault interface{}, min, max *float64, typ consts.OpenApiDataType) (startRet, endRet interface{}) {
   387  	startRet = startDefault
   388  	endRet = endDefault
   389  
   390  	if min != nil {
   391  		if typ == consts.OpenApiDataTypeInteger {
   392  			startRet = int(*min)
   393  		} else if typ == consts.OpenApiDataTypeLong {
   394  			startRet = int64(*min)
   395  		} else if typ == consts.OpenApiDataTypeFloat {
   396  			startRet = float32(*min)
   397  		} else if typ == consts.OpenApiDataTypeDouble {
   398  			startRet = *min
   399  		} else if typ == consts.OpenApiDataTypeByte {
   400  			startRet = int(*min)
   401  		}
   402  	}
   403  
   404  	if max != nil {
   405  		if typ == consts.OpenApiDataTypeInteger {
   406  			endRet = int(*max)
   407  		} else if typ == consts.OpenApiDataTypeLong {
   408  			endRet = int64(*max)
   409  		} else if typ == consts.OpenApiDataTypeFloat {
   410  			endRet = float32(*max)
   411  		} else if typ == consts.OpenApiDataTypeDouble {
   412  			endRet = *max
   413  		} else if typ == consts.OpenApiDataTypeByte {
   414  			endRet = int(*max)
   415  		}
   416  	}
   417  
   418  	return
   419  }
   420  
   421  func (s *MockService) getExample(exampleNode interface{}, sample *map[string]string) {
   422  	bytes, _ := json.Marshal(exampleNode)
   423  	(*sample)["example"] = string(bytes)
   424  }
   425  
   426  func (s *MockService) getExamples(exampleNodes openapi3.Examples, sample *map[string]string) {
   427  	for key, val := range exampleNodes {
   428  		bytes, _ := json.Marshal(val.Value.Value)
   429  		(*sample)[key] = string(bytes)
   430  	}
   431  }