github.com/clubpay/ronykit/kit@v0.14.4-0.20240515065620-d0dace45cbc7/desc/stub.go (about)

     1  package desc
     2  
     3  import (
     4  	"reflect"
     5  	"strings"
     6  
     7  	"github.com/clubpay/ronykit/kit"
     8  	"github.com/clubpay/ronykit/kit/errors"
     9  )
    10  
    11  // DTO represents the description of Data Transfer Object of the Stub
    12  type DTO struct {
    13  	// Comments could be used by generators to print some useful information about this DTO
    14  	Comments []string
    15  	// Name is the name of this DTO struct
    16  	Name   string
    17  	Type   string
    18  	RType  reflect.Type
    19  	IsErr  bool
    20  	Fields []DTOField
    21  }
    22  
    23  func (dto DTO) CodeField() string {
    24  	var fn string
    25  	for _, f := range dto.Fields {
    26  		x := strings.ToLower(f.Name)
    27  		if f.Type != "int" {
    28  			continue
    29  		}
    30  		if x == "code" {
    31  			return f.Name
    32  		}
    33  		if strings.HasPrefix(f.Name, "code") {
    34  			fn = f.Name
    35  		}
    36  	}
    37  
    38  	return fn
    39  }
    40  
    41  func (dto DTO) ItemField() string {
    42  	var fn string
    43  	for _, f := range dto.Fields {
    44  		x := strings.ToLower(f.Name)
    45  		if f.RType.Kind() != reflect.String {
    46  			continue
    47  		}
    48  		if x == "item" || x == "items" {
    49  			return f.Name
    50  		}
    51  		if strings.HasPrefix(f.Name, "item") {
    52  			fn = f.Name
    53  		}
    54  	}
    55  
    56  	return fn
    57  }
    58  
    59  // DTOField represents description of a field of the DTO
    60  type DTOField struct {
    61  	// Name of this field
    62  	Name string
    63  	// RType is the reflected type of this field for handling more complex cases
    64  	RType reflect.Type
    65  	// Type of this field and if this type is slice or map then it might have one or two
    66  	// subtypes
    67  	Type     string
    68  	SubType1 string
    69  	SubType2 string
    70  	// If this field was an embedded field means fields are coming from an embedded DTO
    71  	// If Embedded is TRUE then for sure IsDTO must be TRUE
    72  	Embedded bool
    73  	IsDTO    bool
    74  	IsPtr    bool
    75  	Tags     []DTOFieldTag
    76  }
    77  
    78  func (x DTOField) GetTag(name string) string {
    79  	for _, t := range x.Tags {
    80  		if t.Name == name {
    81  			return t.Value
    82  		}
    83  	}
    84  
    85  	return ""
    86  }
    87  
    88  // DTOFieldTag represents description of a tag of the DTOField
    89  type DTOFieldTag struct {
    90  	Name  string
    91  	Value string
    92  }
    93  
    94  // ErrorDTO represents description of a Data Object Transfer which is used to
    95  // show an error case.
    96  type ErrorDTO struct {
    97  	Code int
    98  	Item string
    99  	DTO  DTO
   100  }
   101  
   102  // RESTMethod represents description of a Contract with kit.RESTRouteSelector.
   103  type RESTMethod struct {
   104  	Name           string
   105  	Method         string
   106  	Path           string
   107  	Encoding       string
   108  	Request        DTO
   109  	Response       DTO
   110  	PossibleErrors []ErrorDTO
   111  }
   112  
   113  func (rm *RESTMethod) addPossibleError(dto ErrorDTO) {
   114  	for _, e := range rm.PossibleErrors {
   115  		if e.Code == dto.Code {
   116  			return
   117  		}
   118  	}
   119  	rm.PossibleErrors = append(rm.PossibleErrors, dto)
   120  }
   121  
   122  // RPCMethod represents description of a Contract with kit.RPCRouteSelector
   123  type RPCMethod struct {
   124  	Name           string
   125  	Predicate      string
   126  	Request        DTO
   127  	Response       DTO
   128  	PossibleErrors []ErrorDTO
   129  	Encoding       string
   130  	kit.IncomingRPCContainer
   131  	kit.OutgoingRPCContainer
   132  }
   133  
   134  func (rm *RPCMethod) addPossibleError(dto ErrorDTO) {
   135  	for _, e := range rm.PossibleErrors {
   136  		if e.Code == dto.Code {
   137  			return
   138  		}
   139  	}
   140  	rm.PossibleErrors = append(rm.PossibleErrors, dto)
   141  }
   142  
   143  // Stub represents description of a stub of the service described by Service descriptor.
   144  type Stub struct {
   145  	tags  []string
   146  	DTOs  map[string]DTO
   147  	RESTs []RESTMethod
   148  	RPCs  []RPCMethod
   149  }
   150  
   151  func newStub(tags ...string) *Stub {
   152  	return &Stub{
   153  		tags: tags,
   154  		DTOs: map[string]DTO{},
   155  	}
   156  }
   157  
   158  //nolint:gocognit
   159  func (d *Stub) addDTO(mTyp reflect.Type, isErr bool) error {
   160  	if mTyp.Kind() == reflect.Ptr {
   161  		mTyp = mTyp.Elem()
   162  	}
   163  
   164  	dto := DTO{
   165  		Name:  mTyp.Name(),
   166  		RType: mTyp,
   167  		Type:  typ("", mTyp),
   168  		IsErr: isErr,
   169  	}
   170  
   171  	if mTyp == reflect.TypeOf(kit.RawMessage{}) {
   172  		return nil
   173  	}
   174  
   175  	// We don't support non-struct DTOs
   176  	if mTyp.Kind() != reflect.Struct {
   177  		return errUnsupportedType(mTyp.Kind().String())
   178  	}
   179  
   180  	// if DTO is already parsed just return
   181  	if _, ok := d.DTOs[dto.Name]; ok {
   182  		return nil
   183  	}
   184  
   185  	d.DTOs[dto.Name] = dto
   186  
   187  	for i := 0; i < mTyp.NumField(); i++ {
   188  		ft := mTyp.Field(i)
   189  		fe := extractElem(ft)
   190  		dtoF := DTOField{
   191  			Name:     ft.Name,
   192  			RType:    ft.Type,
   193  			IsPtr:    ft.Type.Kind() == reflect.Ptr,
   194  			Type:     typ("", ft.Type),
   195  			Embedded: ft.Anonymous,
   196  		}
   197  
   198  		for _, t := range d.tags {
   199  			v, ok := ft.Tag.Lookup(t)
   200  			if ok {
   201  				dtoF.Tags = append(
   202  					dtoF.Tags,
   203  					DTOFieldTag{
   204  						Name:  t,
   205  						Value: v,
   206  					},
   207  				)
   208  			}
   209  		}
   210  
   211  		switch fe.Kind() {
   212  		case reflect.Struct:
   213  			dtoF.IsDTO = true
   214  			err := d.addDTO(fe, false)
   215  			if err != nil {
   216  				return err
   217  			}
   218  		case reflect.Map:
   219  			dtoF.SubType1 = typ("", fe.Key())
   220  			dtoF.SubType2 = typ("", fe.Elem())
   221  			if fe.Key().Kind() == reflect.Struct {
   222  				err := d.addDTO(fe.Key(), false)
   223  				if err != nil {
   224  					return err
   225  				}
   226  			}
   227  			if fe.Elem().Kind() == reflect.Struct {
   228  				err := d.addDTO(fe.Elem(), false)
   229  				if err != nil {
   230  					return err
   231  				}
   232  			}
   233  		case reflect.Slice, reflect.Array:
   234  			dtoF.SubType1 = typ("", fe.Elem())
   235  			if fe.Elem().Kind() == reflect.Struct {
   236  				err := d.addDTO(fe.Elem(), false)
   237  				if err != nil {
   238  					return err
   239  				}
   240  			}
   241  		case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
   242  			reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64,
   243  			reflect.Float32, reflect.Float64, reflect.Bool,
   244  			reflect.String, reflect.Uintptr, reflect.Ptr:
   245  		default:
   246  			continue
   247  		}
   248  
   249  		dto.Fields = append(dto.Fields, dtoF)
   250  	}
   251  
   252  	d.DTOs[dto.Name] = dto
   253  
   254  	return nil
   255  }
   256  
   257  func extractElem(in reflect.StructField) reflect.Type {
   258  	t := in.Type
   259  	k := t.Kind()
   260  
   261  Loop:
   262  	if k == reflect.Ptr {
   263  		return t.Elem()
   264  	}
   265  	if k == reflect.Slice {
   266  		switch t.Elem().Kind() {
   267  		default:
   268  		case reflect.Ptr:
   269  			t = t.Elem()
   270  			k = t.Kind()
   271  
   272  			goto Loop
   273  		}
   274  
   275  		return t.Elem()
   276  	}
   277  
   278  	return t
   279  }
   280  
   281  func (d *Stub) getDTO(mTyp reflect.Type) (DTO, bool) {
   282  	if mTyp.Kind() == reflect.Ptr {
   283  		mTyp = mTyp.Elem()
   284  	}
   285  
   286  	dto, ok := d.DTOs[mTyp.Name()]
   287  
   288  	return dto, ok
   289  }
   290  
   291  func (d Stub) Tags() []string {
   292  	return d.tags
   293  }
   294  
   295  var errUnsupportedType = errors.NewG("non-struct types as DTO : %s")
   296  
   297  func MergeStubs(stubs ...*Stub) *Stub {
   298  	stub := newStub()
   299  
   300  	for _, s := range stubs {
   301  		for _, dto := range s.DTOs {
   302  			stub.DTOs[dto.Name] = dto
   303  		}
   304  
   305  		stub.RESTs = append(stub.RESTs, s.RESTs...)
   306  		stub.RPCs = append(stub.RPCs, s.RPCs...)
   307  		stub.tags = appendUnique(stub.tags, s.tags...)
   308  	}
   309  
   310  	return stub
   311  }
   312  
   313  func appendUnique(slice []string, s ...string) []string {
   314  	for _, z := range s {
   315  		found := false
   316  		for _, x := range slice {
   317  			if z == x {
   318  				found = true
   319  
   320  				break
   321  			}
   322  		}
   323  		if !found {
   324  			slice = append(slice, z)
   325  		}
   326  	}
   327  
   328  	return slice
   329  }