jschema.odin (4153B)
1 // Package jschema generates Odin type declarations from a JSON Schema 2 // document (draft-07 or 2020-12). The generated types parse with 3 // core:encoding/json. 4 package jschema 5 6 import "core:encoding/json" 7 import "core:mem" 8 import "core:mem/virtual" 9 import "core:os" 10 import "core:path/filepath" 11 import "core:strings" 12 13 Options :: struct { 14 package_name: string, // package of the generated file; default "schema" 15 root_name: string, // name of the root declaration; default "Root" 16 openapi: Maybe(bool), // nil = auto-detect via "openapi" property 17 } 18 19 Error :: union { 20 IO_Error, 21 Parse_Error, 22 Resolve_Error, 23 mem.Allocator_Error, 24 } 25 26 // A file could not be read or written. 27 IO_Error :: struct { 28 path: string, 29 error: os.Error, 30 } 31 32 // The schema document is not valid JSON or not a valid schema. 33 Parse_Error :: struct { 34 path: string, 35 pos: json.Pos, 36 message: string, 37 } 38 39 // A $ref points at something that does not exist. 40 Resolve_Error :: struct { 41 path: string, // file containing the ref 42 ref: string, // the $ref as written 43 } 44 45 // Generates an Odin source file from the JSON schema file at schema_path. 46 // Relative $refs are resolved against the schema file's directory. 47 generate :: proc(schema_path: string, output_path: string, opts := Options{}) -> Error { 48 data, rerr := os.read_entire_file_from_path(schema_path, context.allocator) 49 if rerr != nil { 50 return IO_Error{path = schema_path, error = rerr} 51 } 52 defer delete(data) 53 54 // filepath.dir returns a substring of schema_path; nothing to free. 55 source, err := generate_source(data, opts, filepath.dir(schema_path)) 56 if err != nil { 57 return err 58 } 59 defer delete(source) 60 61 if werr := os.write_entire_file(output_path, source); werr != nil { 62 return IO_Error{path = output_path, error = werr} 63 } 64 return nil 65 } 66 67 // Generates Odin source from an in-memory JSON schema document. Relative 68 // $refs are resolved against base_dir (the current directory when empty). 69 // The returned source is allocated with `allocator`; everything else lives in 70 // an internal arena that is freed before returning. 71 generate_source :: proc( 72 data: []u8, 73 opts := Options{}, 74 base_dir := "", 75 allocator := context.allocator, 76 ) -> (source: string, err: Error) { 77 arena: virtual.Arena 78 if aerr := virtual.arena_init_growing(&arena); aerr != nil { 79 return "", aerr 80 } 81 defer virtual.arena_destroy(&arena) 82 83 arena_source: string 84 arena_source, err = generate_in_arena(&arena, data, opts, base_dir) 85 if err != nil { 86 return "", clone_error(err, allocator) 87 } 88 return strings.clone(arena_source, allocator), nil 89 } 90 91 @(private) 92 generate_in_arena :: proc( 93 arena: ^virtual.Arena, 94 data: []u8, 95 opts: Options, 96 base_dir: string, 97 ) -> (source: string, err: Error) { 98 context.allocator = virtual.arena_allocator(arena) 99 context.temp_allocator = context.allocator 100 101 pool: Pool 102 b := Builder { 103 pool = &pool, 104 openapi = resolve_openapi(opts, data), 105 } 106 107 dir := base_dir if base_dir != "" else "." 108 pool.root = parse_document(&b, data, "<schema>", "", dir) or_return 109 resolve_refs(&b) or_return 110 return emit(&pool, opts, b.openapi), nil 111 } 112 113 // Resolves the OpenAPI-idiom flag: an explicit value in opts takes 114 // priority; otherwise the document is shallow-scanned for a top-level 115 // "openapi" property (the OpenAPI version marker). 116 @(private) 117 resolve_openapi :: proc(opts: Options, data: []u8) -> bool { 118 switch v in opts.openapi { 119 case bool: 120 return v 121 case: 122 return detect_openapi(data) 123 } 124 } 125 126 // Error strings can point into the internal arena; clone them so the error 127 // outlives generate_source. 128 @(private) 129 clone_error :: proc(err: Error, allocator: mem.Allocator) -> Error { 130 switch specific in err { 131 case IO_Error: 132 return IO_Error{path = strings.clone(specific.path, allocator), error = specific.error} 133 case Parse_Error: 134 return Parse_Error { 135 path = strings.clone(specific.path, allocator), 136 pos = specific.pos, 137 message = strings.clone(specific.message, allocator), 138 } 139 case Resolve_Error: 140 return Resolve_Error { 141 path = strings.clone(specific.path, allocator), 142 ref = strings.clone(specific.ref, allocator), 143 } 144 case mem.Allocator_Error: 145 return specific 146 } 147 return nil 148 }