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
|
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 {
|
func capitalize(s string) string {
|
||||||
r, size := utf8.DecodeRuneInString(s)
|
r, size := utf8.DecodeRuneInString(s)
|
||||||
if size <= 0 {
|
if size <= 0 {
|
||||||
@ -167,3 +181,39 @@ func filter(types []typeAndName, keep func(typeAndName) bool) []typeAndName {
|
|||||||
}
|
}
|
||||||
return filtered
|
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
|
var fields []string
|
||||||
for i := 0; i < t.NumField(); i++ {
|
for i := 0; i < t.NumField(); i++ {
|
||||||
field := t.Field(i)
|
field := t.Field(i)
|
||||||
jsonTag := field.Tag.Get("json")
|
jsonInfo := parseJSONTag(t, field)
|
||||||
if jsonTag != "" && jsonTag != "-" {
|
if !jsonInfo.Skip {
|
||||||
fields = append(fields, prefix+"\t"+jsonTag+": "+getTypeNameRecursively(field.Type, level+1))
|
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)
|
return fmt.Sprintf("%s{\n%s\n%s}\n", prefix, strings.Join(fields, "\n"), prefix)
|
||||||
|
@ -2,44 +2,25 @@
|
|||||||
// DO NOT EDIT.
|
// DO NOT EDIT.
|
||||||
import { Time, UUID } from '@/types/common';
|
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 {
|
export class Document {
|
||||||
id: UUID;
|
id: UUID;
|
||||||
date: Time;
|
date: Time;
|
||||||
pathParam: string;
|
pathParam: string;
|
||||||
body: string;
|
body: string;
|
||||||
version: Version;
|
version: Version;
|
||||||
|
metadata: Metadata;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Metadata {
|
export class Metadata {
|
||||||
owner: string;
|
owner?: string;
|
||||||
tags?: string[][];
|
tags?: string[][];
|
||||||
}
|
}
|
||||||
|
|
||||||
export class UsersGetResponseItem {
|
export class NewDocument {
|
||||||
|
content: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class User {
|
||||||
name: string;
|
name: string;
|
||||||
surname: string;
|
surname: string;
|
||||||
email: string;
|
email: string;
|
||||||
@ -50,14 +31,6 @@ export class Version {
|
|||||||
number: number;
|
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 {
|
class APIError extends Error {
|
||||||
constructor(
|
constructor(
|
||||||
public readonly msg: string,
|
public readonly msg: string,
|
||||||
@ -86,12 +59,12 @@ export class DocumentsHttpApiV0 {
|
|||||||
this.respStatusCode = respStatusCode;
|
this.respStatusCode = respStatusCode;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async get(): Promise<DocsGetResponse> {
|
public async get(): Promise<Document[]> {
|
||||||
if (this.respStatusCode !== 0) {
|
if (this.respStatusCode !== 0) {
|
||||||
throw new APIError('mock error message: ' + this.respStatusCode, this.respStatusCode);
|
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> {
|
public async getOne(path: string): Promise<Document> {
|
||||||
@ -99,7 +72,7 @@ export class DocumentsHttpApiV0 {
|
|||||||
throw new APIError('mock error message: ' + this.respStatusCode, this.respStatusCode);
|
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[]> {
|
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[];
|
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) {
|
if (this.respStatusCode !== 0) {
|
||||||
throw new APIError('mock error message: ' + this.respStatusCode, this.respStatusCode);
|
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;
|
this.respStatusCode = respStatusCode;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async get(): Promise<UsersGetResponse> {
|
public async get(): Promise<User[]> {
|
||||||
if (this.respStatusCode !== 0) {
|
if (this.respStatusCode !== 0) {
|
||||||
throw new APIError('mock error message: ' + this.respStatusCode, this.respStatusCode);
|
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) {
|
if (this.respStatusCode !== 0) {
|
||||||
throw new APIError('mock error message: ' + this.respStatusCode, this.respStatusCode);
|
throw new APIError('mock error message: ' + this.respStatusCode, this.respStatusCode);
|
||||||
}
|
}
|
||||||
|
@ -4,44 +4,25 @@
|
|||||||
import { HttpClient } from '@/utils/httpClient';
|
import { HttpClient } from '@/utils/httpClient';
|
||||||
import { Time, UUID } from '@/types/common';
|
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 {
|
export class Document {
|
||||||
id: UUID;
|
id: UUID;
|
||||||
date: Time;
|
date: Time;
|
||||||
pathParam: string;
|
pathParam: string;
|
||||||
body: string;
|
body: string;
|
||||||
version: Version;
|
version: Version;
|
||||||
|
metadata: Metadata;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Metadata {
|
export class Metadata {
|
||||||
owner: string;
|
owner?: string;
|
||||||
tags?: string[][];
|
tags?: string[][];
|
||||||
}
|
}
|
||||||
|
|
||||||
export class UsersCreateRequestItem {
|
export class NewDocument {
|
||||||
|
content: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class User {
|
||||||
name: string;
|
name: string;
|
||||||
surname: string;
|
surname: string;
|
||||||
email: string;
|
email: string;
|
||||||
@ -52,14 +33,6 @@ export class Version {
|
|||||||
number: number;
|
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 {
|
class APIError extends Error {
|
||||||
constructor(
|
constructor(
|
||||||
public readonly msg: string,
|
public readonly msg: string,
|
||||||
@ -73,11 +46,11 @@ export class DocumentsHttpApiV0 {
|
|||||||
private readonly http: HttpClient = new HttpClient();
|
private readonly http: HttpClient = new HttpClient();
|
||||||
private readonly ROOT_PATH: string = '/api/v0/docs';
|
private readonly ROOT_PATH: string = '/api/v0/docs';
|
||||||
|
|
||||||
public async get(): Promise<DocsGetResponse> {
|
public async get(): Promise<Document[]> {
|
||||||
const fullPath = `${this.ROOT_PATH}/`;
|
const fullPath = `${this.ROOT_PATH}/`;
|
||||||
const response = await this.http.get(fullPath);
|
const response = await this.http.get(fullPath);
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
return response.json().then((body) => body as DocsGetResponse);
|
return response.json().then((body) => body as Document[]);
|
||||||
}
|
}
|
||||||
const err = await response.json();
|
const err = await response.json();
|
||||||
throw new APIError(err.error, response.status);
|
throw new APIError(err.error, response.status);
|
||||||
@ -113,14 +86,14 @@ export class DocumentsHttpApiV0 {
|
|||||||
throw new APIError(err.error, response.status);
|
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);
|
const u = new URL(`${this.ROOT_PATH}/${path}`, window.location.href);
|
||||||
u.searchParams.set('id', id);
|
u.searchParams.set('id', id);
|
||||||
u.searchParams.set('date', date);
|
u.searchParams.set('date', date);
|
||||||
const fullPath = u.toString();
|
const fullPath = u.toString();
|
||||||
const response = await this.http.post(fullPath, JSON.stringify(request));
|
const response = await this.http.post(fullPath, JSON.stringify(request));
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
return response.json().then((body) => body as DocsUpdateContentResponse);
|
return response.json().then((body) => body as Document);
|
||||||
}
|
}
|
||||||
const err = await response.json();
|
const err = await response.json();
|
||||||
throw new APIError(err.error, response.status);
|
throw new APIError(err.error, response.status);
|
||||||
@ -131,17 +104,17 @@ export class UsersHttpApiV0 {
|
|||||||
private readonly http: HttpClient = new HttpClient();
|
private readonly http: HttpClient = new HttpClient();
|
||||||
private readonly ROOT_PATH: string = '/api/v0/users';
|
private readonly ROOT_PATH: string = '/api/v0/users';
|
||||||
|
|
||||||
public async get(): Promise<UsersGetResponse> {
|
public async get(): Promise<User[]> {
|
||||||
const fullPath = `${this.ROOT_PATH}/`;
|
const fullPath = `${this.ROOT_PATH}/`;
|
||||||
const response = await this.http.get(fullPath);
|
const response = await this.http.get(fullPath);
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
return response.json().then((body) => body as UsersGetResponse);
|
return response.json().then((body) => body as User[]);
|
||||||
}
|
}
|
||||||
const err = await response.json();
|
const err = await response.json();
|
||||||
throw new APIError(err.error, response.status);
|
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 fullPath = `${this.ROOT_PATH}/`;
|
||||||
const response = await this.http.post(fullPath, JSON.stringify(request));
|
const response = await this.http.post(fullPath, JSON.stringify(request));
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
|
@ -27,7 +27,7 @@ type Version struct {
|
|||||||
|
|
||||||
// Metadata is metadata associated to a document.
|
// Metadata is metadata associated to a document.
|
||||||
type Metadata struct {
|
type Metadata struct {
|
||||||
Owner string `json:"owner"`
|
Owner string `json:"owner,omitempty"`
|
||||||
Tags [][2]string `json:"tags"`
|
Tags [][2]string `json:"tags"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -130,24 +130,17 @@ func (types *Types) GenerateTypescriptDefinitions() string {
|
|||||||
|
|
||||||
for i := 0; i < t.Type.NumField(); i++ {
|
for i := 0; i < t.Type.NumField(); i++ {
|
||||||
field := t.Type.Field(i)
|
field := t.Type.Field(i)
|
||||||
attributes := strings.Fields(field.Tag.Get("json"))
|
jsonInfo := parseJSONTag(t.Type, field)
|
||||||
if len(attributes) == 0 || attributes[0] == "" {
|
if jsonInfo.Skip {
|
||||||
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 == "-" {
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
isOptional := ""
|
isOptional := ""
|
||||||
if isNillableType(field.Type) {
|
if isNillableType(field.Type) || jsonInfo.OmitEmpty {
|
||||||
isOptional = "?"
|
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