private/apigen: handle omitempty JSON option in structs
This change makes the TypeScript API code generator properly handle struct fields with the "omitempty" option in the JSON struct tag. Change-Id: I9b22ce33a8b8c39c115ec827a8e5b7e85d856f83
This commit is contained in:
parent
cbc82690d7
commit
0c591fa25a
@ -121,6 +121,20 @@ func isNillableType(t reflect.Type) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// isJSONOmittableType returns whether the "omitempty" JSON tag option works with struct fields of this type.
|
||||
func isJSONOmittableType(t reflect.Type) bool {
|
||||
switch t.Kind() {
|
||||
case reflect.Array, reflect.Map, reflect.Slice, reflect.String,
|
||||
reflect.Bool,
|
||||
reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
|
||||
reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr,
|
||||
reflect.Float32, reflect.Float64,
|
||||
reflect.Interface, reflect.Pointer:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func capitalize(s string) string {
|
||||
r, size := utf8.DecodeRuneInString(s)
|
||||
if size <= 0 {
|
||||
@ -167,3 +181,39 @@ func filter(types []typeAndName, keep func(typeAndName) bool) []typeAndName {
|
||||
}
|
||||
return filtered
|
||||
}
|
||||
|
||||
type jsonTagInfo struct {
|
||||
FieldName string
|
||||
OmitEmpty bool
|
||||
Skip bool
|
||||
}
|
||||
|
||||
func parseJSONTag(structType reflect.Type, field reflect.StructField) jsonTagInfo {
|
||||
tag, ok := field.Tag.Lookup("json")
|
||||
if !ok {
|
||||
panic(fmt.Sprintf("(%s).%s missing json tag", structType.String(), field.Name))
|
||||
}
|
||||
|
||||
options := strings.Split(tag, ",")
|
||||
for i, opt := range options {
|
||||
options[i] = strings.TrimSpace(opt)
|
||||
}
|
||||
|
||||
fieldName := options[0]
|
||||
if fieldName == "" {
|
||||
panic(fmt.Sprintf("(%s).%s missing json field name", structType.String(), field.Name))
|
||||
}
|
||||
if fieldName == "-" && len(options) == 1 {
|
||||
return jsonTagInfo{Skip: true}
|
||||
}
|
||||
|
||||
info := jsonTagInfo{FieldName: fieldName}
|
||||
for _, opt := range options[1:] {
|
||||
if opt == "omitempty" {
|
||||
info.OmitEmpty = isJSONOmittableType(field.Type)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return info
|
||||
}
|
||||
|
@ -156,9 +156,9 @@ func getTypeNameRecursively(t reflect.Type, level int) string {
|
||||
var fields []string
|
||||
for i := 0; i < t.NumField(); i++ {
|
||||
field := t.Field(i)
|
||||
jsonTag := field.Tag.Get("json")
|
||||
if jsonTag != "" && jsonTag != "-" {
|
||||
fields = append(fields, prefix+"\t"+jsonTag+": "+getTypeNameRecursively(field.Type, level+1))
|
||||
jsonInfo := parseJSONTag(t, field)
|
||||
if !jsonInfo.Skip {
|
||||
fields = append(fields, prefix+"\t"+jsonInfo.FieldName+": "+getTypeNameRecursively(field.Type, level+1))
|
||||
}
|
||||
}
|
||||
return fmt.Sprintf("%s{\n%s\n%s}\n", prefix, strings.Join(fields, "\n"), prefix)
|
||||
|
@ -2,44 +2,25 @@
|
||||
// DO NOT EDIT.
|
||||
import { Time, UUID } from '@/types/common';
|
||||
|
||||
export class DocsGetResponseItem {
|
||||
id: UUID;
|
||||
path: string;
|
||||
date: Time;
|
||||
metadata: Metadata;
|
||||
last_retrievals?: DocsGetResponseItemLastRetrievals;
|
||||
}
|
||||
|
||||
export class DocsGetResponseItemLastRetrievalsItem {
|
||||
user: string;
|
||||
when: Time;
|
||||
}
|
||||
|
||||
export class DocsUpdateContentRequest {
|
||||
content: string;
|
||||
}
|
||||
|
||||
export class DocsUpdateContentResponse {
|
||||
id: UUID;
|
||||
date: Time;
|
||||
pathParam: string;
|
||||
body: string;
|
||||
}
|
||||
|
||||
export class Document {
|
||||
id: UUID;
|
||||
date: Time;
|
||||
pathParam: string;
|
||||
body: string;
|
||||
version: Version;
|
||||
metadata: Metadata;
|
||||
}
|
||||
|
||||
export class Metadata {
|
||||
owner: string;
|
||||
owner?: string;
|
||||
tags?: string[][];
|
||||
}
|
||||
|
||||
export class UsersGetResponseItem {
|
||||
export class NewDocument {
|
||||
content: string;
|
||||
}
|
||||
|
||||
export class User {
|
||||
name: string;
|
||||
surname: string;
|
||||
email: string;
|
||||
@ -50,14 +31,6 @@ export class Version {
|
||||
number: number;
|
||||
}
|
||||
|
||||
export type DocsGetResponse = Array<DocsGetResponseItem>
|
||||
|
||||
export type DocsGetResponseItemLastRetrievals = Array<DocsGetResponseItemLastRetrievalsItem>
|
||||
|
||||
export type UsersCreateRequest = Array<UsersGetResponseItem>
|
||||
|
||||
export type UsersGetResponse = Array<UsersGetResponseItem>
|
||||
|
||||
class APIError extends Error {
|
||||
constructor(
|
||||
public readonly msg: string,
|
||||
@ -86,12 +59,12 @@ export class DocumentsHttpApiV0 {
|
||||
this.respStatusCode = respStatusCode;
|
||||
}
|
||||
|
||||
public async get(): Promise<DocsGetResponse> {
|
||||
public async get(): Promise<Document[]> {
|
||||
if (this.respStatusCode !== 0) {
|
||||
throw new APIError('mock error message: ' + this.respStatusCode, this.respStatusCode);
|
||||
}
|
||||
|
||||
return JSON.parse('[{"id":"00000000-0000-0000-0000-000000000000","path":"/workspace/notes.md","date":"0001-01-01T00:00:00Z","metadata":{"owner":"Storj","tags":[["category","general"]]},"last_retrievals":[{"user":"Storj","when":"2001-02-03T03:05:06.000000007Z"}]}]') as DocsGetResponse;
|
||||
return JSON.parse('[{"id":"00000000-0000-0000-0000-000000000000","date":"0001-01-01T00:00:00Z","pathParam":"/workspace/notes.md","body":"","version":{"date":"0001-01-01T00:00:00Z","number":0},"metadata":{"owner":"Storj","tags":[["category","general"]]}}]') as Document[];
|
||||
}
|
||||
|
||||
public async getOne(path: string): Promise<Document> {
|
||||
@ -99,7 +72,7 @@ export class DocumentsHttpApiV0 {
|
||||
throw new APIError('mock error message: ' + this.respStatusCode, this.respStatusCode);
|
||||
}
|
||||
|
||||
return JSON.parse('{"id":"00000000-0000-0000-0000-000000000000","date":"2001-02-02T04:05:06.000000007Z","pathParam":"ID","body":"## Notes","version":{"date":"2001-02-03T03:35:06.000000007Z","number":1}}') as Document;
|
||||
return JSON.parse('{"id":"00000000-0000-0000-0000-000000000000","date":"2001-02-02T04:05:06.000000007Z","pathParam":"ID","body":"## Notes","version":{"date":"2001-02-03T03:35:06.000000007Z","number":1},"metadata":{"tags":null}}') as Document;
|
||||
}
|
||||
|
||||
public async getTag(path: string, tagName: string): Promise<string[]> {
|
||||
@ -118,12 +91,12 @@ export class DocumentsHttpApiV0 {
|
||||
return JSON.parse('[{"date":"2001-01-19T04:05:06.000000007Z","number":1},{"date":"2001-02-02T23:05:06.000000007Z","number":2}]') as Version[];
|
||||
}
|
||||
|
||||
public async updateContent(request: DocsUpdateContentRequest, path: string, id: UUID, date: Time): Promise<DocsUpdateContentResponse> {
|
||||
public async updateContent(request: NewDocument, path: string, id: UUID, date: Time): Promise<Document> {
|
||||
if (this.respStatusCode !== 0) {
|
||||
throw new APIError('mock error message: ' + this.respStatusCode, this.respStatusCode);
|
||||
}
|
||||
|
||||
return JSON.parse('{"id":"00000000-0000-0000-0000-000000000000","date":"2001-02-03T04:05:06.000000007Z","pathParam":"ID","body":"## Notes\n### General"}') as DocsUpdateContentResponse;
|
||||
return JSON.parse('{"id":"00000000-0000-0000-0000-000000000000","date":"2001-02-03T04:05:06.000000007Z","pathParam":"ID","body":"## Notes\n### General","version":{"date":"0001-01-01T00:00:00Z","number":0},"metadata":{"tags":null}}') as Document;
|
||||
}
|
||||
}
|
||||
|
||||
@ -146,15 +119,15 @@ export class UsersHttpApiV0 {
|
||||
this.respStatusCode = respStatusCode;
|
||||
}
|
||||
|
||||
public async get(): Promise<UsersGetResponse> {
|
||||
public async get(): Promise<User[]> {
|
||||
if (this.respStatusCode !== 0) {
|
||||
throw new APIError('mock error message: ' + this.respStatusCode, this.respStatusCode);
|
||||
}
|
||||
|
||||
return JSON.parse('[{"name":"Storj","surname":"Labs","email":"storj@storj.test"},{"name":"Test1","surname":"Testing","email":"test1@example.test"},{"name":"Test2","surname":"Testing","email":"test2@example.test"}]') as UsersGetResponse;
|
||||
return JSON.parse('[{"name":"Storj","surname":"Labs","email":"storj@storj.test"},{"name":"Test1","surname":"Testing","email":"test1@example.test"},{"name":"Test2","surname":"Testing","email":"test2@example.test"}]') as User[];
|
||||
}
|
||||
|
||||
public async create(request: UsersCreateRequest): Promise<void> {
|
||||
public async create(request: User[]): Promise<void> {
|
||||
if (this.respStatusCode !== 0) {
|
||||
throw new APIError('mock error message: ' + this.respStatusCode, this.respStatusCode);
|
||||
}
|
||||
|
@ -4,44 +4,25 @@
|
||||
import { HttpClient } from '@/utils/httpClient';
|
||||
import { Time, UUID } from '@/types/common';
|
||||
|
||||
export class DocsGetResponseItem {
|
||||
id: UUID;
|
||||
path: string;
|
||||
date: Time;
|
||||
metadata: Metadata;
|
||||
last_retrievals?: DocsGetResponseItemLastRetrievals;
|
||||
}
|
||||
|
||||
export class DocsGetResponseItemLastRetrievalsItem {
|
||||
user: string;
|
||||
when: Time;
|
||||
}
|
||||
|
||||
export class DocsUpdateContentRequest {
|
||||
content: string;
|
||||
}
|
||||
|
||||
export class DocsUpdateContentResponse {
|
||||
id: UUID;
|
||||
date: Time;
|
||||
pathParam: string;
|
||||
body: string;
|
||||
}
|
||||
|
||||
export class Document {
|
||||
id: UUID;
|
||||
date: Time;
|
||||
pathParam: string;
|
||||
body: string;
|
||||
version: Version;
|
||||
metadata: Metadata;
|
||||
}
|
||||
|
||||
export class Metadata {
|
||||
owner: string;
|
||||
owner?: string;
|
||||
tags?: string[][];
|
||||
}
|
||||
|
||||
export class UsersCreateRequestItem {
|
||||
export class NewDocument {
|
||||
content: string;
|
||||
}
|
||||
|
||||
export class User {
|
||||
name: string;
|
||||
surname: string;
|
||||
email: string;
|
||||
@ -52,14 +33,6 @@ export class Version {
|
||||
number: number;
|
||||
}
|
||||
|
||||
export type DocsGetResponse = Array<DocsGetResponseItem>
|
||||
|
||||
export type DocsGetResponseItemLastRetrievals = Array<DocsGetResponseItemLastRetrievalsItem>
|
||||
|
||||
export type UsersCreateRequest = Array<UsersCreateRequestItem>
|
||||
|
||||
export type UsersGetResponse = Array<UsersCreateRequestItem>
|
||||
|
||||
class APIError extends Error {
|
||||
constructor(
|
||||
public readonly msg: string,
|
||||
@ -73,11 +46,11 @@ export class DocumentsHttpApiV0 {
|
||||
private readonly http: HttpClient = new HttpClient();
|
||||
private readonly ROOT_PATH: string = '/api/v0/docs';
|
||||
|
||||
public async get(): Promise<DocsGetResponse> {
|
||||
public async get(): Promise<Document[]> {
|
||||
const fullPath = `${this.ROOT_PATH}/`;
|
||||
const response = await this.http.get(fullPath);
|
||||
if (response.ok) {
|
||||
return response.json().then((body) => body as DocsGetResponse);
|
||||
return response.json().then((body) => body as Document[]);
|
||||
}
|
||||
const err = await response.json();
|
||||
throw new APIError(err.error, response.status);
|
||||
@ -113,14 +86,14 @@ export class DocumentsHttpApiV0 {
|
||||
throw new APIError(err.error, response.status);
|
||||
}
|
||||
|
||||
public async updateContent(request: DocsUpdateContentRequest, path: string, id: UUID, date: Time): Promise<DocsUpdateContentResponse> {
|
||||
public async updateContent(request: NewDocument, path: string, id: UUID, date: Time): Promise<Document> {
|
||||
const u = new URL(`${this.ROOT_PATH}/${path}`, window.location.href);
|
||||
u.searchParams.set('id', id);
|
||||
u.searchParams.set('date', date);
|
||||
const fullPath = u.toString();
|
||||
const response = await this.http.post(fullPath, JSON.stringify(request));
|
||||
if (response.ok) {
|
||||
return response.json().then((body) => body as DocsUpdateContentResponse);
|
||||
return response.json().then((body) => body as Document);
|
||||
}
|
||||
const err = await response.json();
|
||||
throw new APIError(err.error, response.status);
|
||||
@ -131,17 +104,17 @@ export class UsersHttpApiV0 {
|
||||
private readonly http: HttpClient = new HttpClient();
|
||||
private readonly ROOT_PATH: string = '/api/v0/users';
|
||||
|
||||
public async get(): Promise<UsersGetResponse> {
|
||||
public async get(): Promise<User[]> {
|
||||
const fullPath = `${this.ROOT_PATH}/`;
|
||||
const response = await this.http.get(fullPath);
|
||||
if (response.ok) {
|
||||
return response.json().then((body) => body as UsersGetResponse);
|
||||
return response.json().then((body) => body as User[]);
|
||||
}
|
||||
const err = await response.json();
|
||||
throw new APIError(err.error, response.status);
|
||||
}
|
||||
|
||||
public async create(request: UsersCreateRequest): Promise<void> {
|
||||
public async create(request: User[]): Promise<void> {
|
||||
const fullPath = `${this.ROOT_PATH}/`;
|
||||
const response = await this.http.post(fullPath, JSON.stringify(request));
|
||||
if (response.ok) {
|
||||
|
@ -27,7 +27,7 @@ type Version struct {
|
||||
|
||||
// Metadata is metadata associated to a document.
|
||||
type Metadata struct {
|
||||
Owner string `json:"owner"`
|
||||
Owner string `json:"owner,omitempty"`
|
||||
Tags [][2]string `json:"tags"`
|
||||
}
|
||||
|
||||
|
@ -130,24 +130,17 @@ func (types *Types) GenerateTypescriptDefinitions() string {
|
||||
|
||||
for i := 0; i < t.Type.NumField(); i++ {
|
||||
field := t.Type.Field(i)
|
||||
attributes := strings.Fields(field.Tag.Get("json"))
|
||||
if len(attributes) == 0 || attributes[0] == "" {
|
||||
pathParts := strings.Split(t.Type.PkgPath(), "/")
|
||||
pkg := pathParts[len(pathParts)-1]
|
||||
panic(fmt.Sprintf("(%s.%s).%s missing json declaration", pkg, name, field.Name))
|
||||
}
|
||||
|
||||
jsonField := attributes[0]
|
||||
if jsonField == "-" {
|
||||
jsonInfo := parseJSONTag(t.Type, field)
|
||||
if jsonInfo.Skip {
|
||||
continue
|
||||
}
|
||||
|
||||
isOptional := ""
|
||||
if isNillableType(field.Type) {
|
||||
if isNillableType(field.Type) || jsonInfo.OmitEmpty {
|
||||
isOptional = "?"
|
||||
}
|
||||
|
||||
pf("\t%s%s: %s;", jsonField, isOptional, TypescriptTypeName(field.Type))
|
||||
pf("\t%s%s: %s;", jsonInfo.FieldName, isOptional, TypescriptTypeName(field.Type))
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user