diff --git a/seed/go-fiber/file-upload/.mock/definition/service.yml b/seed/go-fiber/file-upload/.mock/definition/service.yml index 5ae95021c2d..956e6ba73ba 100644 --- a/seed/go-fiber/file-upload/.mock/definition/service.yml +++ b/seed/go-fiber/file-upload/.mock/definition/service.yml @@ -64,6 +64,9 @@ service: bar: type: MyObject content-type: application/json + foobar: + type: optional + content-type: application/json types: Id: string diff --git a/seed/go-fiber/file-upload/service.go b/seed/go-fiber/file-upload/service.go index b4a34d13bed..872da7d7f83 100644 --- a/seed/go-fiber/file-upload/service.go +++ b/seed/go-fiber/file-upload/service.go @@ -93,6 +93,7 @@ func (o ObjectType) Ptr() *ObjectType { } type WithContentTypeRequest struct { - Foo string `json:"foo" url:"-"` - Bar *MyObject `json:"bar,omitempty" url:"-"` + Foo string `json:"foo" url:"-"` + Bar *MyObject `json:"bar,omitempty" url:"-"` + Foobar *MyObject `json:"foobar,omitempty" url:"-"` } diff --git a/seed/go-fiber/grpc-proto-exhaustive/dataservice.go b/seed/go-fiber/grpc-proto-exhaustive/dataservice.go index 5561334298d..f467e2c8996 100644 --- a/seed/go-fiber/grpc-proto-exhaustive/dataservice.go +++ b/seed/go-fiber/grpc-proto-exhaustive/dataservice.go @@ -5,7 +5,7 @@ package api import ( json "encoding/json" fmt "fmt" - core "github.com/grpc-proto-exhaustive/fern/core" + internal "github.com/grpc-proto-exhaustive/fern/internal" ) type DeleteRequest struct { @@ -92,7 +92,7 @@ func (c *Column) UnmarshalJSON(data []byte) error { } *c = Column(value) - extraProperties, err := core.ExtractExtraProperties(data, *c) + extraProperties, err := internal.ExtractExtraProperties(data, *c) if err != nil { return err } @@ -102,7 +102,7 @@ func (c *Column) UnmarshalJSON(data []byte) error { } func (c *Column) String() string { - if value, err := core.StringifyJSON(c); err == nil { + if value, err := internal.StringifyJSON(c); err == nil { return value } return fmt.Sprintf("%#v", c) @@ -124,7 +124,7 @@ func (d *DeleteResponse) UnmarshalJSON(data []byte) error { } *d = DeleteResponse(value) - extraProperties, err := core.ExtractExtraProperties(data, *d) + extraProperties, err := internal.ExtractExtraProperties(data, *d) if err != nil { return err } @@ -134,7 +134,7 @@ func (d *DeleteResponse) UnmarshalJSON(data []byte) error { } func (d *DeleteResponse) String() string { - if value, err := core.StringifyJSON(d); err == nil { + if value, err := internal.StringifyJSON(d); err == nil { return value } return fmt.Sprintf("%#v", d) @@ -189,7 +189,7 @@ func (d *DescribeResponse) UnmarshalJSON(data []byte) error { } *d = DescribeResponse(value) - extraProperties, err := core.ExtractExtraProperties(data, *d) + extraProperties, err := internal.ExtractExtraProperties(data, *d) if err != nil { return err } @@ -199,7 +199,7 @@ func (d *DescribeResponse) UnmarshalJSON(data []byte) error { } func (d *DescribeResponse) String() string { - if value, err := core.StringifyJSON(d); err == nil { + if value, err := internal.StringifyJSON(d); err == nil { return value } return fmt.Sprintf("%#v", d) @@ -246,7 +246,7 @@ func (f *FetchResponse) UnmarshalJSON(data []byte) error { } *f = FetchResponse(value) - extraProperties, err := core.ExtractExtraProperties(data, *f) + extraProperties, err := internal.ExtractExtraProperties(data, *f) if err != nil { return err } @@ -256,7 +256,7 @@ func (f *FetchResponse) UnmarshalJSON(data []byte) error { } func (f *FetchResponse) String() string { - if value, err := core.StringifyJSON(f); err == nil { + if value, err := internal.StringifyJSON(f); err == nil { return value } return fmt.Sprintf("%#v", f) @@ -295,7 +295,7 @@ func (i *IndexedData) UnmarshalJSON(data []byte) error { } *i = IndexedData(value) - extraProperties, err := core.ExtractExtraProperties(data, *i) + extraProperties, err := internal.ExtractExtraProperties(data, *i) if err != nil { return err } @@ -305,7 +305,7 @@ func (i *IndexedData) UnmarshalJSON(data []byte) error { } func (i *IndexedData) String() string { - if value, err := core.StringifyJSON(i); err == nil { + if value, err := internal.StringifyJSON(i); err == nil { return value } return fmt.Sprintf("%#v", i) @@ -336,7 +336,7 @@ func (l *ListElement) UnmarshalJSON(data []byte) error { } *l = ListElement(value) - extraProperties, err := core.ExtractExtraProperties(data, *l) + extraProperties, err := internal.ExtractExtraProperties(data, *l) if err != nil { return err } @@ -346,7 +346,7 @@ func (l *ListElement) UnmarshalJSON(data []byte) error { } func (l *ListElement) String() string { - if value, err := core.StringifyJSON(l); err == nil { + if value, err := internal.StringifyJSON(l); err == nil { return value } return fmt.Sprintf("%#v", l) @@ -401,7 +401,7 @@ func (l *ListResponse) UnmarshalJSON(data []byte) error { } *l = ListResponse(value) - extraProperties, err := core.ExtractExtraProperties(data, *l) + extraProperties, err := internal.ExtractExtraProperties(data, *l) if err != nil { return err } @@ -411,7 +411,7 @@ func (l *ListResponse) UnmarshalJSON(data []byte) error { } func (l *ListResponse) String() string { - if value, err := core.StringifyJSON(l); err == nil { + if value, err := internal.StringifyJSON(l); err == nil { return value } return fmt.Sprintf("%#v", l) @@ -607,7 +607,7 @@ func (n *NamespaceSummary) UnmarshalJSON(data []byte) error { } *n = NamespaceSummary(value) - extraProperties, err := core.ExtractExtraProperties(data, *n) + extraProperties, err := internal.ExtractExtraProperties(data, *n) if err != nil { return err } @@ -617,7 +617,7 @@ func (n *NamespaceSummary) UnmarshalJSON(data []byte) error { } func (n *NamespaceSummary) String() string { - if value, err := core.StringifyJSON(n); err == nil { + if value, err := internal.StringifyJSON(n); err == nil { return value } return fmt.Sprintf("%#v", n) @@ -648,7 +648,7 @@ func (p *Pagination) UnmarshalJSON(data []byte) error { } *p = Pagination(value) - extraProperties, err := core.ExtractExtraProperties(data, *p) + extraProperties, err := internal.ExtractExtraProperties(data, *p) if err != nil { return err } @@ -658,7 +658,7 @@ func (p *Pagination) UnmarshalJSON(data []byte) error { } func (p *Pagination) String() string { - if value, err := core.StringifyJSON(p); err == nil { + if value, err := internal.StringifyJSON(p); err == nil { return value } return fmt.Sprintf("%#v", p) @@ -721,7 +721,7 @@ func (q *QueryColumn) UnmarshalJSON(data []byte) error { } *q = QueryColumn(value) - extraProperties, err := core.ExtractExtraProperties(data, *q) + extraProperties, err := internal.ExtractExtraProperties(data, *q) if err != nil { return err } @@ -731,7 +731,7 @@ func (q *QueryColumn) UnmarshalJSON(data []byte) error { } func (q *QueryColumn) String() string { - if value, err := core.StringifyJSON(q); err == nil { + if value, err := internal.StringifyJSON(q); err == nil { return value } return fmt.Sprintf("%#v", q) @@ -786,7 +786,7 @@ func (q *QueryResponse) UnmarshalJSON(data []byte) error { } *q = QueryResponse(value) - extraProperties, err := core.ExtractExtraProperties(data, *q) + extraProperties, err := internal.ExtractExtraProperties(data, *q) if err != nil { return err } @@ -796,7 +796,7 @@ func (q *QueryResponse) UnmarshalJSON(data []byte) error { } func (q *QueryResponse) String() string { - if value, err := core.StringifyJSON(q); err == nil { + if value, err := internal.StringifyJSON(q); err == nil { return value } return fmt.Sprintf("%#v", q) @@ -835,7 +835,7 @@ func (q *QueryResult) UnmarshalJSON(data []byte) error { } *q = QueryResult(value) - extraProperties, err := core.ExtractExtraProperties(data, *q) + extraProperties, err := internal.ExtractExtraProperties(data, *q) if err != nil { return err } @@ -845,7 +845,7 @@ func (q *QueryResult) UnmarshalJSON(data []byte) error { } func (q *QueryResult) String() string { - if value, err := core.StringifyJSON(q); err == nil { + if value, err := internal.StringifyJSON(q); err == nil { return value } return fmt.Sprintf("%#v", q) @@ -908,7 +908,7 @@ func (s *ScoredColumn) UnmarshalJSON(data []byte) error { } *s = ScoredColumn(value) - extraProperties, err := core.ExtractExtraProperties(data, *s) + extraProperties, err := internal.ExtractExtraProperties(data, *s) if err != nil { return err } @@ -918,7 +918,7 @@ func (s *ScoredColumn) UnmarshalJSON(data []byte) error { } func (s *ScoredColumn) String() string { - if value, err := core.StringifyJSON(s); err == nil { + if value, err := internal.StringifyJSON(s); err == nil { return value } return fmt.Sprintf("%#v", s) @@ -940,7 +940,7 @@ func (u *UpdateResponse) UnmarshalJSON(data []byte) error { } *u = UpdateResponse(value) - extraProperties, err := core.ExtractExtraProperties(data, *u) + extraProperties, err := internal.ExtractExtraProperties(data, *u) if err != nil { return err } @@ -950,7 +950,7 @@ func (u *UpdateResponse) UnmarshalJSON(data []byte) error { } func (u *UpdateResponse) String() string { - if value, err := core.StringifyJSON(u); err == nil { + if value, err := internal.StringifyJSON(u); err == nil { return value } return fmt.Sprintf("%#v", u) @@ -981,7 +981,7 @@ func (u *UploadResponse) UnmarshalJSON(data []byte) error { } *u = UploadResponse(value) - extraProperties, err := core.ExtractExtraProperties(data, *u) + extraProperties, err := internal.ExtractExtraProperties(data, *u) if err != nil { return err } @@ -991,7 +991,7 @@ func (u *UploadResponse) UnmarshalJSON(data []byte) error { } func (u *UploadResponse) String() string { - if value, err := core.StringifyJSON(u); err == nil { + if value, err := internal.StringifyJSON(u); err == nil { return value } return fmt.Sprintf("%#v", u) @@ -1022,7 +1022,7 @@ func (u *Usage) UnmarshalJSON(data []byte) error { } *u = Usage(value) - extraProperties, err := core.ExtractExtraProperties(data, *u) + extraProperties, err := internal.ExtractExtraProperties(data, *u) if err != nil { return err } @@ -1032,7 +1032,7 @@ func (u *Usage) UnmarshalJSON(data []byte) error { } func (u *Usage) String() string { - if value, err := core.StringifyJSON(u); err == nil { + if value, err := internal.StringifyJSON(u); err == nil { return value } return fmt.Sprintf("%#v", u) diff --git a/seed/go-fiber/grpc-proto-exhaustive/core/extra_properties.go b/seed/go-fiber/grpc-proto-exhaustive/internal/extra_properties.go similarity index 99% rename from seed/go-fiber/grpc-proto-exhaustive/core/extra_properties.go rename to seed/go-fiber/grpc-proto-exhaustive/internal/extra_properties.go index a6af3e12410..540c3fd89ee 100644 --- a/seed/go-fiber/grpc-proto-exhaustive/core/extra_properties.go +++ b/seed/go-fiber/grpc-proto-exhaustive/internal/extra_properties.go @@ -1,4 +1,4 @@ -package core +package internal import ( "bytes" diff --git a/seed/go-sdk/grpc-proto-exhaustive/core/extra_properties_test.go b/seed/go-fiber/grpc-proto-exhaustive/internal/extra_properties_test.go similarity index 99% rename from seed/go-sdk/grpc-proto-exhaustive/core/extra_properties_test.go rename to seed/go-fiber/grpc-proto-exhaustive/internal/extra_properties_test.go index dc66fccd7f1..aa2510ee512 100644 --- a/seed/go-sdk/grpc-proto-exhaustive/core/extra_properties_test.go +++ b/seed/go-fiber/grpc-proto-exhaustive/internal/extra_properties_test.go @@ -1,4 +1,4 @@ -package core +package internal import ( "encoding/json" diff --git a/seed/go-fiber/grpc-proto/core/stringer.go b/seed/go-fiber/grpc-proto-exhaustive/internal/stringer.go similarity index 94% rename from seed/go-fiber/grpc-proto/core/stringer.go rename to seed/go-fiber/grpc-proto-exhaustive/internal/stringer.go index 000cf448641..312801851e0 100644 --- a/seed/go-fiber/grpc-proto/core/stringer.go +++ b/seed/go-fiber/grpc-proto-exhaustive/internal/stringer.go @@ -1,4 +1,4 @@ -package core +package internal import "encoding/json" diff --git a/seed/go-sdk/grpc-proto-exhaustive/core/time.go b/seed/go-fiber/grpc-proto-exhaustive/internal/time.go similarity index 99% rename from seed/go-sdk/grpc-proto-exhaustive/core/time.go rename to seed/go-fiber/grpc-proto-exhaustive/internal/time.go index d009ab30c90..ab0e269fade 100644 --- a/seed/go-sdk/grpc-proto-exhaustive/core/time.go +++ b/seed/go-fiber/grpc-proto-exhaustive/internal/time.go @@ -1,4 +1,4 @@ -package core +package internal import ( "encoding/json" diff --git a/seed/go-sdk/grpc-proto-exhaustive/core/extra_properties.go b/seed/go-fiber/grpc-proto/internal/extra_properties.go similarity index 99% rename from seed/go-sdk/grpc-proto-exhaustive/core/extra_properties.go rename to seed/go-fiber/grpc-proto/internal/extra_properties.go index a6af3e12410..540c3fd89ee 100644 --- a/seed/go-sdk/grpc-proto-exhaustive/core/extra_properties.go +++ b/seed/go-fiber/grpc-proto/internal/extra_properties.go @@ -1,4 +1,4 @@ -package core +package internal import ( "bytes" diff --git a/seed/go-fiber/grpc-proto-exhaustive/core/extra_properties_test.go b/seed/go-fiber/grpc-proto/internal/extra_properties_test.go similarity index 99% rename from seed/go-fiber/grpc-proto-exhaustive/core/extra_properties_test.go rename to seed/go-fiber/grpc-proto/internal/extra_properties_test.go index dc66fccd7f1..aa2510ee512 100644 --- a/seed/go-fiber/grpc-proto-exhaustive/core/extra_properties_test.go +++ b/seed/go-fiber/grpc-proto/internal/extra_properties_test.go @@ -1,4 +1,4 @@ -package core +package internal import ( "encoding/json" diff --git a/seed/go-sdk/grpc-proto-exhaustive/core/stringer.go b/seed/go-fiber/grpc-proto/internal/stringer.go similarity index 94% rename from seed/go-sdk/grpc-proto-exhaustive/core/stringer.go rename to seed/go-fiber/grpc-proto/internal/stringer.go index 000cf448641..312801851e0 100644 --- a/seed/go-sdk/grpc-proto-exhaustive/core/stringer.go +++ b/seed/go-fiber/grpc-proto/internal/stringer.go @@ -1,4 +1,4 @@ -package core +package internal import "encoding/json" diff --git a/seed/go-sdk/grpc-proto/core/time.go b/seed/go-fiber/grpc-proto/internal/time.go similarity index 99% rename from seed/go-sdk/grpc-proto/core/time.go rename to seed/go-fiber/grpc-proto/internal/time.go index d009ab30c90..ab0e269fade 100644 --- a/seed/go-sdk/grpc-proto/core/time.go +++ b/seed/go-fiber/grpc-proto/internal/time.go @@ -1,4 +1,4 @@ -package core +package internal import ( "encoding/json" diff --git a/seed/go-fiber/grpc-proto/userservice.go b/seed/go-fiber/grpc-proto/userservice.go index b5edcc6cdb1..17d8924847f 100644 --- a/seed/go-fiber/grpc-proto/userservice.go +++ b/seed/go-fiber/grpc-proto/userservice.go @@ -5,7 +5,7 @@ package api import ( json "encoding/json" fmt "fmt" - core "github.com/grpc-proto/fern/core" + internal "github.com/grpc-proto/fern/internal" ) type CreateRequest struct { @@ -41,7 +41,7 @@ func (c *CreateResponse) UnmarshalJSON(data []byte) error { } *c = CreateResponse(value) - extraProperties, err := core.ExtractExtraProperties(data, *c) + extraProperties, err := internal.ExtractExtraProperties(data, *c) if err != nil { return err } @@ -51,7 +51,7 @@ func (c *CreateResponse) UnmarshalJSON(data []byte) error { } func (c *CreateResponse) String() string { - if value, err := core.StringifyJSON(c); err == nil { + if value, err := internal.StringifyJSON(c); err == nil { return value } return fmt.Sprintf("%#v", c) @@ -279,7 +279,7 @@ func (u *UserModel) UnmarshalJSON(data []byte) error { } *u = UserModel(value) - extraProperties, err := core.ExtractExtraProperties(data, *u) + extraProperties, err := internal.ExtractExtraProperties(data, *u) if err != nil { return err } @@ -289,7 +289,7 @@ func (u *UserModel) UnmarshalJSON(data []byte) error { } func (u *UserModel) String() string { - if value, err := core.StringifyJSON(u); err == nil { + if value, err := internal.StringifyJSON(u); err == nil { return value } return fmt.Sprintf("%#v", u) diff --git a/seed/go-sdk/file-upload/inline-file-properties/.mock/definition/service.yml b/seed/go-sdk/file-upload/inline-file-properties/.mock/definition/service.yml index 5ae95021c2d..956e6ba73ba 100644 --- a/seed/go-sdk/file-upload/inline-file-properties/.mock/definition/service.yml +++ b/seed/go-sdk/file-upload/inline-file-properties/.mock/definition/service.yml @@ -64,6 +64,9 @@ service: bar: type: MyObject content-type: application/json + foobar: + type: optional + content-type: application/json types: Id: string diff --git a/seed/go-sdk/file-upload/inline-file-properties/service.go b/seed/go-sdk/file-upload/inline-file-properties/service.go index 0efd41c250a..83bc08b4574 100644 --- a/seed/go-sdk/file-upload/inline-file-properties/service.go +++ b/seed/go-sdk/file-upload/inline-file-properties/service.go @@ -110,7 +110,8 @@ func (o ObjectType) Ptr() *ObjectType { } type WithContentTypeRequest struct { - File io.Reader `json:"-" url:"-"` - Foo string `json:"foo" url:"-"` - Bar *MyObject `json:"bar,omitempty" url:"-"` + File io.Reader `json:"-" url:"-"` + Foo string `json:"foo" url:"-"` + Bar *MyObject `json:"bar,omitempty" url:"-"` + Foobar *MyObject `json:"foobar,omitempty" url:"-"` } diff --git a/seed/go-sdk/file-upload/inline-file-properties/service/client.go b/seed/go-sdk/file-upload/inline-file-properties/service/client.go index 2a51b655839..a8691e886eb 100644 --- a/seed/go-sdk/file-upload/inline-file-properties/service/client.go +++ b/seed/go-sdk/file-upload/inline-file-properties/service/client.go @@ -256,6 +256,11 @@ func (c *Client) WithContentType( if err := writer.WriteJSON("bar", request.Bar, internal.WithDefaultContentType("application/json")); err != nil { return err } + if request.Foobar != nil { + if err := writer.WriteJSON("foobar", request.Foobar, internal.WithDefaultContentType("application/json")); err != nil { + return err + } + } if err := writer.Close(); err != nil { return err } diff --git a/seed/go-sdk/file-upload/no-custom-config/.mock/definition/service.yml b/seed/go-sdk/file-upload/no-custom-config/.mock/definition/service.yml index 5ae95021c2d..956e6ba73ba 100644 --- a/seed/go-sdk/file-upload/no-custom-config/.mock/definition/service.yml +++ b/seed/go-sdk/file-upload/no-custom-config/.mock/definition/service.yml @@ -64,6 +64,9 @@ service: bar: type: MyObject content-type: application/json + foobar: + type: optional + content-type: application/json types: Id: string diff --git a/seed/go-sdk/file-upload/no-custom-config/service.go b/seed/go-sdk/file-upload/no-custom-config/service.go index 50b08ca5758..a0a2d481d46 100644 --- a/seed/go-sdk/file-upload/no-custom-config/service.go +++ b/seed/go-sdk/file-upload/no-custom-config/service.go @@ -100,6 +100,7 @@ func (o ObjectType) Ptr() *ObjectType { } type WithContentTypeRequest struct { - Foo string `json:"foo" url:"-"` - Bar *MyObject `json:"bar,omitempty" url:"-"` + Foo string `json:"foo" url:"-"` + Bar *MyObject `json:"bar,omitempty" url:"-"` + Foobar *MyObject `json:"foobar,omitempty" url:"-"` } diff --git a/seed/go-sdk/file-upload/no-custom-config/service/client.go b/seed/go-sdk/file-upload/no-custom-config/service/client.go index fb2c35f2abc..dec72252ed8 100644 --- a/seed/go-sdk/file-upload/no-custom-config/service/client.go +++ b/seed/go-sdk/file-upload/no-custom-config/service/client.go @@ -263,6 +263,11 @@ func (c *Client) WithContentType( if err := writer.WriteJSON("bar", request.Bar, internal.WithDefaultContentType("application/json")); err != nil { return err } + if request.Foobar != nil { + if err := writer.WriteJSON("foobar", request.Foobar, internal.WithDefaultContentType("application/json")); err != nil { + return err + } + } if err := writer.Close(); err != nil { return err } diff --git a/seed/go-sdk/grpc-proto-exhaustive/client/client.go b/seed/go-sdk/grpc-proto-exhaustive/client/client.go index 29316fd9cb8..3f7416c0368 100644 --- a/seed/go-sdk/grpc-proto-exhaustive/client/client.go +++ b/seed/go-sdk/grpc-proto-exhaustive/client/client.go @@ -5,13 +5,14 @@ package client import ( core "github.com/grpc-proto-exhaustive/fern/core" dataservice "github.com/grpc-proto-exhaustive/fern/dataservice" + internal "github.com/grpc-proto-exhaustive/fern/internal" option "github.com/grpc-proto-exhaustive/fern/option" http "net/http" ) type Client struct { baseURL string - caller *core.Caller + caller *internal.Caller header http.Header Dataservice *dataservice.Client @@ -21,8 +22,8 @@ func NewClient(opts ...option.RequestOption) *Client { options := core.NewRequestOptions(opts...) return &Client{ baseURL: options.BaseURL, - caller: core.NewCaller( - &core.CallerParams{ + caller: internal.NewCaller( + &internal.CallerParams{ Client: options.HTTPClient, MaxAttempts: options.MaxAttempts, }, diff --git a/seed/go-sdk/grpc-proto-exhaustive/core/api_error.go b/seed/go-sdk/grpc-proto-exhaustive/core/api_error.go new file mode 100644 index 00000000000..dc4190ca1cd --- /dev/null +++ b/seed/go-sdk/grpc-proto-exhaustive/core/api_error.go @@ -0,0 +1,42 @@ +package core + +import "fmt" + +// APIError is a lightweight wrapper around the standard error +// interface that preserves the status code from the RPC, if any. +type APIError struct { + err error + + StatusCode int `json:"-"` +} + +// NewAPIError constructs a new API error. +func NewAPIError(statusCode int, err error) *APIError { + return &APIError{ + err: err, + StatusCode: statusCode, + } +} + +// Unwrap returns the underlying error. This also makes the error compatible +// with errors.As and errors.Is. +func (a *APIError) Unwrap() error { + if a == nil { + return nil + } + return a.err +} + +// Error returns the API error's message. +func (a *APIError) Error() string { + if a == nil || (a.err == nil && a.StatusCode == 0) { + return "" + } + if a.err == nil { + return fmt.Sprintf("%d", a.StatusCode) + } + if a.StatusCode == 0 { + return a.err.Error() + } + return fmt.Sprintf("%d: %s", a.StatusCode, a.err.Error()) +} diff --git a/seed/go-sdk/grpc-proto-exhaustive/core/http.go b/seed/go-sdk/grpc-proto-exhaustive/core/http.go new file mode 100644 index 00000000000..b553350b84e --- /dev/null +++ b/seed/go-sdk/grpc-proto-exhaustive/core/http.go @@ -0,0 +1,8 @@ +package core + +import "net/http" + +// HTTPClient is an interface for a subset of the *http.Client. +type HTTPClient interface { + Do(*http.Request) (*http.Response, error) +} diff --git a/seed/go-sdk/grpc-proto-exhaustive/core/multipart.go b/seed/go-sdk/grpc-proto-exhaustive/core/multipart.go deleted file mode 100644 index 40f84bcec76..00000000000 --- a/seed/go-sdk/grpc-proto-exhaustive/core/multipart.go +++ /dev/null @@ -1,195 +0,0 @@ -package core - -import ( - "bytes" - "encoding/json" - "fmt" - "io" - "mime/multipart" - "net/textproto" - "strings" -) - -// Named is implemented by types that define a name. -type Named interface { - Name() string -} - -// ContentTyped is implemented by types that define a Content-Type. -type ContentTyped interface { - ContentType() string -} - -// WriteMultipartOption adapts the behavior of the multipart writer. -type WriteMultipartOption func(*writeMultipartOptions) - -// WithMultipartContentType sets the Content-Type for the multipart writer. -func WithMultipartContentType(contentType string) WriteMultipartOption { - return func(options *writeMultipartOptions) { - options.contentType = contentType - } -} - -// MultipartWriter writes multipart/form-data requests. -type MultipartWriter struct { - buffer *bytes.Buffer - writer *multipart.Writer -} - -// NewMultipartWriter creates a new multipart writer. -func NewMultipartWriter() *MultipartWriter { - buffer := bytes.NewBuffer(nil) - return &MultipartWriter{ - buffer: buffer, - writer: multipart.NewWriter(buffer), - } -} - -// Buffer returns the underlying buffer. -func (w *MultipartWriter) Buffer() *bytes.Buffer { - return w.buffer -} - -// ContentType returns the Content-Type for an HTTP multipart/form-data. -func (w *MultipartWriter) ContentType() string { - return w.writer.FormDataContentType() -} - -// WriteFile writes the given file part. -func (w *MultipartWriter) WriteFile( - field string, - file io.Reader, - opts ...WriteMultipartOption, -) error { - options := newWriteMultipartOptions(opts...) - return w.writeFile(field, file, options.contentType) -} - -// WriteField writes the given value as a form field. -func (w *MultipartWriter) WriteField( - field string, - value string, - opts ...WriteMultipartOption, -) error { - options := newWriteMultipartOptions(opts...) - return w.writeField(field, value, options.contentType) -} - -// WriteJSON writes the given value as a JSON form field. -func (w *MultipartWriter) WriteJSON( - field string, - value interface{}, - opts ...WriteMultipartOption, -) error { - bytes, err := json.Marshal(value) - if err != nil { - return err - } - return w.WriteField(field, string(bytes), opts...) -} - -// Close closes the writer. -func (w *MultipartWriter) Close() error { - return w.writer.Close() -} - -func (w *MultipartWriter) writeField( - field string, - value string, - contentType string, -) error { - part, err := w.newFormField(field, contentType) - if err != nil { - return err - } - _, err = part.Write([]byte(value)) - return err -} - -func (w *MultipartWriter) writeFile( - field string, - file io.Reader, - contentType string, -) error { - filename := getFilename(file) - if contentType == "" { - contentType = getContentType(file) - } - part, err := w.newFormPart(field, filename, contentType) - if err != nil { - return err - } - _, err = io.Copy(part, file) - return err -} - -// newFormField creates a new form field. -func (w *MultipartWriter) newFormField( - field string, - contentType string, -) (io.Writer, error) { - return w.newFormPart(field, "" /* filename */, contentType) -} - -// newFormPart creates a new form data part. -func (w *MultipartWriter) newFormPart( - field string, - filename string, - contentType string, -) (io.Writer, error) { - h := make(textproto.MIMEHeader) - h.Set("Content-Disposition", getContentDispositionHeaderValue(field, filename)) - if contentType != "" { - h.Set("Content-Type", contentType) - } - return w.writer.CreatePart(h) -} - -// writeMultipartOptions are options used to adapt the behavior of the multipart writer. -type writeMultipartOptions struct { - contentType string -} - -// newWriteMultipartOptions returns a new write multipart options. -func newWriteMultipartOptions(opts ...WriteMultipartOption) *writeMultipartOptions { - options := new(writeMultipartOptions) - for _, opt := range opts { - opt(options) - } - return options -} - -// getContentType returns the Content-Type for the given file, if any. -func getContentType(file io.Reader) string { - if v, ok := file.(ContentTyped); ok { - return v.ContentType() - } - return "" -} - -// getFilename returns the name for the given file, if any. -func getFilename(file io.Reader) string { - if v, ok := file.(Named); ok { - return v.Name() - } - return "" -} - -// getContentDispositionHeaderValue returns the value for the Content-Disposition header. -func getContentDispositionHeaderValue(field string, filename string) string { - contentDisposition := fmt.Sprintf("form-data; name=%q", field) - if filename != "" { - contentDisposition += fmt.Sprintf(`; filename=%q`, escapeQuotes(filename)) - } - return contentDisposition -} - -// https://cs.opensource.google/go/go/+/refs/tags/go1.23.2:src/mime/multipart/writer.go;l=132 -var quoteEscaper = strings.NewReplacer("\\", "\\\\", `"`, "\\\"") - -// escapeQuotes is directly referenced from the standard library. -// -// https://cs.opensource.google/go/go/+/refs/tags/go1.23.2:src/mime/multipart/writer.go;l=134 -func escapeQuotes(s string) string { - return quoteEscaper.Replace(s) -} diff --git a/seed/go-sdk/grpc-proto-exhaustive/core/multipart_test.go b/seed/go-sdk/grpc-proto-exhaustive/core/multipart_test.go deleted file mode 100644 index ba73d413f0c..00000000000 --- a/seed/go-sdk/grpc-proto-exhaustive/core/multipart_test.go +++ /dev/null @@ -1,251 +0,0 @@ -package core - -import ( - "encoding/json" - "io" - "mime/multipart" - "strings" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -const maxFormMemory = 32 << 20 // 32MB - -type mockFile struct { - name string - content string - contentType string - - reader io.Reader -} - -func (f *mockFile) Read(p []byte) (n int, err error) { - if f.reader == nil { - f.reader = strings.NewReader(f.content) - } - return f.reader.Read(p) -} - -func (f *mockFile) Name() string { - return f.name -} - -func (f *mockFile) ContentType() string { - return f.contentType -} - -func TestMultipartWriter(t *testing.T) { - t.Run("empty", func(t *testing.T) { - w := NewMultipartWriter() - assert.NotNil(t, w.Buffer()) - assert.Contains(t, w.ContentType(), "multipart/form-data; boundary=") - require.NoError(t, w.Close()) - }) - - t.Run("write field", func(t *testing.T) { - tests := []struct { - desc string - giveField string - giveValue string - giveContentType string - }{ - { - desc: "empty field", - giveField: "empty", - giveValue: "", - }, - { - desc: "simple field", - giveField: "greeting", - giveValue: "hello world", - }, - { - desc: "field with content type", - giveField: "message", - giveValue: "hello", - giveContentType: "text/plain", - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - var opts []WriteMultipartOption - if tt.giveContentType != "" { - opts = append(opts, WithMultipartContentType(tt.giveContentType)) - } - - require.NoError(t, w.WriteField(tt.giveField, tt.giveValue, opts...)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - - assert.Equal(t, []string{tt.giveValue}, form.Value[tt.giveField]) - require.NoError(t, form.RemoveAll()) - }) - } - }) - - t.Run("write file", func(t *testing.T) { - tests := []struct { - desc string - giveField string - giveFile *mockFile - giveContentType string - }{ - { - desc: "simple file", - giveField: "file", - giveFile: &mockFile{ - name: "test.txt", - content: "hello world", - contentType: "text/plain", - }, - }, - { - desc: "override content type", - giveField: "file", - giveFile: &mockFile{ - name: "test.txt", - content: "hello world", - contentType: "text/plain", - }, - giveContentType: "application/octet-stream", - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - var opts []WriteMultipartOption - if tt.giveContentType != "" { - opts = append(opts, WithMultipartContentType(tt.giveContentType)) - } - - require.NoError(t, w.WriteFile(tt.giveField, tt.giveFile, opts...)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - files := form.File[tt.giveField] - require.Len(t, files, 1) - - file := files[0] - assert.Equal(t, tt.giveFile.name, file.Filename) - - f, err := file.Open() - require.NoError(t, err) - defer func() { - require.NoError(t, f.Close()) - }() - - content, err := io.ReadAll(f) - require.NoError(t, err) - assert.Equal(t, tt.giveFile.content, string(content)) - - expectedContentType := tt.giveContentType - if expectedContentType == "" { - expectedContentType = tt.giveFile.contentType - } - if expectedContentType != "" { - assert.Equal(t, expectedContentType, file.Header.Get("Content-Type")) - } - }) - } - }) - - t.Run("write JSON", func(t *testing.T) { - type testStruct struct { - Name string `json:"name"` - Value int `json:"value"` - } - - tests := []struct { - desc string - giveField string - giveValue interface{} - }{ - { - desc: "struct", - giveField: "data", - giveValue: testStruct{Name: "test", Value: 123}, - }, - { - desc: "map", - giveField: "data", - giveValue: map[string]string{"key": "value"}, - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - require.NoError(t, w.WriteJSON(tt.giveField, tt.giveValue)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - expected, err := json.Marshal(tt.giveValue) - require.NoError(t, err) - assert.Equal(t, []string{string(expected)}, form.Value[tt.giveField]) - }) - } - }) - - t.Run("complex", func(t *testing.T) { - w := NewMultipartWriter() - - // Add multiple fields and files - require.NoError(t, w.WriteField("foo", "bar")) - require.NoError(t, w.WriteField("baz", "qux")) - - hello := mockFile{name: "file.txt", content: "Hello, world!", contentType: "text/plain"} - require.NoError(t, w.WriteFile("file", &hello)) - require.NoError(t, w.WriteJSON("data", map[string]string{"key": "value"})) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - assert.Equal(t, []string{"bar"}, form.Value["foo"]) - assert.Equal(t, []string{"qux"}, form.Value["baz"]) - assert.Equal(t, []string{`{"key":"value"}`}, form.Value["data"]) - - files := form.File["file"] - require.Len(t, files, 1) - - file := files[0] - assert.Equal(t, "file.txt", file.Filename) - - f, err := file.Open() - require.NoError(t, err) - defer func() { - require.NoError(t, f.Close()) - }() - - content, err := io.ReadAll(f) - require.NoError(t, err) - assert.Equal(t, "Hello, world!", string(content)) - }) -} diff --git a/seed/go-sdk/grpc-proto-exhaustive/dataservice.go b/seed/go-sdk/grpc-proto-exhaustive/dataservice.go index 6bfbed803d1..84ee94a71bb 100644 --- a/seed/go-sdk/grpc-proto-exhaustive/dataservice.go +++ b/seed/go-sdk/grpc-proto-exhaustive/dataservice.go @@ -5,7 +5,7 @@ package api import ( json "encoding/json" fmt "fmt" - core "github.com/grpc-proto-exhaustive/fern/core" + internal "github.com/grpc-proto-exhaustive/fern/internal" ) type DeleteRequest struct { @@ -93,7 +93,7 @@ func (c *Column) UnmarshalJSON(data []byte) error { } *c = Column(value) - extraProperties, err := core.ExtractExtraProperties(data, *c) + extraProperties, err := internal.ExtractExtraProperties(data, *c) if err != nil { return err } @@ -105,11 +105,11 @@ func (c *Column) UnmarshalJSON(data []byte) error { func (c *Column) String() string { if len(c._rawJSON) > 0 { - if value, err := core.StringifyJSON(c._rawJSON); err == nil { + if value, err := internal.StringifyJSON(c._rawJSON); err == nil { return value } } - if value, err := core.StringifyJSON(c); err == nil { + if value, err := internal.StringifyJSON(c); err == nil { return value } return fmt.Sprintf("%#v", c) @@ -132,7 +132,7 @@ func (d *DeleteResponse) UnmarshalJSON(data []byte) error { } *d = DeleteResponse(value) - extraProperties, err := core.ExtractExtraProperties(data, *d) + extraProperties, err := internal.ExtractExtraProperties(data, *d) if err != nil { return err } @@ -144,11 +144,11 @@ func (d *DeleteResponse) UnmarshalJSON(data []byte) error { func (d *DeleteResponse) String() string { if len(d._rawJSON) > 0 { - if value, err := core.StringifyJSON(d._rawJSON); err == nil { + if value, err := internal.StringifyJSON(d._rawJSON); err == nil { return value } } - if value, err := core.StringifyJSON(d); err == nil { + if value, err := internal.StringifyJSON(d); err == nil { return value } return fmt.Sprintf("%#v", d) @@ -204,7 +204,7 @@ func (d *DescribeResponse) UnmarshalJSON(data []byte) error { } *d = DescribeResponse(value) - extraProperties, err := core.ExtractExtraProperties(data, *d) + extraProperties, err := internal.ExtractExtraProperties(data, *d) if err != nil { return err } @@ -216,11 +216,11 @@ func (d *DescribeResponse) UnmarshalJSON(data []byte) error { func (d *DescribeResponse) String() string { if len(d._rawJSON) > 0 { - if value, err := core.StringifyJSON(d._rawJSON); err == nil { + if value, err := internal.StringifyJSON(d._rawJSON); err == nil { return value } } - if value, err := core.StringifyJSON(d); err == nil { + if value, err := internal.StringifyJSON(d); err == nil { return value } return fmt.Sprintf("%#v", d) @@ -268,7 +268,7 @@ func (f *FetchResponse) UnmarshalJSON(data []byte) error { } *f = FetchResponse(value) - extraProperties, err := core.ExtractExtraProperties(data, *f) + extraProperties, err := internal.ExtractExtraProperties(data, *f) if err != nil { return err } @@ -280,11 +280,11 @@ func (f *FetchResponse) UnmarshalJSON(data []byte) error { func (f *FetchResponse) String() string { if len(f._rawJSON) > 0 { - if value, err := core.StringifyJSON(f._rawJSON); err == nil { + if value, err := internal.StringifyJSON(f._rawJSON); err == nil { return value } } - if value, err := core.StringifyJSON(f); err == nil { + if value, err := internal.StringifyJSON(f); err == nil { return value } return fmt.Sprintf("%#v", f) @@ -324,7 +324,7 @@ func (i *IndexedData) UnmarshalJSON(data []byte) error { } *i = IndexedData(value) - extraProperties, err := core.ExtractExtraProperties(data, *i) + extraProperties, err := internal.ExtractExtraProperties(data, *i) if err != nil { return err } @@ -336,11 +336,11 @@ func (i *IndexedData) UnmarshalJSON(data []byte) error { func (i *IndexedData) String() string { if len(i._rawJSON) > 0 { - if value, err := core.StringifyJSON(i._rawJSON); err == nil { + if value, err := internal.StringifyJSON(i._rawJSON); err == nil { return value } } - if value, err := core.StringifyJSON(i); err == nil { + if value, err := internal.StringifyJSON(i); err == nil { return value } return fmt.Sprintf("%#v", i) @@ -372,7 +372,7 @@ func (l *ListElement) UnmarshalJSON(data []byte) error { } *l = ListElement(value) - extraProperties, err := core.ExtractExtraProperties(data, *l) + extraProperties, err := internal.ExtractExtraProperties(data, *l) if err != nil { return err } @@ -384,11 +384,11 @@ func (l *ListElement) UnmarshalJSON(data []byte) error { func (l *ListElement) String() string { if len(l._rawJSON) > 0 { - if value, err := core.StringifyJSON(l._rawJSON); err == nil { + if value, err := internal.StringifyJSON(l._rawJSON); err == nil { return value } } - if value, err := core.StringifyJSON(l); err == nil { + if value, err := internal.StringifyJSON(l); err == nil { return value } return fmt.Sprintf("%#v", l) @@ -444,7 +444,7 @@ func (l *ListResponse) UnmarshalJSON(data []byte) error { } *l = ListResponse(value) - extraProperties, err := core.ExtractExtraProperties(data, *l) + extraProperties, err := internal.ExtractExtraProperties(data, *l) if err != nil { return err } @@ -456,11 +456,11 @@ func (l *ListResponse) UnmarshalJSON(data []byte) error { func (l *ListResponse) String() string { if len(l._rawJSON) > 0 { - if value, err := core.StringifyJSON(l._rawJSON); err == nil { + if value, err := internal.StringifyJSON(l._rawJSON); err == nil { return value } } - if value, err := core.StringifyJSON(l); err == nil { + if value, err := internal.StringifyJSON(l); err == nil { return value } return fmt.Sprintf("%#v", l) @@ -657,7 +657,7 @@ func (n *NamespaceSummary) UnmarshalJSON(data []byte) error { } *n = NamespaceSummary(value) - extraProperties, err := core.ExtractExtraProperties(data, *n) + extraProperties, err := internal.ExtractExtraProperties(data, *n) if err != nil { return err } @@ -669,11 +669,11 @@ func (n *NamespaceSummary) UnmarshalJSON(data []byte) error { func (n *NamespaceSummary) String() string { if len(n._rawJSON) > 0 { - if value, err := core.StringifyJSON(n._rawJSON); err == nil { + if value, err := internal.StringifyJSON(n._rawJSON); err == nil { return value } } - if value, err := core.StringifyJSON(n); err == nil { + if value, err := internal.StringifyJSON(n); err == nil { return value } return fmt.Sprintf("%#v", n) @@ -705,7 +705,7 @@ func (p *Pagination) UnmarshalJSON(data []byte) error { } *p = Pagination(value) - extraProperties, err := core.ExtractExtraProperties(data, *p) + extraProperties, err := internal.ExtractExtraProperties(data, *p) if err != nil { return err } @@ -717,11 +717,11 @@ func (p *Pagination) UnmarshalJSON(data []byte) error { func (p *Pagination) String() string { if len(p._rawJSON) > 0 { - if value, err := core.StringifyJSON(p._rawJSON); err == nil { + if value, err := internal.StringifyJSON(p._rawJSON); err == nil { return value } } - if value, err := core.StringifyJSON(p); err == nil { + if value, err := internal.StringifyJSON(p); err == nil { return value } return fmt.Sprintf("%#v", p) @@ -785,7 +785,7 @@ func (q *QueryColumn) UnmarshalJSON(data []byte) error { } *q = QueryColumn(value) - extraProperties, err := core.ExtractExtraProperties(data, *q) + extraProperties, err := internal.ExtractExtraProperties(data, *q) if err != nil { return err } @@ -797,11 +797,11 @@ func (q *QueryColumn) UnmarshalJSON(data []byte) error { func (q *QueryColumn) String() string { if len(q._rawJSON) > 0 { - if value, err := core.StringifyJSON(q._rawJSON); err == nil { + if value, err := internal.StringifyJSON(q._rawJSON); err == nil { return value } } - if value, err := core.StringifyJSON(q); err == nil { + if value, err := internal.StringifyJSON(q); err == nil { return value } return fmt.Sprintf("%#v", q) @@ -857,7 +857,7 @@ func (q *QueryResponse) UnmarshalJSON(data []byte) error { } *q = QueryResponse(value) - extraProperties, err := core.ExtractExtraProperties(data, *q) + extraProperties, err := internal.ExtractExtraProperties(data, *q) if err != nil { return err } @@ -869,11 +869,11 @@ func (q *QueryResponse) UnmarshalJSON(data []byte) error { func (q *QueryResponse) String() string { if len(q._rawJSON) > 0 { - if value, err := core.StringifyJSON(q._rawJSON); err == nil { + if value, err := internal.StringifyJSON(q._rawJSON); err == nil { return value } } - if value, err := core.StringifyJSON(q); err == nil { + if value, err := internal.StringifyJSON(q); err == nil { return value } return fmt.Sprintf("%#v", q) @@ -913,7 +913,7 @@ func (q *QueryResult) UnmarshalJSON(data []byte) error { } *q = QueryResult(value) - extraProperties, err := core.ExtractExtraProperties(data, *q) + extraProperties, err := internal.ExtractExtraProperties(data, *q) if err != nil { return err } @@ -925,11 +925,11 @@ func (q *QueryResult) UnmarshalJSON(data []byte) error { func (q *QueryResult) String() string { if len(q._rawJSON) > 0 { - if value, err := core.StringifyJSON(q._rawJSON); err == nil { + if value, err := internal.StringifyJSON(q._rawJSON); err == nil { return value } } - if value, err := core.StringifyJSON(q); err == nil { + if value, err := internal.StringifyJSON(q); err == nil { return value } return fmt.Sprintf("%#v", q) @@ -993,7 +993,7 @@ func (s *ScoredColumn) UnmarshalJSON(data []byte) error { } *s = ScoredColumn(value) - extraProperties, err := core.ExtractExtraProperties(data, *s) + extraProperties, err := internal.ExtractExtraProperties(data, *s) if err != nil { return err } @@ -1005,11 +1005,11 @@ func (s *ScoredColumn) UnmarshalJSON(data []byte) error { func (s *ScoredColumn) String() string { if len(s._rawJSON) > 0 { - if value, err := core.StringifyJSON(s._rawJSON); err == nil { + if value, err := internal.StringifyJSON(s._rawJSON); err == nil { return value } } - if value, err := core.StringifyJSON(s); err == nil { + if value, err := internal.StringifyJSON(s); err == nil { return value } return fmt.Sprintf("%#v", s) @@ -1032,7 +1032,7 @@ func (u *UpdateResponse) UnmarshalJSON(data []byte) error { } *u = UpdateResponse(value) - extraProperties, err := core.ExtractExtraProperties(data, *u) + extraProperties, err := internal.ExtractExtraProperties(data, *u) if err != nil { return err } @@ -1044,11 +1044,11 @@ func (u *UpdateResponse) UnmarshalJSON(data []byte) error { func (u *UpdateResponse) String() string { if len(u._rawJSON) > 0 { - if value, err := core.StringifyJSON(u._rawJSON); err == nil { + if value, err := internal.StringifyJSON(u._rawJSON); err == nil { return value } } - if value, err := core.StringifyJSON(u); err == nil { + if value, err := internal.StringifyJSON(u); err == nil { return value } return fmt.Sprintf("%#v", u) @@ -1080,7 +1080,7 @@ func (u *UploadResponse) UnmarshalJSON(data []byte) error { } *u = UploadResponse(value) - extraProperties, err := core.ExtractExtraProperties(data, *u) + extraProperties, err := internal.ExtractExtraProperties(data, *u) if err != nil { return err } @@ -1092,11 +1092,11 @@ func (u *UploadResponse) UnmarshalJSON(data []byte) error { func (u *UploadResponse) String() string { if len(u._rawJSON) > 0 { - if value, err := core.StringifyJSON(u._rawJSON); err == nil { + if value, err := internal.StringifyJSON(u._rawJSON); err == nil { return value } } - if value, err := core.StringifyJSON(u); err == nil { + if value, err := internal.StringifyJSON(u); err == nil { return value } return fmt.Sprintf("%#v", u) @@ -1128,7 +1128,7 @@ func (u *Usage) UnmarshalJSON(data []byte) error { } *u = Usage(value) - extraProperties, err := core.ExtractExtraProperties(data, *u) + extraProperties, err := internal.ExtractExtraProperties(data, *u) if err != nil { return err } @@ -1140,11 +1140,11 @@ func (u *Usage) UnmarshalJSON(data []byte) error { func (u *Usage) String() string { if len(u._rawJSON) > 0 { - if value, err := core.StringifyJSON(u._rawJSON); err == nil { + if value, err := internal.StringifyJSON(u._rawJSON); err == nil { return value } } - if value, err := core.StringifyJSON(u); err == nil { + if value, err := internal.StringifyJSON(u); err == nil { return value } return fmt.Sprintf("%#v", u) diff --git a/seed/go-sdk/grpc-proto-exhaustive/dataservice/client.go b/seed/go-sdk/grpc-proto-exhaustive/dataservice/client.go index 5c093d0701b..9bfe63485d0 100644 --- a/seed/go-sdk/grpc-proto-exhaustive/dataservice/client.go +++ b/seed/go-sdk/grpc-proto-exhaustive/dataservice/client.go @@ -6,13 +6,14 @@ import ( context "context" fern "github.com/grpc-proto-exhaustive/fern" core "github.com/grpc-proto-exhaustive/fern/core" + internal "github.com/grpc-proto-exhaustive/fern/internal" option "github.com/grpc-proto-exhaustive/fern/option" http "net/http" ) type Client struct { baseURL string - caller *core.Caller + caller *internal.Caller header http.Header } @@ -20,8 +21,8 @@ func NewClient(opts ...option.RequestOption) *Client { options := core.NewRequestOptions(opts...) return &Client{ baseURL: options.BaseURL, - caller: core.NewCaller( - &core.CallerParams{ + caller: internal.NewCaller( + &internal.CallerParams{ Client: options.HTTPClient, MaxAttempts: options.MaxAttempts, }, @@ -46,13 +47,13 @@ func (c *Client) Upload( } endpointURL := baseURL + "/data" - headers := core.MergeHeaders(c.header.Clone(), options.ToHeader()) + headers := internal.MergeHeaders(c.header.Clone(), options.ToHeader()) headers.Set("Content-Type", "application/json") var response *fern.UploadResponse if err := c.caller.Call( ctx, - &core.CallParams{ + &internal.CallParams{ URL: endpointURL, Method: http.MethodPost, MaxAttempts: options.MaxAttempts, @@ -85,13 +86,13 @@ func (c *Client) Delete( } endpointURL := baseURL + "/data/delete" - headers := core.MergeHeaders(c.header.Clone(), options.ToHeader()) + headers := internal.MergeHeaders(c.header.Clone(), options.ToHeader()) headers.Set("Content-Type", "application/json") var response *fern.DeleteResponse if err := c.caller.Call( ctx, - &core.CallParams{ + &internal.CallParams{ URL: endpointURL, Method: http.MethodPost, MaxAttempts: options.MaxAttempts, @@ -124,13 +125,13 @@ func (c *Client) Describe( } endpointURL := baseURL + "/data/describe" - headers := core.MergeHeaders(c.header.Clone(), options.ToHeader()) + headers := internal.MergeHeaders(c.header.Clone(), options.ToHeader()) headers.Set("Content-Type", "application/json") var response *fern.DescribeResponse if err := c.caller.Call( ctx, - &core.CallParams{ + &internal.CallParams{ URL: endpointURL, Method: http.MethodPost, MaxAttempts: options.MaxAttempts, @@ -163,7 +164,7 @@ func (c *Client) Fetch( } endpointURL := baseURL + "/data/fetch" - queryParams, err := core.QueryValues(request) + queryParams, err := internal.QueryValues(request) if err != nil { return nil, err } @@ -171,12 +172,12 @@ func (c *Client) Fetch( endpointURL += "?" + queryParams.Encode() } - headers := core.MergeHeaders(c.header.Clone(), options.ToHeader()) + headers := internal.MergeHeaders(c.header.Clone(), options.ToHeader()) var response *fern.FetchResponse if err := c.caller.Call( ctx, - &core.CallParams{ + &internal.CallParams{ URL: endpointURL, Method: http.MethodGet, MaxAttempts: options.MaxAttempts, @@ -208,7 +209,7 @@ func (c *Client) List( } endpointURL := baseURL + "/data/list" - queryParams, err := core.QueryValues(request) + queryParams, err := internal.QueryValues(request) if err != nil { return nil, err } @@ -216,12 +217,12 @@ func (c *Client) List( endpointURL += "?" + queryParams.Encode() } - headers := core.MergeHeaders(c.header.Clone(), options.ToHeader()) + headers := internal.MergeHeaders(c.header.Clone(), options.ToHeader()) var response *fern.ListResponse if err := c.caller.Call( ctx, - &core.CallParams{ + &internal.CallParams{ URL: endpointURL, Method: http.MethodGet, MaxAttempts: options.MaxAttempts, @@ -253,13 +254,13 @@ func (c *Client) Query( } endpointURL := baseURL + "/data/query" - headers := core.MergeHeaders(c.header.Clone(), options.ToHeader()) + headers := internal.MergeHeaders(c.header.Clone(), options.ToHeader()) headers.Set("Content-Type", "application/json") var response *fern.QueryResponse if err := c.caller.Call( ctx, - &core.CallParams{ + &internal.CallParams{ URL: endpointURL, Method: http.MethodPost, MaxAttempts: options.MaxAttempts, @@ -292,13 +293,13 @@ func (c *Client) Update( } endpointURL := baseURL + "/data/update" - headers := core.MergeHeaders(c.header.Clone(), options.ToHeader()) + headers := internal.MergeHeaders(c.header.Clone(), options.ToHeader()) headers.Set("Content-Type", "application/json") var response *fern.UpdateResponse if err := c.caller.Call( ctx, - &core.CallParams{ + &internal.CallParams{ URL: endpointURL, Method: http.MethodPost, MaxAttempts: options.MaxAttempts, diff --git a/seed/go-sdk/grpc-proto-exhaustive/core/core.go b/seed/go-sdk/grpc-proto-exhaustive/internal/caller.go similarity index 70% rename from seed/go-sdk/grpc-proto-exhaustive/core/core.go rename to seed/go-sdk/grpc-proto-exhaustive/internal/caller.go index 6b5a8f3c011..8ebdc9cc493 100644 --- a/seed/go-sdk/grpc-proto-exhaustive/core/core.go +++ b/seed/go-sdk/grpc-proto-exhaustive/internal/caller.go @@ -1,4 +1,4 @@ -package core +package internal import ( "bytes" @@ -7,11 +7,12 @@ import ( "errors" "fmt" "io" - "mime/multipart" "net/http" "net/url" "reflect" "strings" + + "github.com/grpc-proto-exhaustive/fern/core" ) const ( @@ -20,105 +21,25 @@ const ( contentTypeHeader = "Content-Type" ) -// HTTPClient is an interface for a subset of the *http.Client. -type HTTPClient interface { - Do(*http.Request) (*http.Response, error) -} - -// EncodeURL encodes the given arguments into the URL, escaping -// values as needed. -func EncodeURL(urlFormat string, args ...interface{}) string { - escapedArgs := make([]interface{}, 0, len(args)) - for _, arg := range args { - escapedArgs = append(escapedArgs, url.PathEscape(fmt.Sprintf("%v", arg))) - } - return fmt.Sprintf(urlFormat, escapedArgs...) -} - -// MergeHeaders merges the given headers together, where the right -// takes precedence over the left. -func MergeHeaders(left, right http.Header) http.Header { - for key, values := range right { - if len(values) > 1 { - left[key] = values - continue - } - if value := right.Get(key); value != "" { - left.Set(key, value) - } - } - return left -} - -// WriteMultipartJSON writes the given value as a JSON part. -// This is used to serialize non-primitive multipart properties -// (i.e. lists, objects, etc). -func WriteMultipartJSON(writer *multipart.Writer, field string, value interface{}) error { - bytes, err := json.Marshal(value) - if err != nil { - return err - } - return writer.WriteField(field, string(bytes)) -} - -// APIError is a lightweight wrapper around the standard error -// interface that preserves the status code from the RPC, if any. -type APIError struct { - err error - - StatusCode int `json:"-"` -} - -// NewAPIError constructs a new API error. -func NewAPIError(statusCode int, err error) *APIError { - return &APIError{ - err: err, - StatusCode: statusCode, - } -} - -// Unwrap returns the underlying error. This also makes the error compatible -// with errors.As and errors.Is. -func (a *APIError) Unwrap() error { - if a == nil { - return nil - } - return a.err -} - -// Error returns the API error's message. -func (a *APIError) Error() string { - if a == nil || (a.err == nil && a.StatusCode == 0) { - return "" - } - if a.err == nil { - return fmt.Sprintf("%d", a.StatusCode) - } - if a.StatusCode == 0 { - return a.err.Error() - } - return fmt.Sprintf("%d: %s", a.StatusCode, a.err.Error()) -} - // ErrorDecoder decodes *http.Response errors and returns a -// typed API error (e.g. *APIError). +// typed API error (e.g. *core.APIError). type ErrorDecoder func(statusCode int, body io.Reader) error // Caller calls APIs and deserializes their response, if any. type Caller struct { - client HTTPClient + client core.HTTPClient retrier *Retrier } // CallerParams represents the parameters used to constrcut a new *Caller. type CallerParams struct { - Client HTTPClient + Client core.HTTPClient MaxAttempts uint } // NewCaller returns a new *Caller backed by the given parameters. func NewCaller(params *CallerParams) *Caller { - var httpClient HTTPClient = http.DefaultClient + var httpClient core.HTTPClient = http.DefaultClient if params.Client != nil { httpClient = params.Client } @@ -140,7 +61,7 @@ type CallParams struct { Headers http.Header BodyProperties map[string]interface{} QueryParameters url.Values - Client HTTPClient + Client core.HTTPClient Request interface{} Response interface{} ResponseIsOptional bool @@ -309,9 +230,9 @@ func decodeError(response *http.Response, errorDecoder ErrorDecoder) error { // The error didn't have a response body, // so all we can do is return an error // with the status code. - return NewAPIError(response.StatusCode, nil) + return core.NewAPIError(response.StatusCode, nil) } - return NewAPIError(response.StatusCode, errors.New(string(bytes))) + return core.NewAPIError(response.StatusCode, errors.New(string(bytes))) } // isNil is used to determine if the request value is equal to nil (i.e. an interface diff --git a/seed/go-sdk/grpc-proto-exhaustive/core/core_test.go b/seed/go-sdk/grpc-proto-exhaustive/internal/caller_test.go similarity index 97% rename from seed/go-sdk/grpc-proto-exhaustive/core/core_test.go rename to seed/go-sdk/grpc-proto-exhaustive/internal/caller_test.go index e6eaef3a86a..dccfa8d7c3a 100644 --- a/seed/go-sdk/grpc-proto-exhaustive/core/core_test.go +++ b/seed/go-sdk/grpc-proto-exhaustive/internal/caller_test.go @@ -1,4 +1,4 @@ -package core +package internal import ( "bytes" @@ -13,6 +13,7 @@ import ( "strconv" "testing" + "github.com/grpc-proto-exhaustive/fern/core" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -50,7 +51,7 @@ type Response struct { // NotFoundError represents a 404. type NotFoundError struct { - *APIError + *core.APIError Message string `json:"message"` } @@ -98,7 +99,7 @@ func TestCall(t *testing.T) { }, giveErrorDecoder: newTestErrorDecoder(t), wantError: &NotFoundError{ - APIError: NewAPIError( + APIError: core.NewAPIError( http.StatusNotFound, errors.New(`{"message":"ID \"404\" not found"}`), ), @@ -111,7 +112,7 @@ func TestCall(t *testing.T) { "X-API-Status": []string{"fail"}, }, giveRequest: nil, - wantError: NewAPIError( + wantError: core.NewAPIError( http.StatusBadRequest, errors.New("invalid request"), ), @@ -136,7 +137,7 @@ func TestCall(t *testing.T) { giveRequest: &Request{ Id: strconv.Itoa(http.StatusInternalServerError), }, - wantError: NewAPIError( + wantError: core.NewAPIError( http.StatusInternalServerError, errors.New("failed to process request"), ), @@ -324,7 +325,7 @@ func newTestServer(t *testing.T, tc *TestCase) *httptest.Server { switch request.Id { case strconv.Itoa(http.StatusNotFound): notFoundError := &NotFoundError{ - APIError: &APIError{ + APIError: &core.APIError{ StatusCode: http.StatusNotFound, }, Message: fmt.Sprintf("ID %q not found", request.Id), @@ -375,7 +376,7 @@ func newTestErrorDecoder(t *testing.T) func(int, io.Reader) error { require.NoError(t, err) var ( - apiError = NewAPIError(statusCode, errors.New(string(raw))) + apiError = core.NewAPIError(statusCode, errors.New(string(raw))) decoder = json.NewDecoder(bytes.NewReader(raw)) ) if statusCode == http.StatusNotFound { diff --git a/seed/go-fiber/grpc-proto/core/extra_properties.go b/seed/go-sdk/grpc-proto-exhaustive/internal/extra_properties.go similarity index 99% rename from seed/go-fiber/grpc-proto/core/extra_properties.go rename to seed/go-sdk/grpc-proto-exhaustive/internal/extra_properties.go index a6af3e12410..540c3fd89ee 100644 --- a/seed/go-fiber/grpc-proto/core/extra_properties.go +++ b/seed/go-sdk/grpc-proto-exhaustive/internal/extra_properties.go @@ -1,4 +1,4 @@ -package core +package internal import ( "bytes" diff --git a/seed/go-sdk/grpc-proto/core/extra_properties_test.go b/seed/go-sdk/grpc-proto-exhaustive/internal/extra_properties_test.go similarity index 99% rename from seed/go-sdk/grpc-proto/core/extra_properties_test.go rename to seed/go-sdk/grpc-proto-exhaustive/internal/extra_properties_test.go index dc66fccd7f1..aa2510ee512 100644 --- a/seed/go-sdk/grpc-proto/core/extra_properties_test.go +++ b/seed/go-sdk/grpc-proto-exhaustive/internal/extra_properties_test.go @@ -1,4 +1,4 @@ -package core +package internal import ( "encoding/json" diff --git a/seed/go-sdk/grpc-proto-exhaustive/internal/http.go b/seed/go-sdk/grpc-proto-exhaustive/internal/http.go new file mode 100644 index 00000000000..2be0805a8be --- /dev/null +++ b/seed/go-sdk/grpc-proto-exhaustive/internal/http.go @@ -0,0 +1,37 @@ +package internal + +import ( + "fmt" + "net/http" + "net/url" +) + +// HTTPClient is an interface for a subset of the *http.Client. +type HTTPClient interface { + Do(*http.Request) (*http.Response, error) +} + +// EncodeURL encodes the given arguments into the URL, escaping +// values as needed. +func EncodeURL(urlFormat string, args ...interface{}) string { + escapedArgs := make([]interface{}, 0, len(args)) + for _, arg := range args { + escapedArgs = append(escapedArgs, url.PathEscape(fmt.Sprintf("%v", arg))) + } + return fmt.Sprintf(urlFormat, escapedArgs...) +} + +// MergeHeaders merges the given headers together, where the right +// takes precedence over the left. +func MergeHeaders(left, right http.Header) http.Header { + for key, values := range right { + if len(values) > 1 { + left[key] = values + continue + } + if value := right.Get(key); value != "" { + left.Set(key, value) + } + } + return left +} diff --git a/seed/go-sdk/grpc-proto/core/query.go b/seed/go-sdk/grpc-proto-exhaustive/internal/query.go similarity index 99% rename from seed/go-sdk/grpc-proto/core/query.go rename to seed/go-sdk/grpc-proto-exhaustive/internal/query.go index 2670ff7feda..6129e71ffe5 100644 --- a/seed/go-sdk/grpc-proto/core/query.go +++ b/seed/go-sdk/grpc-proto-exhaustive/internal/query.go @@ -1,4 +1,4 @@ -package core +package internal import ( "encoding/base64" diff --git a/seed/go-sdk/grpc-proto-exhaustive/core/query_test.go b/seed/go-sdk/grpc-proto-exhaustive/internal/query_test.go similarity index 99% rename from seed/go-sdk/grpc-proto-exhaustive/core/query_test.go rename to seed/go-sdk/grpc-proto-exhaustive/internal/query_test.go index 5498fa92aa5..2e58ccadde1 100644 --- a/seed/go-sdk/grpc-proto-exhaustive/core/query_test.go +++ b/seed/go-sdk/grpc-proto-exhaustive/internal/query_test.go @@ -1,4 +1,4 @@ -package core +package internal import ( "testing" diff --git a/seed/go-sdk/grpc-proto-exhaustive/core/retrier.go b/seed/go-sdk/grpc-proto-exhaustive/internal/retrier.go similarity index 98% rename from seed/go-sdk/grpc-proto-exhaustive/core/retrier.go rename to seed/go-sdk/grpc-proto-exhaustive/internal/retrier.go index ea24916b786..6040147154b 100644 --- a/seed/go-sdk/grpc-proto-exhaustive/core/retrier.go +++ b/seed/go-sdk/grpc-proto-exhaustive/internal/retrier.go @@ -1,4 +1,4 @@ -package core +package internal import ( "crypto/rand" @@ -130,7 +130,6 @@ func (r *Retrier) run( func (r *Retrier) shouldRetry(response *http.Response) bool { return response.StatusCode == http.StatusTooManyRequests || response.StatusCode == http.StatusRequestTimeout || - response.StatusCode == http.StatusConflict || response.StatusCode >= http.StatusInternalServerError } diff --git a/seed/go-sdk/grpc-proto/core/retrier_test.go b/seed/go-sdk/grpc-proto-exhaustive/internal/retrier_test.go similarity index 95% rename from seed/go-sdk/grpc-proto/core/retrier_test.go rename to seed/go-sdk/grpc-proto-exhaustive/internal/retrier_test.go index 7638274d738..1942442ddbf 100644 --- a/seed/go-sdk/grpc-proto/core/retrier_test.go +++ b/seed/go-sdk/grpc-proto-exhaustive/internal/retrier_test.go @@ -1,16 +1,15 @@ -package core +package internal import ( "context" "encoding/json" "io" - "net/http" "net/http/httptest" - "testing" "time" + "github.com/grpc-proto-exhaustive/fern/core" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -23,7 +22,7 @@ type RetryTestCase struct { giveResponse *Response wantResponse *Response - wantError *APIError + wantError *core.APIError } func TestRetrier(t *testing.T) { @@ -52,7 +51,7 @@ func TestRetrier(t *testing.T) { http.StatusRequestTimeout, http.StatusOK, }, - wantError: &APIError{ + wantError: &core.APIError{ StatusCode: http.StatusRequestTimeout, }, }, @@ -70,7 +69,7 @@ func TestRetrier(t *testing.T) { description: "retry does not occur on status code 404", giveAttempts: 2, giveStatusCodes: []int{http.StatusNotFound, http.StatusOK}, - wantError: &APIError{ + wantError: &core.APIError{ StatusCode: http.StatusNotFound, }, }, @@ -121,9 +120,9 @@ func TestRetrier(t *testing.T) { ) if test.wantError != nil { - require.IsType(t, err, &APIError{}) + require.IsType(t, err, &core.APIError{}) expectedErrorCode := test.wantError.StatusCode - actualErrorCode := err.(*APIError).StatusCode + actualErrorCode := err.(*core.APIError).StatusCode assert.Equal(t, expectedErrorCode, actualErrorCode) return } diff --git a/seed/go-fiber/grpc-proto-exhaustive/core/stringer.go b/seed/go-sdk/grpc-proto-exhaustive/internal/stringer.go similarity index 94% rename from seed/go-fiber/grpc-proto-exhaustive/core/stringer.go rename to seed/go-sdk/grpc-proto-exhaustive/internal/stringer.go index 000cf448641..312801851e0 100644 --- a/seed/go-fiber/grpc-proto-exhaustive/core/stringer.go +++ b/seed/go-sdk/grpc-proto-exhaustive/internal/stringer.go @@ -1,4 +1,4 @@ -package core +package internal import "encoding/json" diff --git a/seed/go-fiber/grpc-proto/core/time.go b/seed/go-sdk/grpc-proto-exhaustive/internal/time.go similarity index 99% rename from seed/go-fiber/grpc-proto/core/time.go rename to seed/go-sdk/grpc-proto-exhaustive/internal/time.go index d009ab30c90..ab0e269fade 100644 --- a/seed/go-fiber/grpc-proto/core/time.go +++ b/seed/go-sdk/grpc-proto-exhaustive/internal/time.go @@ -1,4 +1,4 @@ -package core +package internal import ( "encoding/json" diff --git a/seed/go-sdk/grpc-proto/client/client.go b/seed/go-sdk/grpc-proto/client/client.go index b879717f084..efa8f81707c 100644 --- a/seed/go-sdk/grpc-proto/client/client.go +++ b/seed/go-sdk/grpc-proto/client/client.go @@ -4,6 +4,7 @@ package client import ( core "github.com/grpc-proto/fern/core" + internal "github.com/grpc-proto/fern/internal" option "github.com/grpc-proto/fern/option" userservice "github.com/grpc-proto/fern/userservice" http "net/http" @@ -11,7 +12,7 @@ import ( type Client struct { baseURL string - caller *core.Caller + caller *internal.Caller header http.Header Userservice *userservice.Client @@ -21,8 +22,8 @@ func NewClient(opts ...option.RequestOption) *Client { options := core.NewRequestOptions(opts...) return &Client{ baseURL: options.BaseURL, - caller: core.NewCaller( - &core.CallerParams{ + caller: internal.NewCaller( + &internal.CallerParams{ Client: options.HTTPClient, MaxAttempts: options.MaxAttempts, }, diff --git a/seed/go-sdk/grpc-proto/core/api_error.go b/seed/go-sdk/grpc-proto/core/api_error.go new file mode 100644 index 00000000000..dc4190ca1cd --- /dev/null +++ b/seed/go-sdk/grpc-proto/core/api_error.go @@ -0,0 +1,42 @@ +package core + +import "fmt" + +// APIError is a lightweight wrapper around the standard error +// interface that preserves the status code from the RPC, if any. +type APIError struct { + err error + + StatusCode int `json:"-"` +} + +// NewAPIError constructs a new API error. +func NewAPIError(statusCode int, err error) *APIError { + return &APIError{ + err: err, + StatusCode: statusCode, + } +} + +// Unwrap returns the underlying error. This also makes the error compatible +// with errors.As and errors.Is. +func (a *APIError) Unwrap() error { + if a == nil { + return nil + } + return a.err +} + +// Error returns the API error's message. +func (a *APIError) Error() string { + if a == nil || (a.err == nil && a.StatusCode == 0) { + return "" + } + if a.err == nil { + return fmt.Sprintf("%d", a.StatusCode) + } + if a.StatusCode == 0 { + return a.err.Error() + } + return fmt.Sprintf("%d: %s", a.StatusCode, a.err.Error()) +} diff --git a/seed/go-sdk/grpc-proto/core/http.go b/seed/go-sdk/grpc-proto/core/http.go new file mode 100644 index 00000000000..b553350b84e --- /dev/null +++ b/seed/go-sdk/grpc-proto/core/http.go @@ -0,0 +1,8 @@ +package core + +import "net/http" + +// HTTPClient is an interface for a subset of the *http.Client. +type HTTPClient interface { + Do(*http.Request) (*http.Response, error) +} diff --git a/seed/go-sdk/grpc-proto/core/multipart.go b/seed/go-sdk/grpc-proto/core/multipart.go deleted file mode 100644 index 40f84bcec76..00000000000 --- a/seed/go-sdk/grpc-proto/core/multipart.go +++ /dev/null @@ -1,195 +0,0 @@ -package core - -import ( - "bytes" - "encoding/json" - "fmt" - "io" - "mime/multipart" - "net/textproto" - "strings" -) - -// Named is implemented by types that define a name. -type Named interface { - Name() string -} - -// ContentTyped is implemented by types that define a Content-Type. -type ContentTyped interface { - ContentType() string -} - -// WriteMultipartOption adapts the behavior of the multipart writer. -type WriteMultipartOption func(*writeMultipartOptions) - -// WithMultipartContentType sets the Content-Type for the multipart writer. -func WithMultipartContentType(contentType string) WriteMultipartOption { - return func(options *writeMultipartOptions) { - options.contentType = contentType - } -} - -// MultipartWriter writes multipart/form-data requests. -type MultipartWriter struct { - buffer *bytes.Buffer - writer *multipart.Writer -} - -// NewMultipartWriter creates a new multipart writer. -func NewMultipartWriter() *MultipartWriter { - buffer := bytes.NewBuffer(nil) - return &MultipartWriter{ - buffer: buffer, - writer: multipart.NewWriter(buffer), - } -} - -// Buffer returns the underlying buffer. -func (w *MultipartWriter) Buffer() *bytes.Buffer { - return w.buffer -} - -// ContentType returns the Content-Type for an HTTP multipart/form-data. -func (w *MultipartWriter) ContentType() string { - return w.writer.FormDataContentType() -} - -// WriteFile writes the given file part. -func (w *MultipartWriter) WriteFile( - field string, - file io.Reader, - opts ...WriteMultipartOption, -) error { - options := newWriteMultipartOptions(opts...) - return w.writeFile(field, file, options.contentType) -} - -// WriteField writes the given value as a form field. -func (w *MultipartWriter) WriteField( - field string, - value string, - opts ...WriteMultipartOption, -) error { - options := newWriteMultipartOptions(opts...) - return w.writeField(field, value, options.contentType) -} - -// WriteJSON writes the given value as a JSON form field. -func (w *MultipartWriter) WriteJSON( - field string, - value interface{}, - opts ...WriteMultipartOption, -) error { - bytes, err := json.Marshal(value) - if err != nil { - return err - } - return w.WriteField(field, string(bytes), opts...) -} - -// Close closes the writer. -func (w *MultipartWriter) Close() error { - return w.writer.Close() -} - -func (w *MultipartWriter) writeField( - field string, - value string, - contentType string, -) error { - part, err := w.newFormField(field, contentType) - if err != nil { - return err - } - _, err = part.Write([]byte(value)) - return err -} - -func (w *MultipartWriter) writeFile( - field string, - file io.Reader, - contentType string, -) error { - filename := getFilename(file) - if contentType == "" { - contentType = getContentType(file) - } - part, err := w.newFormPart(field, filename, contentType) - if err != nil { - return err - } - _, err = io.Copy(part, file) - return err -} - -// newFormField creates a new form field. -func (w *MultipartWriter) newFormField( - field string, - contentType string, -) (io.Writer, error) { - return w.newFormPart(field, "" /* filename */, contentType) -} - -// newFormPart creates a new form data part. -func (w *MultipartWriter) newFormPart( - field string, - filename string, - contentType string, -) (io.Writer, error) { - h := make(textproto.MIMEHeader) - h.Set("Content-Disposition", getContentDispositionHeaderValue(field, filename)) - if contentType != "" { - h.Set("Content-Type", contentType) - } - return w.writer.CreatePart(h) -} - -// writeMultipartOptions are options used to adapt the behavior of the multipart writer. -type writeMultipartOptions struct { - contentType string -} - -// newWriteMultipartOptions returns a new write multipart options. -func newWriteMultipartOptions(opts ...WriteMultipartOption) *writeMultipartOptions { - options := new(writeMultipartOptions) - for _, opt := range opts { - opt(options) - } - return options -} - -// getContentType returns the Content-Type for the given file, if any. -func getContentType(file io.Reader) string { - if v, ok := file.(ContentTyped); ok { - return v.ContentType() - } - return "" -} - -// getFilename returns the name for the given file, if any. -func getFilename(file io.Reader) string { - if v, ok := file.(Named); ok { - return v.Name() - } - return "" -} - -// getContentDispositionHeaderValue returns the value for the Content-Disposition header. -func getContentDispositionHeaderValue(field string, filename string) string { - contentDisposition := fmt.Sprintf("form-data; name=%q", field) - if filename != "" { - contentDisposition += fmt.Sprintf(`; filename=%q`, escapeQuotes(filename)) - } - return contentDisposition -} - -// https://cs.opensource.google/go/go/+/refs/tags/go1.23.2:src/mime/multipart/writer.go;l=132 -var quoteEscaper = strings.NewReplacer("\\", "\\\\", `"`, "\\\"") - -// escapeQuotes is directly referenced from the standard library. -// -// https://cs.opensource.google/go/go/+/refs/tags/go1.23.2:src/mime/multipart/writer.go;l=134 -func escapeQuotes(s string) string { - return quoteEscaper.Replace(s) -} diff --git a/seed/go-sdk/grpc-proto/core/multipart_test.go b/seed/go-sdk/grpc-proto/core/multipart_test.go deleted file mode 100644 index ba73d413f0c..00000000000 --- a/seed/go-sdk/grpc-proto/core/multipart_test.go +++ /dev/null @@ -1,251 +0,0 @@ -package core - -import ( - "encoding/json" - "io" - "mime/multipart" - "strings" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -const maxFormMemory = 32 << 20 // 32MB - -type mockFile struct { - name string - content string - contentType string - - reader io.Reader -} - -func (f *mockFile) Read(p []byte) (n int, err error) { - if f.reader == nil { - f.reader = strings.NewReader(f.content) - } - return f.reader.Read(p) -} - -func (f *mockFile) Name() string { - return f.name -} - -func (f *mockFile) ContentType() string { - return f.contentType -} - -func TestMultipartWriter(t *testing.T) { - t.Run("empty", func(t *testing.T) { - w := NewMultipartWriter() - assert.NotNil(t, w.Buffer()) - assert.Contains(t, w.ContentType(), "multipart/form-data; boundary=") - require.NoError(t, w.Close()) - }) - - t.Run("write field", func(t *testing.T) { - tests := []struct { - desc string - giveField string - giveValue string - giveContentType string - }{ - { - desc: "empty field", - giveField: "empty", - giveValue: "", - }, - { - desc: "simple field", - giveField: "greeting", - giveValue: "hello world", - }, - { - desc: "field with content type", - giveField: "message", - giveValue: "hello", - giveContentType: "text/plain", - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - var opts []WriteMultipartOption - if tt.giveContentType != "" { - opts = append(opts, WithMultipartContentType(tt.giveContentType)) - } - - require.NoError(t, w.WriteField(tt.giveField, tt.giveValue, opts...)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - - assert.Equal(t, []string{tt.giveValue}, form.Value[tt.giveField]) - require.NoError(t, form.RemoveAll()) - }) - } - }) - - t.Run("write file", func(t *testing.T) { - tests := []struct { - desc string - giveField string - giveFile *mockFile - giveContentType string - }{ - { - desc: "simple file", - giveField: "file", - giveFile: &mockFile{ - name: "test.txt", - content: "hello world", - contentType: "text/plain", - }, - }, - { - desc: "override content type", - giveField: "file", - giveFile: &mockFile{ - name: "test.txt", - content: "hello world", - contentType: "text/plain", - }, - giveContentType: "application/octet-stream", - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - var opts []WriteMultipartOption - if tt.giveContentType != "" { - opts = append(opts, WithMultipartContentType(tt.giveContentType)) - } - - require.NoError(t, w.WriteFile(tt.giveField, tt.giveFile, opts...)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - files := form.File[tt.giveField] - require.Len(t, files, 1) - - file := files[0] - assert.Equal(t, tt.giveFile.name, file.Filename) - - f, err := file.Open() - require.NoError(t, err) - defer func() { - require.NoError(t, f.Close()) - }() - - content, err := io.ReadAll(f) - require.NoError(t, err) - assert.Equal(t, tt.giveFile.content, string(content)) - - expectedContentType := tt.giveContentType - if expectedContentType == "" { - expectedContentType = tt.giveFile.contentType - } - if expectedContentType != "" { - assert.Equal(t, expectedContentType, file.Header.Get("Content-Type")) - } - }) - } - }) - - t.Run("write JSON", func(t *testing.T) { - type testStruct struct { - Name string `json:"name"` - Value int `json:"value"` - } - - tests := []struct { - desc string - giveField string - giveValue interface{} - }{ - { - desc: "struct", - giveField: "data", - giveValue: testStruct{Name: "test", Value: 123}, - }, - { - desc: "map", - giveField: "data", - giveValue: map[string]string{"key": "value"}, - }, - } - - for _, tt := range tests { - t.Run(tt.desc, func(t *testing.T) { - w := NewMultipartWriter() - - require.NoError(t, w.WriteJSON(tt.giveField, tt.giveValue)) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - expected, err := json.Marshal(tt.giveValue) - require.NoError(t, err) - assert.Equal(t, []string{string(expected)}, form.Value[tt.giveField]) - }) - } - }) - - t.Run("complex", func(t *testing.T) { - w := NewMultipartWriter() - - // Add multiple fields and files - require.NoError(t, w.WriteField("foo", "bar")) - require.NoError(t, w.WriteField("baz", "qux")) - - hello := mockFile{name: "file.txt", content: "Hello, world!", contentType: "text/plain"} - require.NoError(t, w.WriteFile("file", &hello)) - require.NoError(t, w.WriteJSON("data", map[string]string{"key": "value"})) - require.NoError(t, w.Close()) - - reader := multipart.NewReader(w.Buffer(), w.writer.Boundary()) - form, err := reader.ReadForm(maxFormMemory) - require.NoError(t, err) - defer func() { - require.NoError(t, form.RemoveAll()) - }() - - assert.Equal(t, []string{"bar"}, form.Value["foo"]) - assert.Equal(t, []string{"qux"}, form.Value["baz"]) - assert.Equal(t, []string{`{"key":"value"}`}, form.Value["data"]) - - files := form.File["file"] - require.Len(t, files, 1) - - file := files[0] - assert.Equal(t, "file.txt", file.Filename) - - f, err := file.Open() - require.NoError(t, err) - defer func() { - require.NoError(t, f.Close()) - }() - - content, err := io.ReadAll(f) - require.NoError(t, err) - assert.Equal(t, "Hello, world!", string(content)) - }) -} diff --git a/seed/go-sdk/grpc-proto/core/core.go b/seed/go-sdk/grpc-proto/internal/caller.go similarity index 70% rename from seed/go-sdk/grpc-proto/core/core.go rename to seed/go-sdk/grpc-proto/internal/caller.go index 6b5a8f3c011..cfeebc21ef3 100644 --- a/seed/go-sdk/grpc-proto/core/core.go +++ b/seed/go-sdk/grpc-proto/internal/caller.go @@ -1,4 +1,4 @@ -package core +package internal import ( "bytes" @@ -7,11 +7,12 @@ import ( "errors" "fmt" "io" - "mime/multipart" "net/http" "net/url" "reflect" "strings" + + "github.com/grpc-proto/fern/core" ) const ( @@ -20,105 +21,25 @@ const ( contentTypeHeader = "Content-Type" ) -// HTTPClient is an interface for a subset of the *http.Client. -type HTTPClient interface { - Do(*http.Request) (*http.Response, error) -} - -// EncodeURL encodes the given arguments into the URL, escaping -// values as needed. -func EncodeURL(urlFormat string, args ...interface{}) string { - escapedArgs := make([]interface{}, 0, len(args)) - for _, arg := range args { - escapedArgs = append(escapedArgs, url.PathEscape(fmt.Sprintf("%v", arg))) - } - return fmt.Sprintf(urlFormat, escapedArgs...) -} - -// MergeHeaders merges the given headers together, where the right -// takes precedence over the left. -func MergeHeaders(left, right http.Header) http.Header { - for key, values := range right { - if len(values) > 1 { - left[key] = values - continue - } - if value := right.Get(key); value != "" { - left.Set(key, value) - } - } - return left -} - -// WriteMultipartJSON writes the given value as a JSON part. -// This is used to serialize non-primitive multipart properties -// (i.e. lists, objects, etc). -func WriteMultipartJSON(writer *multipart.Writer, field string, value interface{}) error { - bytes, err := json.Marshal(value) - if err != nil { - return err - } - return writer.WriteField(field, string(bytes)) -} - -// APIError is a lightweight wrapper around the standard error -// interface that preserves the status code from the RPC, if any. -type APIError struct { - err error - - StatusCode int `json:"-"` -} - -// NewAPIError constructs a new API error. -func NewAPIError(statusCode int, err error) *APIError { - return &APIError{ - err: err, - StatusCode: statusCode, - } -} - -// Unwrap returns the underlying error. This also makes the error compatible -// with errors.As and errors.Is. -func (a *APIError) Unwrap() error { - if a == nil { - return nil - } - return a.err -} - -// Error returns the API error's message. -func (a *APIError) Error() string { - if a == nil || (a.err == nil && a.StatusCode == 0) { - return "" - } - if a.err == nil { - return fmt.Sprintf("%d", a.StatusCode) - } - if a.StatusCode == 0 { - return a.err.Error() - } - return fmt.Sprintf("%d: %s", a.StatusCode, a.err.Error()) -} - // ErrorDecoder decodes *http.Response errors and returns a -// typed API error (e.g. *APIError). +// typed API error (e.g. *core.APIError). type ErrorDecoder func(statusCode int, body io.Reader) error // Caller calls APIs and deserializes their response, if any. type Caller struct { - client HTTPClient + client core.HTTPClient retrier *Retrier } // CallerParams represents the parameters used to constrcut a new *Caller. type CallerParams struct { - Client HTTPClient + Client core.HTTPClient MaxAttempts uint } // NewCaller returns a new *Caller backed by the given parameters. func NewCaller(params *CallerParams) *Caller { - var httpClient HTTPClient = http.DefaultClient + var httpClient core.HTTPClient = http.DefaultClient if params.Client != nil { httpClient = params.Client } @@ -140,7 +61,7 @@ type CallParams struct { Headers http.Header BodyProperties map[string]interface{} QueryParameters url.Values - Client HTTPClient + Client core.HTTPClient Request interface{} Response interface{} ResponseIsOptional bool @@ -309,9 +230,9 @@ func decodeError(response *http.Response, errorDecoder ErrorDecoder) error { // The error didn't have a response body, // so all we can do is return an error // with the status code. - return NewAPIError(response.StatusCode, nil) + return core.NewAPIError(response.StatusCode, nil) } - return NewAPIError(response.StatusCode, errors.New(string(bytes))) + return core.NewAPIError(response.StatusCode, errors.New(string(bytes))) } // isNil is used to determine if the request value is equal to nil (i.e. an interface diff --git a/seed/go-sdk/grpc-proto/core/core_test.go b/seed/go-sdk/grpc-proto/internal/caller_test.go similarity index 97% rename from seed/go-sdk/grpc-proto/core/core_test.go rename to seed/go-sdk/grpc-proto/internal/caller_test.go index e6eaef3a86a..93b682f32b0 100644 --- a/seed/go-sdk/grpc-proto/core/core_test.go +++ b/seed/go-sdk/grpc-proto/internal/caller_test.go @@ -1,4 +1,4 @@ -package core +package internal import ( "bytes" @@ -13,6 +13,7 @@ import ( "strconv" "testing" + "github.com/grpc-proto/fern/core" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -50,7 +51,7 @@ type Response struct { // NotFoundError represents a 404. type NotFoundError struct { - *APIError + *core.APIError Message string `json:"message"` } @@ -98,7 +99,7 @@ func TestCall(t *testing.T) { }, giveErrorDecoder: newTestErrorDecoder(t), wantError: &NotFoundError{ - APIError: NewAPIError( + APIError: core.NewAPIError( http.StatusNotFound, errors.New(`{"message":"ID \"404\" not found"}`), ), @@ -111,7 +112,7 @@ func TestCall(t *testing.T) { "X-API-Status": []string{"fail"}, }, giveRequest: nil, - wantError: NewAPIError( + wantError: core.NewAPIError( http.StatusBadRequest, errors.New("invalid request"), ), @@ -136,7 +137,7 @@ func TestCall(t *testing.T) { giveRequest: &Request{ Id: strconv.Itoa(http.StatusInternalServerError), }, - wantError: NewAPIError( + wantError: core.NewAPIError( http.StatusInternalServerError, errors.New("failed to process request"), ), @@ -324,7 +325,7 @@ func newTestServer(t *testing.T, tc *TestCase) *httptest.Server { switch request.Id { case strconv.Itoa(http.StatusNotFound): notFoundError := &NotFoundError{ - APIError: &APIError{ + APIError: &core.APIError{ StatusCode: http.StatusNotFound, }, Message: fmt.Sprintf("ID %q not found", request.Id), @@ -375,7 +376,7 @@ func newTestErrorDecoder(t *testing.T) func(int, io.Reader) error { require.NoError(t, err) var ( - apiError = NewAPIError(statusCode, errors.New(string(raw))) + apiError = core.NewAPIError(statusCode, errors.New(string(raw))) decoder = json.NewDecoder(bytes.NewReader(raw)) ) if statusCode == http.StatusNotFound { diff --git a/seed/go-sdk/grpc-proto/core/extra_properties.go b/seed/go-sdk/grpc-proto/internal/extra_properties.go similarity index 99% rename from seed/go-sdk/grpc-proto/core/extra_properties.go rename to seed/go-sdk/grpc-proto/internal/extra_properties.go index a6af3e12410..540c3fd89ee 100644 --- a/seed/go-sdk/grpc-proto/core/extra_properties.go +++ b/seed/go-sdk/grpc-proto/internal/extra_properties.go @@ -1,4 +1,4 @@ -package core +package internal import ( "bytes" diff --git a/seed/go-fiber/grpc-proto/core/extra_properties_test.go b/seed/go-sdk/grpc-proto/internal/extra_properties_test.go similarity index 99% rename from seed/go-fiber/grpc-proto/core/extra_properties_test.go rename to seed/go-sdk/grpc-proto/internal/extra_properties_test.go index dc66fccd7f1..aa2510ee512 100644 --- a/seed/go-fiber/grpc-proto/core/extra_properties_test.go +++ b/seed/go-sdk/grpc-proto/internal/extra_properties_test.go @@ -1,4 +1,4 @@ -package core +package internal import ( "encoding/json" diff --git a/seed/go-sdk/grpc-proto/internal/http.go b/seed/go-sdk/grpc-proto/internal/http.go new file mode 100644 index 00000000000..2be0805a8be --- /dev/null +++ b/seed/go-sdk/grpc-proto/internal/http.go @@ -0,0 +1,37 @@ +package internal + +import ( + "fmt" + "net/http" + "net/url" +) + +// HTTPClient is an interface for a subset of the *http.Client. +type HTTPClient interface { + Do(*http.Request) (*http.Response, error) +} + +// EncodeURL encodes the given arguments into the URL, escaping +// values as needed. +func EncodeURL(urlFormat string, args ...interface{}) string { + escapedArgs := make([]interface{}, 0, len(args)) + for _, arg := range args { + escapedArgs = append(escapedArgs, url.PathEscape(fmt.Sprintf("%v", arg))) + } + return fmt.Sprintf(urlFormat, escapedArgs...) +} + +// MergeHeaders merges the given headers together, where the right +// takes precedence over the left. +func MergeHeaders(left, right http.Header) http.Header { + for key, values := range right { + if len(values) > 1 { + left[key] = values + continue + } + if value := right.Get(key); value != "" { + left.Set(key, value) + } + } + return left +} diff --git a/seed/go-sdk/grpc-proto-exhaustive/core/query.go b/seed/go-sdk/grpc-proto/internal/query.go similarity index 99% rename from seed/go-sdk/grpc-proto-exhaustive/core/query.go rename to seed/go-sdk/grpc-proto/internal/query.go index 2670ff7feda..6129e71ffe5 100644 --- a/seed/go-sdk/grpc-proto-exhaustive/core/query.go +++ b/seed/go-sdk/grpc-proto/internal/query.go @@ -1,4 +1,4 @@ -package core +package internal import ( "encoding/base64" diff --git a/seed/go-sdk/grpc-proto/core/query_test.go b/seed/go-sdk/grpc-proto/internal/query_test.go similarity index 99% rename from seed/go-sdk/grpc-proto/core/query_test.go rename to seed/go-sdk/grpc-proto/internal/query_test.go index 5498fa92aa5..2e58ccadde1 100644 --- a/seed/go-sdk/grpc-proto/core/query_test.go +++ b/seed/go-sdk/grpc-proto/internal/query_test.go @@ -1,4 +1,4 @@ -package core +package internal import ( "testing" diff --git a/seed/go-sdk/grpc-proto/core/retrier.go b/seed/go-sdk/grpc-proto/internal/retrier.go similarity index 98% rename from seed/go-sdk/grpc-proto/core/retrier.go rename to seed/go-sdk/grpc-proto/internal/retrier.go index ea24916b786..6040147154b 100644 --- a/seed/go-sdk/grpc-proto/core/retrier.go +++ b/seed/go-sdk/grpc-proto/internal/retrier.go @@ -1,4 +1,4 @@ -package core +package internal import ( "crypto/rand" @@ -130,7 +130,6 @@ func (r *Retrier) run( func (r *Retrier) shouldRetry(response *http.Response) bool { return response.StatusCode == http.StatusTooManyRequests || response.StatusCode == http.StatusRequestTimeout || - response.StatusCode == http.StatusConflict || response.StatusCode >= http.StatusInternalServerError } diff --git a/seed/go-sdk/grpc-proto-exhaustive/core/retrier_test.go b/seed/go-sdk/grpc-proto/internal/retrier_test.go similarity index 95% rename from seed/go-sdk/grpc-proto-exhaustive/core/retrier_test.go rename to seed/go-sdk/grpc-proto/internal/retrier_test.go index 7638274d738..35a67096caa 100644 --- a/seed/go-sdk/grpc-proto-exhaustive/core/retrier_test.go +++ b/seed/go-sdk/grpc-proto/internal/retrier_test.go @@ -1,16 +1,15 @@ -package core +package internal import ( "context" "encoding/json" "io" - "net/http" "net/http/httptest" - "testing" "time" + "github.com/grpc-proto/fern/core" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -23,7 +22,7 @@ type RetryTestCase struct { giveResponse *Response wantResponse *Response - wantError *APIError + wantError *core.APIError } func TestRetrier(t *testing.T) { @@ -52,7 +51,7 @@ func TestRetrier(t *testing.T) { http.StatusRequestTimeout, http.StatusOK, }, - wantError: &APIError{ + wantError: &core.APIError{ StatusCode: http.StatusRequestTimeout, }, }, @@ -70,7 +69,7 @@ func TestRetrier(t *testing.T) { description: "retry does not occur on status code 404", giveAttempts: 2, giveStatusCodes: []int{http.StatusNotFound, http.StatusOK}, - wantError: &APIError{ + wantError: &core.APIError{ StatusCode: http.StatusNotFound, }, }, @@ -121,9 +120,9 @@ func TestRetrier(t *testing.T) { ) if test.wantError != nil { - require.IsType(t, err, &APIError{}) + require.IsType(t, err, &core.APIError{}) expectedErrorCode := test.wantError.StatusCode - actualErrorCode := err.(*APIError).StatusCode + actualErrorCode := err.(*core.APIError).StatusCode assert.Equal(t, expectedErrorCode, actualErrorCode) return } diff --git a/seed/go-sdk/grpc-proto/core/stringer.go b/seed/go-sdk/grpc-proto/internal/stringer.go similarity index 94% rename from seed/go-sdk/grpc-proto/core/stringer.go rename to seed/go-sdk/grpc-proto/internal/stringer.go index 000cf448641..312801851e0 100644 --- a/seed/go-sdk/grpc-proto/core/stringer.go +++ b/seed/go-sdk/grpc-proto/internal/stringer.go @@ -1,4 +1,4 @@ -package core +package internal import "encoding/json" diff --git a/seed/go-fiber/grpc-proto-exhaustive/core/time.go b/seed/go-sdk/grpc-proto/internal/time.go similarity index 99% rename from seed/go-fiber/grpc-proto-exhaustive/core/time.go rename to seed/go-sdk/grpc-proto/internal/time.go index d009ab30c90..ab0e269fade 100644 --- a/seed/go-fiber/grpc-proto-exhaustive/core/time.go +++ b/seed/go-sdk/grpc-proto/internal/time.go @@ -1,4 +1,4 @@ -package core +package internal import ( "encoding/json" diff --git a/seed/go-sdk/grpc-proto/userservice.go b/seed/go-sdk/grpc-proto/userservice.go index cbadca02982..fb39ffb45e7 100644 --- a/seed/go-sdk/grpc-proto/userservice.go +++ b/seed/go-sdk/grpc-proto/userservice.go @@ -5,7 +5,7 @@ package api import ( json "encoding/json" fmt "fmt" - core "github.com/grpc-proto/fern/core" + internal "github.com/grpc-proto/fern/internal" ) type CreateRequest struct { @@ -42,7 +42,7 @@ func (c *CreateResponse) UnmarshalJSON(data []byte) error { } *c = CreateResponse(value) - extraProperties, err := core.ExtractExtraProperties(data, *c) + extraProperties, err := internal.ExtractExtraProperties(data, *c) if err != nil { return err } @@ -54,11 +54,11 @@ func (c *CreateResponse) UnmarshalJSON(data []byte) error { func (c *CreateResponse) String() string { if len(c._rawJSON) > 0 { - if value, err := core.StringifyJSON(c._rawJSON); err == nil { + if value, err := internal.StringifyJSON(c._rawJSON); err == nil { return value } } - if value, err := core.StringifyJSON(c); err == nil { + if value, err := internal.StringifyJSON(c); err == nil { return value } return fmt.Sprintf("%#v", c) @@ -287,7 +287,7 @@ func (u *UserModel) UnmarshalJSON(data []byte) error { } *u = UserModel(value) - extraProperties, err := core.ExtractExtraProperties(data, *u) + extraProperties, err := internal.ExtractExtraProperties(data, *u) if err != nil { return err } @@ -299,11 +299,11 @@ func (u *UserModel) UnmarshalJSON(data []byte) error { func (u *UserModel) String() string { if len(u._rawJSON) > 0 { - if value, err := core.StringifyJSON(u._rawJSON); err == nil { + if value, err := internal.StringifyJSON(u._rawJSON); err == nil { return value } } - if value, err := core.StringifyJSON(u); err == nil { + if value, err := internal.StringifyJSON(u); err == nil { return value } return fmt.Sprintf("%#v", u) diff --git a/seed/go-sdk/grpc-proto/userservice/client.go b/seed/go-sdk/grpc-proto/userservice/client.go index 9805230e831..634cae165e2 100644 --- a/seed/go-sdk/grpc-proto/userservice/client.go +++ b/seed/go-sdk/grpc-proto/userservice/client.go @@ -6,13 +6,14 @@ import ( context "context" fern "github.com/grpc-proto/fern" core "github.com/grpc-proto/fern/core" + internal "github.com/grpc-proto/fern/internal" option "github.com/grpc-proto/fern/option" http "net/http" ) type Client struct { baseURL string - caller *core.Caller + caller *internal.Caller header http.Header } @@ -20,8 +21,8 @@ func NewClient(opts ...option.RequestOption) *Client { options := core.NewRequestOptions(opts...) return &Client{ baseURL: options.BaseURL, - caller: core.NewCaller( - &core.CallerParams{ + caller: internal.NewCaller( + &internal.CallerParams{ Client: options.HTTPClient, MaxAttempts: options.MaxAttempts, }, @@ -46,13 +47,13 @@ func (c *Client) Create( } endpointURL := baseURL + "/users" - headers := core.MergeHeaders(c.header.Clone(), options.ToHeader()) + headers := internal.MergeHeaders(c.header.Clone(), options.ToHeader()) headers.Set("Content-Type", "application/json") var response *fern.CreateResponse if err := c.caller.Call( ctx, - &core.CallParams{ + &internal.CallParams{ URL: endpointURL, Method: http.MethodPost, MaxAttempts: options.MaxAttempts, diff --git a/seed/php-model/exhaustive/.mock/definition/endpoints/content-type.yml b/seed/php-model/exhaustive/.mock/definition/endpoints/content-type.yml new file mode 100644 index 00000000000..7c54e39fa5a --- /dev/null +++ b/seed/php-model/exhaustive/.mock/definition/endpoints/content-type.yml @@ -0,0 +1,19 @@ +imports: + objects: ../types/object.yml + +service: + auth: true + base-path: /foo + endpoints: + postJsonPatchContentType: + path: /bar + method: POST + request: + body: objects.ObjectWithOptionalField + content-type: application/json-patch+json + postJsonPatchContentWithCharsetType: + path: /baz + method: POST + request: + body: objects.ObjectWithOptionalField + content-type: application/json-patch+json; charset=utf-8 diff --git a/seed/php-model/file-upload/.mock/definition/service.yml b/seed/php-model/file-upload/.mock/definition/service.yml index 5ae95021c2d..956e6ba73ba 100644 --- a/seed/php-model/file-upload/.mock/definition/service.yml +++ b/seed/php-model/file-upload/.mock/definition/service.yml @@ -64,6 +64,9 @@ service: bar: type: MyObject content-type: application/json + foobar: + type: optional + content-type: application/json types: Id: string diff --git a/seed/php-model/grpc-proto-exhaustive/.mock/generators.yml b/seed/php-model/grpc-proto-exhaustive/.mock/generators.yml index c23323621f2..972ed6d7b73 100644 --- a/seed/php-model/grpc-proto-exhaustive/.mock/generators.yml +++ b/seed/php-model/grpc-proto-exhaustive/.mock/generators.yml @@ -1,7 +1,6 @@ api: - - path: openapi/openapi.yml - - proto: - root: proto - target: proto/data/v1/data.proto - overrides: overrides.yml - local-generation: true + - proto: + root: proto + target: proto/data/v1/data.proto + overrides: overrides.yml + local-generation: true \ No newline at end of file diff --git a/seed/php-model/grpc-proto-exhaustive/.mock/openapi/openapi.yml b/seed/php-model/grpc-proto-exhaustive/.mock/openapi/openapi.yml deleted file mode 100644 index ebc23143df3..00000000000 --- a/seed/php-model/grpc-proto-exhaustive/.mock/openapi/openapi.yml +++ /dev/null @@ -1,33 +0,0 @@ -openapi: 3.0.3 -info: - title: Test API - version: 1.0.0 -servers: - - url: https://localhost -tags: - - name: dataservice -paths: - /foo: - post: - tag: dataservice - x-fern-sdk-group-name: - - dataservice - x-fern-sdk-method-name: foo - security: - - ApiKeyAuth: [] - operationId: foo - responses: - "200": - content: - application/json: - schema: - type: object - -security: - - ApiKeyAuth: [] -components: - securitySchemes: - ApiKeyAuth: - type: apiKey - in: header - name: X-API-Key diff --git a/seed/php-model/license/.github/workflows/ci.yml b/seed/php-model/license/.github/workflows/ci.yml new file mode 100644 index 00000000000..258bf33a19f --- /dev/null +++ b/seed/php-model/license/.github/workflows/ci.yml @@ -0,0 +1,48 @@ +name: ci + +on: [push] + +jobs: + compile: + runs-on: ubuntu-latest + + steps: + - name: Checkout repo + uses: actions/checkout@v4 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: "8.1" + + - name: Install tools + run: | + composer install + + - name: Build + run: | + composer build + + - name: Analyze + run: | + composer analyze + + unit-tests: + runs-on: ubuntu-latest + + steps: + - name: Checkout repo + uses: actions/checkout@v4 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: "8.1" + + - name: Install tools + run: | + composer install + + - name: Run Tests + run: | + composer test \ No newline at end of file diff --git a/seed/php-model/license/.gitignore b/seed/php-model/license/.gitignore new file mode 100644 index 00000000000..f38efc46ade --- /dev/null +++ b/seed/php-model/license/.gitignore @@ -0,0 +1,4 @@ +.php-cs-fixer.cache +.phpunit.result.cache +composer.lock +vendor/ \ No newline at end of file diff --git a/seed/php-model/license/.mock/definition/__package__.yml b/seed/php-model/license/.mock/definition/__package__.yml new file mode 100644 index 00000000000..b1e4d4a878f --- /dev/null +++ b/seed/php-model/license/.mock/definition/__package__.yml @@ -0,0 +1,13 @@ +types: + Type: + docs: A simple type with just a name. + properties: + name: string + +service: + auth: false + base-path: / + endpoints: + get: + path: "/" + method: GET diff --git a/seed/php-model/license/.mock/definition/api.yml b/seed/php-model/license/.mock/definition/api.yml new file mode 100644 index 00000000000..5523ff1f181 --- /dev/null +++ b/seed/php-model/license/.mock/definition/api.yml @@ -0,0 +1 @@ +name: license diff --git a/seed/php-model/license/.mock/fern.config.json b/seed/php-model/license/.mock/fern.config.json new file mode 100644 index 00000000000..4c8e54ac313 --- /dev/null +++ b/seed/php-model/license/.mock/fern.config.json @@ -0,0 +1 @@ +{"organization": "fern-test", "version": "*"} \ No newline at end of file diff --git a/seed/php-model/license/.mock/generators.yml b/seed/php-model/license/.mock/generators.yml new file mode 100644 index 00000000000..0967ef424bc --- /dev/null +++ b/seed/php-model/license/.mock/generators.yml @@ -0,0 +1 @@ +{} diff --git a/seed/php-model/license/composer.json b/seed/php-model/license/composer.json new file mode 100644 index 00000000000..5c96c0056e0 --- /dev/null +++ b/seed/php-model/license/composer.json @@ -0,0 +1,40 @@ + +{ + "name": "seed/seed", + "version": "0.0.1", + "description": "Seed PHP Library", + "keywords": [ + "seed", + "api", + "sdk" + ], + "license": [], + "require": { + "php": "^8.1", + "ext-json": "*", + "guzzlehttp/guzzle": "^7.9" + }, + "require-dev": { + "phpunit/phpunit": "^9.0", + "friendsofphp/php-cs-fixer": "3.5.0", + "phpstan/phpstan": "^1.12" + }, + "autoload": { + "psr-4": { + "Seed\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "\\Seed\\Tests\\": "tests/" + } + }, + "scripts": { + "build": [ + "@php -l src", + "@php -l tests" + ], + "test": "phpunit", + "analyze": "phpstan analyze src tests" + } +} diff --git a/seed/php-model/license/phpstan.neon b/seed/php-model/license/phpstan.neon new file mode 100644 index 00000000000..29a11a92a19 --- /dev/null +++ b/seed/php-model/license/phpstan.neon @@ -0,0 +1,5 @@ +parameters: + level: max + paths: + - src + - tests \ No newline at end of file diff --git a/seed/php-model/license/phpunit.xml b/seed/php-model/license/phpunit.xml new file mode 100644 index 00000000000..54630a51163 --- /dev/null +++ b/seed/php-model/license/phpunit.xml @@ -0,0 +1,7 @@ + + + + tests + + + \ No newline at end of file diff --git a/seed/php-model/license/snippet-templates.json b/seed/php-model/license/snippet-templates.json new file mode 100644 index 00000000000..e69de29bb2d diff --git a/seed/php-model/license/snippet.json b/seed/php-model/license/snippet.json new file mode 100644 index 00000000000..e69de29bb2d diff --git a/seed/php-model/license/src/Core/Json/JsonDecoder.php b/seed/php-model/license/src/Core/Json/JsonDecoder.php new file mode 100644 index 00000000000..2ddff027348 --- /dev/null +++ b/seed/php-model/license/src/Core/Json/JsonDecoder.php @@ -0,0 +1,161 @@ + $type The type definition for deserialization. + * @return mixed[]|array The deserialized array. + * @throws JsonException If the decoded value is not an array. + */ + public static function decodeArray(string $json, array $type): array + { + $decoded = self::decode($json); + if (!is_array($decoded)) { + throw new JsonException("Unexpected non-array json value: " . $json); + } + return JsonDeserializer::deserializeArray($decoded, $type); + } + + /** + * Decodes a JSON string and deserializes it based on the provided union type definition. + * + * @param string $json The JSON string to decode. + * @param Union $union The union type definition for deserialization. + * @return mixed The deserialized value. + * @throws JsonException If the deserialization for all types in the union fails. + */ + public static function decodeUnion(string $json, Union $union): mixed + { + $decoded = self::decode($json); + return JsonDeserializer::deserializeUnion($decoded, $union); + } + /** + * Decodes a JSON string and returns a mixed. + * + * @param string $json The JSON string to decode. + * @return mixed The decoded mixed. + * @throws JsonException If the decoded value is not an mixed. + */ + public static function decodeMixed(string $json): mixed + { + return self::decode($json); + } + + /** + * Decodes a JSON string into a PHP value. + * + * @param string $json The JSON string to decode. + * @return mixed The decoded value. + * @throws JsonException If an error occurs during JSON decoding. + */ + public static function decode(string $json): mixed + { + return json_decode($json, associative: true, flags: JSON_THROW_ON_ERROR); + } +} diff --git a/seed/php-model/license/src/Core/Json/JsonDeserializer.php b/seed/php-model/license/src/Core/Json/JsonDeserializer.php new file mode 100644 index 00000000000..5f0ca2d7ed0 --- /dev/null +++ b/seed/php-model/license/src/Core/Json/JsonDeserializer.php @@ -0,0 +1,204 @@ + $data The array to be deserialized. + * @param mixed[]|array $type The type definition from the annotation. + * @return mixed[]|array The deserialized array. + * @throws JsonException If deserialization fails. + */ + public static function deserializeArray(array $data, array $type): array + { + return Utils::isMapType($type) + ? self::deserializeMap($data, $type) + : self::deserializeList($data, $type); + } + + /** + * Deserializes a value based on its type definition. + * + * @param mixed $data The data to deserialize. + * @param mixed $type The type definition. + * @return mixed The deserialized value. + * @throws JsonException If deserialization fails. + */ + private static function deserializeValue(mixed $data, mixed $type): mixed + { + if ($type instanceof Union) { + return self::deserializeUnion($data, $type); + } + + if (is_array($type)) { + return self::deserializeArray((array)$data, $type); + } + + if (gettype($type) != "string") { + throw new JsonException("Unexpected non-string type."); + } + + return self::deserializeSingleValue($data, $type); + } + + /** + * Deserializes a value based on the possible types in a union type definition. + * + * @param mixed $data The data to deserialize. + * @param Union $type The union type definition. + * @return mixed The deserialized value. + * @throws JsonException If none of the union types can successfully deserialize the value. + */ + public static function deserializeUnion(mixed $data, Union $type): mixed + { + foreach ($type->types as $unionType) { + try { + return self::deserializeValue($data, $unionType); + } catch (Exception) { + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot deserialize value of type $readableType with any of the union types: " . $type + ); + } + + /** + * Deserializes a single value based on its expected type. + * + * @param mixed $data The data to deserialize. + * @param string $type The expected type. + * @return mixed The deserialized value. + * @throws JsonException If deserialization fails. + */ + private static function deserializeSingleValue(mixed $data, string $type): mixed + { + if ($type === 'null' && $data === null) { + return null; + } + + if ($type === 'date' && is_string($data)) { + return self::deserializeDate($data); + } + + if ($type === 'datetime' && is_string($data)) { + return self::deserializeDateTime($data); + } + + if ($type === 'mixed') { + return $data; + } + + if (class_exists($type) && is_array($data)) { + return self::deserializeObject($data, $type); + } + + // Handle floats as a special case since gettype($data) returns "double" for float values in PHP, and because + // floats make come through from json_decoded as integers + if ($type === 'float' && (is_numeric($data))) { + return (float) $data; + } + + if (gettype($data) === $type) { + return $data; + } + + throw new JsonException("Unable to deserialize value of type '" . gettype($data) . "' as '$type'."); + } + + /** + * Deserializes an array into an object of the given type. + * + * @param array $data The data to deserialize. + * @param string $type The class name of the object to deserialize into. + * + * @return object The deserialized object. + * + * @throws JsonException If the type does not implement JsonSerializableType. + */ + public static function deserializeObject(array $data, string $type): object + { + if (!is_subclass_of($type, JsonSerializableType::class)) { + throw new JsonException("$type is not a subclass of JsonSerializableType."); + } + return $type::jsonDeserialize($data); + } + + /** + * Deserializes a map (associative array) with defined key and value types. + * + * @param array $data The associative array to deserialize. + * @param array $type The type definition for the map. + * @return array The deserialized map. + * @throws JsonException If deserialization fails. + */ + private static function deserializeMap(array $data, array $type): array + { + $keyType = array_key_first($type); + $valueType = $type[$keyType]; + $result = []; + + foreach ($data as $key => $item) { + $key = Utils::castKey($key, (string)$keyType); + $result[$key] = self::deserializeValue($item, $valueType); + } + + return $result; + } + + /** + * Deserializes a list (indexed array) with a defined value type. + * + * @param array $data The list to deserialize. + * @param array $type The type definition for the list. + * @return array The deserialized list. + * @throws JsonException If deserialization fails. + */ + private static function deserializeList(array $data, array $type): array + { + $valueType = $type[0]; + return array_map(fn ($item) => self::deserializeValue($item, $valueType), $data); + } +} diff --git a/seed/php-model/license/src/Core/Json/JsonEncoder.php b/seed/php-model/license/src/Core/Json/JsonEncoder.php new file mode 100644 index 00000000000..0dbf3fcc994 --- /dev/null +++ b/seed/php-model/license/src/Core/Json/JsonEncoder.php @@ -0,0 +1,20 @@ +jsonSerialize(); + $encoded = JsonEncoder::encode($serializedObject); + if (!$encoded) { + throw new Exception("Could not encode type"); + } + return $encoded; + } + + /** + * Serializes the object to an array. + * + * @return mixed[] Array representation of the object. + * @throws JsonException If serialization fails. + */ + public function jsonSerialize(): array + { + $result = []; + $reflectionClass = new \ReflectionClass($this); + + foreach ($reflectionClass->getProperties() as $property) { + $jsonKey = self::getJsonKey($property); + if ($jsonKey == null) { + continue; + } + $value = $property->getValue($this); + + // Handle DateTime properties + $dateTypeAttr = $property->getAttributes(Date::class)[0] ?? null; + if ($dateTypeAttr && $value instanceof DateTime) { + $dateType = $dateTypeAttr->newInstance()->type; + $value = ($dateType === Date::TYPE_DATE) + ? JsonSerializer::serializeDate($value) + : JsonSerializer::serializeDateTime($value); + } + + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonSerializer::serializeUnion($value, $unionType); + } + + // Handle arrays with type annotations + $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; + if ($arrayTypeAttr && is_array($value)) { + $arrayType = $arrayTypeAttr->newInstance()->type; + $value = JsonSerializer::serializeArray($value, $arrayType); + } + + // Handle object + if (is_object($value)) { + $value = JsonSerializer::serializeObject($value); + } + + if ($value !== null) { + $result[$jsonKey] = $value; + } + } + + return $result; + } + + /** + * Deserializes a JSON string into an instance of the calling class. + * + * @param string $json JSON string to deserialize. + * @return static Deserialized object. + * @throws JsonException If decoding fails or the result is not an array. + * @throws Exception If deserialization fails. + */ + public static function fromJson(string $json): static + { + $decodedJson = JsonDecoder::decode($json); + if (!is_array($decodedJson)) { + throw new JsonException("Unexpected non-array decoded type: " . gettype($decodedJson)); + } + return self::jsonDeserialize($decodedJson); + } + + /** + * Deserializes an array into an instance of the calling class. + * + * @param array $data Array data to deserialize. + * @return static Deserialized object. + * @throws JsonException If deserialization fails. + */ + public static function jsonDeserialize(array $data): static + { + $reflectionClass = new \ReflectionClass(static::class); + $constructor = $reflectionClass->getConstructor(); + + if ($constructor === null) { + throw new JsonException("No constructor found."); + } + + $args = []; + foreach ($reflectionClass->getProperties() as $property) { + $jsonKey = self::getJsonKey($property) ?? $property->getName(); + + if (array_key_exists($jsonKey, $data)) { + $value = $data[$jsonKey]; + + // Handle Date annotation + $dateTypeAttr = $property->getAttributes(Date::class)[0] ?? null; + if ($dateTypeAttr) { + $dateType = $dateTypeAttr->newInstance()->type; + if (!is_string($value)) { + throw new JsonException("Unexpected non-string type for date."); + } + $value = ($dateType === Date::TYPE_DATE) + ? JsonDeserializer::deserializeDate($value) + : JsonDeserializer::deserializeDateTime($value); + } + + // Handle Array annotation + $arrayTypeAttr = $property->getAttributes(ArrayType::class)[0] ?? null; + if (is_array($value) && $arrayTypeAttr) { + $arrayType = $arrayTypeAttr->newInstance()->type; + $value = JsonDeserializer::deserializeArray($value, $arrayType); + } + + // Handle Union annotations + $unionTypeAttr = $property->getAttributes(Union::class)[0] ?? null; + if ($unionTypeAttr) { + $unionType = $unionTypeAttr->newInstance(); + $value = JsonDeserializer::deserializeUnion($value, $unionType); + } + + // Handle object + $type = $property->getType(); + if (is_array($value) && $type instanceof ReflectionNamedType && !$type->isBuiltin()) { + $value = JsonDeserializer::deserializeObject($value, $type->getName()); + } + + $args[$property->getName()] = $value; + } else { + $defaultValue = $property->getDefaultValue() ?? null; + $args[$property->getName()] = $defaultValue; + } + } + // @phpstan-ignore-next-line + return new static($args); + } + + /** + * Retrieves the JSON key associated with a property. + * + * @param ReflectionProperty $property The reflection property. + * @return ?string The JSON key, or null if not available. + */ + private static function getJsonKey(ReflectionProperty $property): ?string + { + $jsonPropertyAttr = $property->getAttributes(JsonProperty::class)[0] ?? null; + return $jsonPropertyAttr?->newInstance()?->name; + } +} diff --git a/seed/php-model/license/src/Core/Json/JsonSerializer.php b/seed/php-model/license/src/Core/Json/JsonSerializer.php new file mode 100644 index 00000000000..7dd6fe517af --- /dev/null +++ b/seed/php-model/license/src/Core/Json/JsonSerializer.php @@ -0,0 +1,192 @@ +format(Constant::DateFormat); + } + + /** + * Serializes a DateTime object into a string using the date-time format. + * + * @param DateTime $date The DateTime object to serialize. + * @return string The serialized date-time string. + */ + public static function serializeDateTime(DateTime $date): string + { + return $date->format(Constant::DateTimeFormat); + } + + /** + * Serializes an array based on type annotations (either a list or map). + * + * @param mixed[]|array $data The array to be serialized. + * @param mixed[]|array $type The type definition from the annotation. + * @return mixed[]|array The serialized array. + * @throws JsonException If serialization fails. + */ + public static function serializeArray(array $data, array $type): array + { + return Utils::isMapType($type) + ? self::serializeMap($data, $type) + : self::serializeList($data, $type); + } + + /** + * Serializes a value based on its type definition. + * + * @param mixed $data The value to serialize. + * @param mixed $type The type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails. + */ + private static function serializeValue(mixed $data, mixed $type): mixed + { + if ($type instanceof Union) { + return self::serializeUnion($data, $type); + } + + if (is_array($type)) { + return self::serializeArray((array)$data, $type); + } + + if (gettype($type) != "string") { + throw new JsonException("Unexpected non-string type."); + } + + return self::serializeSingleValue($data, $type); + } + + /** + * Serializes a value for a union type definition. + * + * @param mixed $data The value to serialize. + * @param Union $unionType The union type definition. + * @return mixed The serialized value. + * @throws JsonException If serialization fails for all union types. + */ + public static function serializeUnion(mixed $data, Union $unionType): mixed + { + foreach ($unionType->types as $type) { + try { + return self::serializeValue($data, $type); + } catch (Exception) { + // Try the next type in the union + continue; + } + } + $readableType = Utils::getReadableType($data); + throw new JsonException( + "Cannot serialize value of type $readableType with any of the union types: " . $unionType + ); + } + + /** + * Serializes a single value based on its type. + * + * @param mixed $data The value to serialize. + * @param string $type The expected type. + * @return mixed The serialized value. + * @throws JsonException If serialization fails. + */ + private static function serializeSingleValue(mixed $data, string $type): mixed + { + if ($type === 'null' && $data === null) { + return null; + } + + if (($type === 'date' || $type === 'datetime') && $data instanceof DateTime) { + return $type === 'date' ? self::serializeDate($data) : self::serializeDateTime($data); + } + + if ($type === 'mixed') { + return $data; + } + + if (class_exists($type) && $data instanceof $type) { + return self::serializeObject($data); + } + + // Handle floats as a special case since gettype($data) returns "double" for float values in PHP. + if ($type === 'float' && is_float($data)) { + return $data; + } + + if (gettype($data) === $type) { + return $data; + } + + throw new JsonException("Unable to serialize value of type '" . gettype($data) . "' as '$type'."); + } + + /** + * Serializes an object to a JSON-serializable format. + * + * @param object $data The object to serialize. + * @return mixed The serialized data. + * @throws JsonException If the object does not implement JsonSerializable. + */ + public static function serializeObject(object $data): mixed + { + if (!is_subclass_of($data, JsonSerializable::class)) { + $type = get_class($data); + throw new JsonException("Class $type must implement JsonSerializable."); + } + return $data->jsonSerialize(); + } + + /** + * Serializes a map (associative array) with defined key and value types. + * + * @param array $data The associative array to serialize. + * @param array $type The type definition for the map. + * @return array The serialized map. + * @throws JsonException If serialization fails. + */ + private static function serializeMap(array $data, array $type): array + { + $keyType = array_key_first($type); + if ($keyType === null) { + throw new JsonException("Unexpected no key in ArrayType."); + } + $valueType = $type[$keyType]; + $result = []; + + foreach ($data as $key => $item) { + $key = Utils::castKey($key, $keyType); + $result[$key] = self::serializeValue($item, $valueType); + } + + return $result; + } + + /** + * Serializes a list (indexed array) where only the value type is defined. + * + * @param array $data The list to serialize. + * @param array $type The type definition for the list. + * @return array The serialized list. + * @throws JsonException If serialization fails. + */ + private static function serializeList(array $data, array $type): array + { + $valueType = $type[0]; + return array_map(fn ($item) => self::serializeValue($item, $valueType), $data); + } +} diff --git a/seed/php-model/license/src/Core/Json/Utils.php b/seed/php-model/license/src/Core/Json/Utils.php new file mode 100644 index 00000000000..7577c058916 --- /dev/null +++ b/seed/php-model/license/src/Core/Json/Utils.php @@ -0,0 +1,61 @@ + $type The type definition from the annotation. + * @return bool True if the type is a map, false if it's a list. + */ + public static function isMapType(array $type): bool + { + return count($type) === 1 && !array_is_list($type); + } + + /** + * Casts the key to the appropriate type based on the key type. + * + * @param mixed $key The key to be cast. + * @param string $keyType The type to cast the key to ('string', 'integer', 'float'). + * @return mixed The casted key. + * @throws JsonException + */ + public static function castKey(mixed $key, string $keyType): mixed + { + if (!is_scalar($key)) { + throw new JsonException("Key must be a scalar type."); + } + return match ($keyType) { + 'integer' => (int)$key, + 'float' => (float)$key, + 'string' => (string)$key, + default => $key, + }; + } + + /** + * Returns a human-readable representation of the input's type. + * + * @param mixed $input The input value to determine the type of. + * @return string A readable description of the input type. + */ + public static function getReadableType(mixed $input): string + { + if (is_object($input)) { + return get_class($input); + } elseif (is_array($input)) { + return 'array(' . count($input) . ' items)'; + } elseif (is_null($input)) { + return 'null'; + } else { + return gettype($input); + } + } +} diff --git a/seed/php-model/license/src/Core/Types/ArrayType.php b/seed/php-model/license/src/Core/Types/ArrayType.php new file mode 100644 index 00000000000..a26d29008ec --- /dev/null +++ b/seed/php-model/license/src/Core/Types/ArrayType.php @@ -0,0 +1,16 @@ + 'valueType'] for maps, or ['valueType'] for lists + */ + public function __construct(public array $type) + { + } +} diff --git a/seed/php-model/license/src/Core/Types/Constant.php b/seed/php-model/license/src/Core/Types/Constant.php new file mode 100644 index 00000000000..5ac4518cc6d --- /dev/null +++ b/seed/php-model/license/src/Core/Types/Constant.php @@ -0,0 +1,12 @@ +> The types allowed for this property, which can be strings, arrays, or nested Union types. + */ + public array $types; + + /** + * Constructor for the Union attribute. + * + * @param string|Union|array ...$types The list of types that the property can accept. + * This can include primitive types (e.g., 'string', 'int'), arrays, or other Union instances. + * + * Example: + * ```php + * #[Union('string', 'null', 'date', new Union('boolean', 'int'))] + * ``` + */ + public function __construct(string|Union|array ...$types) + { + $this->types = $types; + } + + /** + * Converts the Union type to a string representation. + * + * @return string A string representation of the union types. + */ + public function __toString(): string + { + return implode(' | ', array_map(function ($type) { + if (is_string($type)) { + return $type; + } elseif ($type instanceof Union) { + return (string) $type; // Recursively handle nested unions + } elseif (is_array($type)) { + return 'array'; // Handle arrays + } + }, $this->types)); + } +} diff --git a/seed/php-model/license/src/Type.php b/seed/php-model/license/src/Type.php new file mode 100644 index 00000000000..6e0056d463b --- /dev/null +++ b/seed/php-model/license/src/Type.php @@ -0,0 +1,29 @@ +name = $values['name']; + } +} diff --git a/seed/php-model/license/tests/Seed/Core/Json/DateArrayTest.php b/seed/php-model/license/tests/Seed/Core/Json/DateArrayTest.php new file mode 100644 index 00000000000..a72cfdbdd22 --- /dev/null +++ b/seed/php-model/license/tests/Seed/Core/Json/DateArrayTest.php @@ -0,0 +1,54 @@ +dates = $values['dates']; + } +} + +class DateArrayTest extends TestCase +{ + public function testDateTimeInArrays(): void + { + $expectedJson = json_encode( + [ + 'dates' => ['2023-01-01', '2023-02-01', '2023-03-01'] + ], + JSON_THROW_ON_ERROR + ); + + $object = DateArray::fromJson($expectedJson); + $this->assertInstanceOf(DateTime::class, $object->dates[0], 'dates[0] should be a DateTime instance.'); + $this->assertEquals('2023-01-01', $object->dates[0]->format('Y-m-d'), 'dates[0] should have the correct date.'); + $this->assertInstanceOf(DateTime::class, $object->dates[1], 'dates[1] should be a DateTime instance.'); + $this->assertEquals('2023-02-01', $object->dates[1]->format('Y-m-d'), 'dates[1] should have the correct date.'); + $this->assertInstanceOf(DateTime::class, $object->dates[2], 'dates[2] should be a DateTime instance.'); + $this->assertEquals('2023-03-01', $object->dates[2]->format('Y-m-d'), 'dates[2] should have the correct date.'); + + $actualJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($expectedJson, $actualJson, 'Serialized JSON does not match original JSON for dates array.'); + } +} diff --git a/seed/php-model/license/tests/Seed/Core/Json/EmptyArrayTest.php b/seed/php-model/license/tests/Seed/Core/Json/EmptyArrayTest.php new file mode 100644 index 00000000000..d243a08916d --- /dev/null +++ b/seed/php-model/license/tests/Seed/Core/Json/EmptyArrayTest.php @@ -0,0 +1,71 @@ + $emptyMapArray + */ + #[JsonProperty('empty_map_array')] + #[ArrayType(['integer' => new Union('string', 'null')])] + public array $emptyMapArray; + + /** + * @var array $emptyDatesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('empty_dates_array')] + public array $emptyDatesArray; + + /** + * @param array{ + * emptyStringArray: string[], + * emptyMapArray: array, + * emptyDatesArray: array, + * } $values + */ + public function __construct( + array $values, + ) { + $this->emptyStringArray = $values['emptyStringArray']; + $this->emptyMapArray = $values['emptyMapArray']; + $this->emptyDatesArray = $values['emptyDatesArray']; + } +} + +class EmptyArrayTest extends TestCase +{ + public function testEmptyArray(): void + { + $expectedJson = json_encode( + [ + 'empty_string_array' => [], + 'empty_map_array' => [], + 'empty_dates_array' => [] + ], + JSON_THROW_ON_ERROR + ); + + $object = EmptyArray::fromJson($expectedJson); + $this->assertEmpty($object->emptyStringArray, 'empty_string_array should be empty.'); + $this->assertEmpty($object->emptyMapArray, 'empty_map_array should be empty.'); + $this->assertEmpty($object->emptyDatesArray, 'empty_dates_array should be empty.'); + + $actualJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($expectedJson, $actualJson, 'Serialized JSON does not match original JSON for EmptyArraysType.'); + } +} diff --git a/seed/php-model/license/tests/Seed/Core/Json/EnumTest.php b/seed/php-model/license/tests/Seed/Core/Json/EnumTest.php new file mode 100644 index 00000000000..bf83d5b8ab0 --- /dev/null +++ b/seed/php-model/license/tests/Seed/Core/Json/EnumTest.php @@ -0,0 +1,76 @@ +value; + } +} + +class ShapeType extends JsonSerializableType +{ + /** + * @var Shape $shape + */ + #[JsonProperty('shape')] + public Shape $shape; + + /** + * @var Shape[] $shapes + */ + #[ArrayType([Shape::class])] + #[JsonProperty('shapes')] + public array $shapes; + + /** + * @param Shape $shape + * @param Shape[] $shapes + */ + public function __construct( + Shape $shape, + array $shapes, + ) { + $this->shape = $shape; + $this->shapes = $shapes; + } +} + +class EnumTest extends TestCase +{ + public function testEnumSerialization(): void + { + $object = new ShapeType( + Shape::Circle, + [Shape::Square, Shape::Circle, Shape::Triangle] + ); + + $expectedJson = json_encode([ + 'shape' => 'CIRCLE', + 'shapes' => ['SQUARE', 'CIRCLE', 'TRIANGLE'] + ], JSON_THROW_ON_ERROR); + + $actualJson = $object->toJson(); + + $this->assertJsonStringEqualsJsonString( + $expectedJson, + $actualJson, + 'Serialized JSON does not match expected JSON for shape and shapes properties.' + ); + } +} diff --git a/seed/php-model/license/tests/Seed/Core/Json/ExhaustiveTest.php b/seed/php-model/license/tests/Seed/Core/Json/ExhaustiveTest.php new file mode 100644 index 00000000000..f542d6a535d --- /dev/null +++ b/seed/php-model/license/tests/Seed/Core/Json/ExhaustiveTest.php @@ -0,0 +1,197 @@ +nestedProperty = $values['nestedProperty']; + } +} + +class Type extends JsonSerializableType +{ + /** + * @var Nested nestedType + */ + #[JsonProperty('nested_type')] + public Nested $nestedType; /** + + * @var string $simpleProperty + */ + #[JsonProperty('simple_property')] + public string $simpleProperty; + + /** + * @var DateTime $dateProperty + */ + #[Date(Date::TYPE_DATE)] + #[JsonProperty('date_property')] + public DateTime $dateProperty; + + /** + * @var DateTime $datetimeProperty + */ + #[Date(Date::TYPE_DATETIME)] + #[JsonProperty('datetime_property')] + public DateTime $datetimeProperty; + + /** + * @var array $stringArray + */ + #[ArrayType(['string'])] + #[JsonProperty('string_array')] + public array $stringArray; + + /** + * @var array $mapProperty + */ + #[ArrayType(['string' => 'integer'])] + #[JsonProperty('map_property')] + public array $mapProperty; + + /** + * @var array $objectArray + */ + #[ArrayType(['integer' => new Union(Nested::class, 'null')])] + #[JsonProperty('object_array')] + public array $objectArray; + + /** + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union('string', 'null')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @var array $datesArray + */ + #[ArrayType([new Union('date', 'null')])] + #[JsonProperty('dates_array')] + public array $datesArray; + + /** + * @var string|null $nullableProperty + */ + #[JsonProperty('nullable_property')] + public ?string $nullableProperty; + + /** + * @param array{ + * nestedType: Nested, + * simpleProperty: string, + * dateProperty: DateTime, + * datetimeProperty: DateTime, + * stringArray: array, + * mapProperty: array, + * objectArray: array, + * nestedArray: array>, + * datesArray: array, + * nullableProperty?: string|null, + * } $values + */ + public function __construct( + array $values, + ) { + $this->nestedType = $values['nestedType']; + $this->simpleProperty = $values['simpleProperty']; + $this->dateProperty = $values['dateProperty']; + $this->datetimeProperty = $values['datetimeProperty']; + $this->stringArray = $values['stringArray']; + $this->mapProperty = $values['mapProperty']; + $this->objectArray = $values['objectArray']; + $this->nestedArray = $values['nestedArray']; + $this->datesArray = $values['datesArray']; + $this->nullableProperty = $values['nullableProperty'] ?? null; + } +} + +class ExhaustiveTest extends TestCase +{ + /** + * Test serialization and deserialization of all types in Type. + */ + public function testExhaustive(): void + { + $expectedJson = json_encode( + [ + 'nested_type' => ['nested_property' => '1995-07-20'], + 'simple_property' => 'Test String', + // Omit 'nullable_property' to test null serialization + 'date_property' => '2023-01-01', + 'datetime_property' => '2023-01-01T12:34:56+00:00', + 'string_array' => ['one', 'two', 'three'], + 'map_property' => ['key1' => 1, 'key2' => 2], + 'object_array' => [ + 1 => ['nested_property' => '2021-07-20'], + 2 => null, // Testing nullable objects in array + ], + 'nested_array' => [ + 1 => [1 => 'value1', 2 => null], // Testing nullable strings in nested array + 2 => [3 => 'value3', 4 => 'value4'] + ], + 'dates_array' => ['2023-01-01', null, '2023-03-01'] // Testing nullable dates in array> + ], + JSON_THROW_ON_ERROR + ); + + $object = Type::fromJson($expectedJson); + + // Check that nullable property is null and not included in JSON + $this->assertNull($object->nullableProperty, 'Nullable property should be null.'); + + // Check date properties + $this->assertInstanceOf(DateTime::class, $object->dateProperty, 'date_property should be a DateTime instance.'); + $this->assertEquals('2023-01-01', $object->dateProperty->format('Y-m-d'), 'date_property should have the correct date.'); + $this->assertInstanceOf(DateTime::class, $object->datetimeProperty, 'datetime_property should be a DateTime instance.'); + $this->assertEquals('2023-01-01 12:34:56', $object->datetimeProperty->format('Y-m-d H:i:s'), 'datetime_property should have the correct datetime.'); + + // Check scalar arrays + $this->assertEquals(['one', 'two', 'three'], $object->stringArray, 'string_array should match the original data.'); + $this->assertEquals(['key1' => 1, 'key2' => 2], $object->mapProperty, 'map_property should match the original data.'); + + // Check object array with nullable elements + $this->assertInstanceOf(Nested::class, $object->objectArray[1], 'object_array[1] should be an instance of TestNestedType1.'); + $this->assertEquals('2021-07-20', $object->objectArray[1]->nestedProperty->format('Y-m-d'), 'object_array[1]->nestedProperty should match the original data.'); + $this->assertNull($object->objectArray[2], 'object_array[2] should be null.'); + + // Check nested array with nullable strings + $this->assertEquals('value1', $object->nestedArray[1][1], 'nested_array[1][1] should match the original data.'); + $this->assertNull($object->nestedArray[1][2], 'nested_array[1][2] should be null.'); + $this->assertEquals('value3', $object->nestedArray[2][3], 'nested_array[2][3] should match the original data.'); + $this->assertEquals('value4', $object->nestedArray[2][4], 'nested_array[2][4] should match the original data.'); + + // Check dates array with nullable DateTime objects + $this->assertInstanceOf(DateTime::class, $object->datesArray[0], 'dates_array[0] should be a DateTime instance.'); + $this->assertEquals('2023-01-01', $object->datesArray[0]->format('Y-m-d'), 'dates_array[0] should have the correct date.'); + $this->assertNull($object->datesArray[1], 'dates_array[1] should be null.'); + $this->assertInstanceOf(DateTime::class, $object->datesArray[2], 'dates_array[2] should be a DateTime instance.'); + $this->assertEquals('2023-03-01', $object->datesArray[2]->format('Y-m-d'), 'dates_array[2] should have the correct date.'); + + $actualJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($expectedJson, $actualJson, 'The serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-model/license/tests/Seed/Core/Json/InvalidTest.php b/seed/php-model/license/tests/Seed/Core/Json/InvalidTest.php new file mode 100644 index 00000000000..7d1d79406a5 --- /dev/null +++ b/seed/php-model/license/tests/Seed/Core/Json/InvalidTest.php @@ -0,0 +1,42 @@ +integerProperty = $values['integerProperty']; + } +} + +class InvalidTest extends TestCase +{ + public function testInvalidJsonThrowsException(): void + { + $this->expectException(\TypeError::class); + $json = json_encode( + [ + 'integer_property' => 'not_an_integer' + ], + JSON_THROW_ON_ERROR + ); + Invalid::fromJson($json); + } +} diff --git a/seed/php-model/license/tests/Seed/Core/Json/NestedUnionArrayTest.php b/seed/php-model/license/tests/Seed/Core/Json/NestedUnionArrayTest.php new file mode 100644 index 00000000000..0fcdd06667e --- /dev/null +++ b/seed/php-model/license/tests/Seed/Core/Json/NestedUnionArrayTest.php @@ -0,0 +1,89 @@ +nestedProperty = $values['nestedProperty']; + } +} + +class NestedUnionArray extends JsonSerializableType +{ + /** + * @var array> $nestedArray + */ + #[ArrayType(['integer' => ['integer' => new Union(UnionObject::class, 'null', 'date')]])] + #[JsonProperty('nested_array')] + public array $nestedArray; + + /** + * @param array{ + * nestedArray: array>, + * } $values + */ + public function __construct( + array $values, + ) { + $this->nestedArray = $values['nestedArray']; + } +} + +class NestedUnionArrayTest extends TestCase +{ + public function testNestedUnionArray(): void + { + $expectedJson = json_encode( + [ + 'nested_array' => [ + 1 => [ + 1 => ['nested_property' => 'Nested One'], + 2 => null, + 4 => '2023-01-02' + ], + 2 => [ + 5 => ['nested_property' => 'Nested Two'], + 7 => '2023-02-02' + ] + ] + ], + JSON_THROW_ON_ERROR + ); + + $object = NestedUnionArray::fromJson($expectedJson); + $this->assertInstanceOf(UnionObject::class, $object->nestedArray[1][1], 'nested_array[1][1] should be an instance of Object.'); + $this->assertEquals('Nested One', $object->nestedArray[1][1]->nestedProperty, 'nested_array[1][1]->nestedProperty should match the original data.'); + $this->assertNull($object->nestedArray[1][2], 'nested_array[1][2] should be null.'); + $this->assertInstanceOf(DateTime::class, $object->nestedArray[1][4], 'nested_array[1][4] should be a DateTime instance.'); + $this->assertEquals('2023-01-02T00:00:00+00:00', $object->nestedArray[1][4]->format(Constant::DateTimeFormat), 'nested_array[1][4] should have the correct datetime.'); + $this->assertInstanceOf(UnionObject::class, $object->nestedArray[2][5], 'nested_array[2][5] should be an instance of Object.'); + $this->assertEquals('Nested Two', $object->nestedArray[2][5]->nestedProperty, 'nested_array[2][5]->nestedProperty should match the original data.'); + $this->assertInstanceOf(DateTime::class, $object->nestedArray[2][7], 'nested_array[1][4] should be a DateTime instance.'); + $this->assertEquals('2023-02-02', $object->nestedArray[2][7]->format('Y-m-d'), 'nested_array[1][4] should have the correct date.'); + + $actualJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($expectedJson, $actualJson, 'Serialized JSON does not match original JSON for nested_array.'); + } +} diff --git a/seed/php-model/license/tests/Seed/Core/Json/NullPropertyTest.php b/seed/php-model/license/tests/Seed/Core/Json/NullPropertyTest.php new file mode 100644 index 00000000000..ce20a244282 --- /dev/null +++ b/seed/php-model/license/tests/Seed/Core/Json/NullPropertyTest.php @@ -0,0 +1,53 @@ +nonNullProperty = $values['nonNullProperty']; + $this->nullProperty = $values['nullProperty'] ?? null; + } +} + +class NullPropertyTest extends TestCase +{ + public function testNullPropertiesAreOmitted(): void + { + $object = new NullProperty( + [ + "nonNullProperty" => "Test String", + "nullProperty" => null + ] + ); + + $serialized = $object->jsonSerialize(); + $this->assertArrayHasKey('non_null_property', $serialized, 'non_null_property should be present in the serialized JSON.'); + $this->assertArrayNotHasKey('null_property', $serialized, 'null_property should be omitted from the serialized JSON.'); + $this->assertEquals('Test String', $serialized['non_null_property'], 'non_null_property should have the correct value.'); + } +} diff --git a/seed/php-model/license/tests/Seed/Core/Json/NullableArrayTest.php b/seed/php-model/license/tests/Seed/Core/Json/NullableArrayTest.php new file mode 100644 index 00000000000..fe0f19de6b1 --- /dev/null +++ b/seed/php-model/license/tests/Seed/Core/Json/NullableArrayTest.php @@ -0,0 +1,49 @@ + $nullableStringArray + */ + #[ArrayType([new Union('string', 'null')])] + #[JsonProperty('nullable_string_array')] + public array $nullableStringArray; + + /** + * @param array{ + * nullableStringArray: array, + * } $values + */ + public function __construct( + array $values, + ) { + $this->nullableStringArray = $values['nullableStringArray']; + } +} + +class NullableArrayTest extends TestCase +{ + public function testNullableArray(): void + { + $expectedJson = json_encode( + [ + 'nullable_string_array' => ['one', null, 'three'] + ], + JSON_THROW_ON_ERROR + ); + + $object = NullableArray::fromJson($expectedJson); + $this->assertEquals(['one', null, 'three'], $object->nullableStringArray, 'nullable_string_array should match the original data.'); + + $actualJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($expectedJson, $actualJson, 'Serialized JSON does not match original JSON for nullable_string_array.'); + } +} diff --git a/seed/php-model/license/tests/Seed/Core/Json/ScalarTest.php b/seed/php-model/license/tests/Seed/Core/Json/ScalarTest.php new file mode 100644 index 00000000000..604b7c0b959 --- /dev/null +++ b/seed/php-model/license/tests/Seed/Core/Json/ScalarTest.php @@ -0,0 +1,116 @@ + $intFloatArray + */ + #[ArrayType([new Union('integer', 'float')])] + #[JsonProperty('int_float_array')] + public array $intFloatArray; + + /** + * @var array $floatArray + */ + #[ArrayType(['float'])] + #[JsonProperty('float_array')] + public array $floatArray; + + /** + * @var bool|null $nullableBooleanProperty + */ + #[JsonProperty('nullable_boolean_property')] + public ?bool $nullableBooleanProperty; + + /** + * @param array{ + * integerProperty: int, + * floatProperty: float, + * otherFloatProperty: float, + * booleanProperty: bool, + * stringProperty: string, + * intFloatArray: array, + * floatArray: array, + * nullableBooleanProperty?: bool|null, + * } $values + */ + public function __construct( + array $values, + ) { + $this->integerProperty = $values['integerProperty']; + $this->floatProperty = $values['floatProperty']; + $this->otherFloatProperty = $values['otherFloatProperty']; + $this->booleanProperty = $values['booleanProperty']; + $this->stringProperty = $values['stringProperty']; + $this->intFloatArray = $values['intFloatArray']; + $this->floatArray = $values['floatArray']; + $this->nullableBooleanProperty = $values['nullableBooleanProperty'] ?? null; + } +} + +class ScalarTest extends TestCase +{ + public function testAllScalarTypesIncludingFloat(): void + { + $expectedJson = json_encode( + [ + 'integer_property' => 42, + 'float_property' => 3.14159, + 'other_float_property' => 3, + 'boolean_property' => true, + 'string_property' => 'Hello, World!', + 'int_float_array' => [1, 2.5, 3, 4.75], + 'float_array' => [1, 2, 3, 4] // Ensure we handle "integer-looking" floats + ], + JSON_THROW_ON_ERROR + ); + + $object = Scalar::fromJson($expectedJson); + $this->assertEquals(42, $object->integerProperty, 'integer_property should be 42.'); + $this->assertEquals(3.14159, $object->floatProperty, 'float_property should be 3.14159.'); + $this->assertTrue($object->booleanProperty, 'boolean_property should be true.'); + $this->assertEquals('Hello, World!', $object->stringProperty, 'string_property should be "Hello, World!".'); + $this->assertNull($object->nullableBooleanProperty, 'nullable_boolean_property should be null.'); + $this->assertEquals([1, 2.5, 3, 4.75], $object->intFloatArray, 'int_float_array should match the original data.'); + + $actualJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($expectedJson, $actualJson, 'Serialized JSON does not match original JSON for ScalarTypesTest.'); + } +} diff --git a/seed/php-model/license/tests/Seed/Core/Json/TraitTest.php b/seed/php-model/license/tests/Seed/Core/Json/TraitTest.php new file mode 100644 index 00000000000..837f239115f --- /dev/null +++ b/seed/php-model/license/tests/Seed/Core/Json/TraitTest.php @@ -0,0 +1,60 @@ +integerProperty = $values['integerProperty']; + $this->stringProperty = $values['stringProperty']; + } +} + +class TraitTest extends TestCase +{ + public function testTraitPropertyAndString(): void + { + $expectedJson = json_encode( + [ + 'integer_property' => 42, + 'string_property' => 'Hello, World!', + ], + JSON_THROW_ON_ERROR + ); + + $object = TypeWithTrait::fromJson($expectedJson); + $this->assertEquals(42, $object->integerProperty, 'integer_property should be 42.'); + $this->assertEquals('Hello, World!', $object->stringProperty, 'string_property should be "Hello, World!".'); + + $actualJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($expectedJson, $actualJson, 'Serialized JSON does not match original JSON for ScalarTypesTestWithTrait.'); + } +} diff --git a/seed/php-model/license/tests/Seed/Core/Json/UnionArrayTest.php b/seed/php-model/license/tests/Seed/Core/Json/UnionArrayTest.php new file mode 100644 index 00000000000..09933d2321d --- /dev/null +++ b/seed/php-model/license/tests/Seed/Core/Json/UnionArrayTest.php @@ -0,0 +1,57 @@ + $mixedDates + */ + #[ArrayType(['integer' => new Union('datetime', 'string', 'null')])] + #[JsonProperty('mixed_dates')] + public array $mixedDates; + + /** + * @param array{ + * mixedDates: array, + * } $values + */ + public function __construct( + array $values, + ) { + $this->mixedDates = $values['mixedDates']; + } +} + +class UnionArrayTest extends TestCase +{ + public function testUnionArray(): void + { + $expectedJson = json_encode( + [ + 'mixed_dates' => [ + 1 => '2023-01-01T12:00:00+00:00', + 2 => null, + 3 => 'Some String' + ] + ], + JSON_THROW_ON_ERROR + ); + + $object = UnionArray::fromJson($expectedJson); + $this->assertInstanceOf(DateTime::class, $object->mixedDates[1], 'mixed_dates[1] should be a DateTime instance.'); + $this->assertEquals('2023-01-01 12:00:00', $object->mixedDates[1]->format('Y-m-d H:i:s'), 'mixed_dates[1] should have the correct datetime.'); + $this->assertNull($object->mixedDates[2], 'mixed_dates[2] should be null.'); + $this->assertEquals('Some String', $object->mixedDates[3], 'mixed_dates[3] should be "Some String".'); + + $actualJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($expectedJson, $actualJson, 'Serialized JSON does not match original JSON for mixed_dates.'); + } +} diff --git a/seed/php-model/license/tests/Seed/Core/Json/UnionPropertyTest.php b/seed/php-model/license/tests/Seed/Core/Json/UnionPropertyTest.php new file mode 100644 index 00000000000..3119baace62 --- /dev/null +++ b/seed/php-model/license/tests/Seed/Core/Json/UnionPropertyTest.php @@ -0,0 +1,115 @@ + 'integer'], UnionProperty::class)] + #[JsonProperty('complexUnion')] + public mixed $complexUnion; + + /** + * @param array{ + * complexUnion: string|int|null|array|UnionProperty + * } $values + */ + public function __construct( + array $values, + ) { + $this->complexUnion = $values['complexUnion']; + } +} + +class UnionPropertyTest extends TestCase +{ + public function testWithMapOfIntToInt(): void + { + $expectedJson = json_encode( + [ + 'complexUnion' => [1 => 100, 2 => 200] + ], + JSON_THROW_ON_ERROR + ); + + $object = UnionProperty::fromJson($expectedJson); + $this->assertIsArray($object->complexUnion, 'complexUnion should be an array.'); + $this->assertEquals([1 => 100, 2 => 200], $object->complexUnion, 'complexUnion should match the original map of int => int.'); + + $actualJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($expectedJson, $actualJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNestedUnionPropertyType(): void + { + $expectedJson = json_encode( + [ + 'complexUnion' => new UnionProperty( + [ + 'complexUnion' => 'Nested String' + ] + ) + ], + JSON_THROW_ON_ERROR + ); + + $object = UnionProperty::fromJson($expectedJson); + $this->assertInstanceOf(UnionProperty::class, $object->complexUnion, 'complexUnion should be an instance of UnionPropertyType.'); + $this->assertEquals('Nested String', $object->complexUnion->complexUnion, 'Nested complexUnion should match the original value.'); + + $actualJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($expectedJson, $actualJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithNull(): void + { + $expectedJson = json_encode( + [], + JSON_THROW_ON_ERROR + ); + + $object = UnionProperty::fromJson($expectedJson); + $this->assertNull($object->complexUnion, 'complexUnion should be null.'); + + $actualJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($expectedJson, $actualJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithInteger(): void + { + $expectedJson = json_encode( + [ + 'complexUnion' => 42 + ], + JSON_THROW_ON_ERROR + ); + + $object = UnionProperty::fromJson($expectedJson); + $this->assertIsInt($object->complexUnion, 'complexUnion should be an integer.'); + $this->assertEquals(42, $object->complexUnion, 'complexUnion should match the original integer.'); + + $actualJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($expectedJson, $actualJson, 'Serialized JSON does not match the original JSON.'); + } + + public function testWithString(): void + { + $expectedJson = json_encode( + [ + 'complexUnion' => 'Some String' + ], + JSON_THROW_ON_ERROR + ); + + $object = UnionProperty::fromJson($expectedJson); + $this->assertIsString($object->complexUnion, 'complexUnion should be a string.'); + $this->assertEquals('Some String', $object->complexUnion, 'complexUnion should match the original string.'); + + $actualJson = $object->toJson(); + $this->assertJsonStringEqualsJsonString($expectedJson, $actualJson, 'Serialized JSON does not match the original JSON.'); + } +} diff --git a/seed/php-sdk/file-upload/.mock/definition/service.yml b/seed/php-sdk/file-upload/.mock/definition/service.yml index 5ae95021c2d..956e6ba73ba 100644 --- a/seed/php-sdk/file-upload/.mock/definition/service.yml +++ b/seed/php-sdk/file-upload/.mock/definition/service.yml @@ -64,6 +64,9 @@ service: bar: type: MyObject content-type: application/json + foobar: + type: optional + content-type: application/json types: Id: string diff --git a/seed/php-sdk/file-upload/src/Service/Requests/WithContentTypeRequest.php b/seed/php-sdk/file-upload/src/Service/Requests/WithContentTypeRequest.php index c30015ed1a7..e2f619bcebb 100644 --- a/seed/php-sdk/file-upload/src/Service/Requests/WithContentTypeRequest.php +++ b/seed/php-sdk/file-upload/src/Service/Requests/WithContentTypeRequest.php @@ -26,11 +26,18 @@ class WithContentTypeRequest extends JsonSerializableType #[JsonProperty('bar')] public MyObject $bar; + /** + * @var ?MyObject $foobar + */ + #[JsonProperty('foobar')] + public ?MyObject $foobar; + /** * @param array{ * file: File, * foo: string, * bar: MyObject, + * foobar?: ?MyObject, * } $values */ public function __construct( @@ -39,5 +46,6 @@ public function __construct( $this->file = $values['file']; $this->foo = $values['foo']; $this->bar = $values['bar']; + $this->foobar = $values['foobar'] ?? null; } } diff --git a/seed/php-sdk/file-upload/src/Service/ServiceClient.php b/seed/php-sdk/file-upload/src/Service/ServiceClient.php index d7b9cc634e2..8984c6e0799 100644 --- a/seed/php-sdk/file-upload/src/Service/ServiceClient.php +++ b/seed/php-sdk/file-upload/src/Service/ServiceClient.php @@ -207,6 +207,13 @@ public function withContentType(WithContentTypeRequest $request, ?array $options value: $request->bar->toJson(), contentType: 'application/json', ); + if ($request->foobar != null) { + $body->add( + name: 'foobar', + value: $request->foobar->toJson(), + contentType: 'application/json', + ); + } try { $response = $this->client->sendRequest( new MultipartApiRequest(