odin-jsonschema

Implementation of JSON schema for Odin
Log | Files | Refs | LICENSE

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 }